first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
.taxonomy-term-preview {
background-color: #eee;
}
.taxonomy-term-divider-top {
border-bottom: none;
}
.taxonomy-term-divider-bottom {
border-top: 1px dotted #ccc;
}

View File

@@ -0,0 +1,36 @@
---
label: 'Configuring taxonomy'
related:
- taxonomy.overview
- field_ui.reference_field
- field_ui.manage_display
- field_ui.manage_form
---
{% set taxonomy_permissions_link_text %}
{% trans %}Administer vocabularies and terms{% endtrans %}
{% endset %}
{% set taxonomy_permissions_link = render_var(help_route_link(taxonomy_permissions_link_text, 'user.admin_permissions.module', {'modules': 'taxonomy'})) %}
{% set taxonomy_admin_link_text %}
{% trans %}Taxonomy{% endtrans %}
{% endset %}
{% set taxonomy_admin_link = render_var(help_route_link(taxonomy_admin_link_text, 'entity.taxonomy_vocabulary.collection')) %}
{% set taxonomy_overview_topic = render_var(help_topic_link('taxonomy.overview')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a taxonomy vocabulary and add a reference field for that vocabulary to a content entity. See {{ taxonomy_overview_topic }} for information about taxonomy and {{ content_structure_topic }} for more on content entities.{% endtrans %}</p>
<h2>{% trans %}Who can configure a taxonomy vocabulary?{% endtrans %}</h2>
<p>{% trans %}Users with the <em>{{ taxonomy_permissions_link }}</em> permission can configure a vocabulary. To add a field in the administrative interface, the core Field UI module must be installed, and you will also need the <em>Administer fields</em> permission for the entity you are adding the field to.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ taxonomy_admin_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add vocabulary</em>.{% endtrans %}</li>
<li>{% trans %}In the <em>Name</em> field, enter a name for the vocabulary (for example "Ingredients"), which is how it will be shown in the administrative interface. Optionally, add a description.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. Your vocabulary will be created and you will see the page that lists all the terms in this vocabulary.{% endtrans %}</li>
<li>{% trans %}Optionally, click <em>Add term</em> to add a term to the new vocabulary. In the <em>Name</em> field, enter the term name (for example "Butter"). Click <em>Save</em>. You will receive a confirmation about the term you created. You may optionally continue to add more terms.{% endtrans %}</li>
<li>{% trans %}If you have the Field UI module installed, follow the instructions on the related <em>Adding a reference field to an entity sub-type</em> topic to add a taxonomy reference field to the entity type of your choice. The settings page will allow you to choose the <em>Vocabulary</em> that you created as the vocabulary to reference.{% endtrans %}</li>
<li>{% trans %}You may also want to configure the display and form display of the new field (see related topics).{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/user_guide/en/structure-taxonomy-setup.html">{% trans %}Setting Up a Taxonomy (Drupal User Guide){% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,22 @@
---
label: 'Managing taxonomy'
top_level: true
---
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}What is taxonomy?{% endtrans %}</h2>
<p>{% trans %}<em>Taxonomy</em> is used to classify website content. One common example of taxonomy is the tags used to classify or categorize posts in a blog website; a cooking website could use an ingredients taxonomy to classify recipes. Individual taxonomy items are known as <em>terms</em> (the blog tags or recipe ingredients in these examples); and a set of terms is known as a <em>vocabulary</em> (the set of all blog post tags, or the set of all recipe ingredients in these examples). Technically, taxonomy terms are an entity type and the entity subtypes are the vocabularies; see {{ content_structure_topic }} for more on content entities. Like other entities, taxonomy terms can have fields attached; for instance, you could set up an image field to contain an icon for each term.{% endtrans %}</p>
<p>{% trans %}An individual vocabulary can organize its terms in a hierarchy, or it could be flat. For example, blog tags normally have a flat structure, while a recipe ingredients vocabulary could be hierarchical (for example, tomatoes could be a sub-term of vegetables, and under tomatoes, you could have green and red tomatoes).{% endtrans %}</p>
<p>{% trans %}Taxonomy terms are normally attached as reference fields to other content entities, which is how you can use them to classify content. When you set up a taxonomy reference field, you can let users enter terms in two ways:{% endtrans %}</p>
<dl>
<dt>{% trans %}Free tagging{% endtrans %}</dt>
<dd>{% trans %}New terms can be created right on the content editing form.{% endtrans %}</dd>
<dt>{% trans %}Fixed list of terms{% endtrans %}</dt>
<dd>{% trans %}The list of terms is curated and managed outside the content editing form, and users can only select from the existing list when editing content.{% endtrans %}</dd>
</dl>
<p>{% trans %}Taxonomy reference fields can be added to any entity, such as user accounts, content blocks, or regular content items. If you use them to classify regular content items, your site will automatically be set up with taxonomy listing pages for each term; each of these pages lists all of the content items that are classified with that term.{% endtrans %}</p>
<h2>{% trans %}Overview of managing taxonomy{% endtrans %}</h2>
<p>{% trans %}The core Taxonomy module allows you to create and edit taxonomy vocabularies and taxonomy terms. The core Field UI module provides a user interface for adding fields to entities, including the taxonomy reference field, and configuring field editing and display. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/user_guide/en/structure-taxonomy.html">{% trans %}Concept: Taxonomy (Drupal User Guide){% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,39 @@
id: d6_taxonomy_term
label: Taxonomy terms
audit: true
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_taxonomy_term
process:
# If you are using this file to build a custom migration consider removing
# the tid and revision_id fields to allow incremental migrations.
tid: tid
revision_id: tid
vid:
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
name: name
description: description
weight: weight
# Only attempt to stub real (non-zero) parents.
parent_id:
-
plugin: skip_on_empty
method: process
source: parent
-
plugin: migration_lookup
migration: d6_taxonomy_term
parent:
plugin: default_value
default_value: 0
source: '@parent_id'
changed: timestamp
destination:
plugin: entity:taxonomy_term
migration_dependencies:
required:
- d6_taxonomy_vocabulary

View File

@@ -0,0 +1,24 @@
id: d6_taxonomy_vocabulary
label: Taxonomy vocabularies
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary
process:
vid:
-
plugin: machine_name
source: name
-
plugin: make_unique_entity_field
entity_type: taxonomy_vocabulary
field: vid
length: 30
migrated: true
label: name
name: name
description: description
weight: weight
destination:
plugin: entity:taxonomy_vocabulary

View File

@@ -0,0 +1,30 @@
id: d6_term_node
label: Term/node relationships
migration_tags:
- Drupal 6
- Content
deriver: Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver
source:
plugin: d6_term_node
process:
nid:
-
plugin: migration_lookup
migration:
- d6_node_complete
- d6_node
source: nid
-
plugin: node_complete_node_lookup
-
plugin: skip_on_empty
method: row
type: type
# The actual field name is dynamic and will be added by the builder.
destination:
plugin: entity:node
migration_dependencies:
required:
- d6_vocabulary_entity_display
- d6_vocabulary_entity_form_display
- d6_node

View File

@@ -0,0 +1,30 @@
id: d6_term_node_revision
label: Term/node relationship revisions
audit: true
migration_tags:
- Drupal 6
- Content
deriver: Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver
source:
plugin: d6_term_node_revision
process:
vid:
-
plugin: migration_lookup
migration:
- d6_node_complete
- d6_node_revision
source: vid
-
plugin: node_complete_node_revision_lookup
-
plugin: skip_on_empty
method: row
type: type
# The actual field name is dynamic and will be added by the builder.
destination:
plugin: entity_revision:node
migration_dependencies:
required:
- d6_term_node
- d6_node_revision

View File

@@ -0,0 +1,52 @@
id: d6_vocabulary_entity_display
label: Vocabulary display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary_per_type
constants:
entity_type: node
view_mode: default
options:
label: hidden
type: entity_reference_label
weight: 20
field_prefix: field_
process:
entity_type: 'constants/entity_type'
view_mode: 'constants/view_mode'
options: 'constants/options'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: type
-
plugin: skip_on_empty
method: row
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
destination:
plugin: component_entity_display
migration_dependencies:
required:
- d6_vocabulary_field_instance

View File

@@ -0,0 +1,56 @@
id: d6_vocabulary_entity_form_display
label: Vocabulary form display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary_per_type
constants:
entity_type: node
form_mode: default
options:
weight: 20
field_prefix: field_
process:
entity_type: 'constants/entity_type'
form_mode: 'constants/form_mode'
options/type:
plugin: static_map
source: tags
map:
0: options_select
1: entity_reference_autocomplete_tags
options/weight: 'constants/options/weight'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: type
-
plugin: skip_on_empty
method: row
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- d6_vocabulary_field_instance

View File

@@ -0,0 +1,45 @@
id: d6_vocabulary_field
label: Vocabulary field configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary
constants:
entity_type: node
type: entity_reference
target_entity_type: taxonomy_term
field_prefix: field_
process:
entity_type: 'constants/entity_type'
type: 'constants/type'
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
'settings/target_type': 'constants/target_entity_type'
cardinality: cardinality
destination:
plugin: entity:field_storage_config
dependencies:
module:
- entity_reference
migration_dependencies:
required:
- d6_taxonomy_vocabulary

View File

@@ -0,0 +1,78 @@
# cspell:ignore localizable
id: d6_vocabulary_field_instance
label: Vocabulary field instance configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary_per_type
constants:
entity_type: node
auto_create: true
selection_handler: 'default:taxonomy_term'
field_prefix: field_
process:
entity_type: 'constants/entity_type'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: type
-
plugin: skip_on_empty
method: row
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
label: name
_vid:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
'settings/handler': 'constants/selection_handler'
'settings/handler_settings/target_bundles':
plugin: target_bundle
'settings/handler_settings/auto_create': 'constants/auto_create'
required: required
# Get the i18n taxonomy translation setting for this vocabulary.
# 0 - No multilingual options
# 1 - Localizable terms. Run through the localization system.
# 2 - Predefined language for a vocabulary and its terms.
# 3 - Per-language terms, translatable (referencing terms with different
# languages) but not localizable.
translatable:
plugin: static_map
source: i18ntaxonomy_vocabulary
default_value: 0
map:
0: false
1: true
2: false
3: true
destination:
plugin: entity:field_config
migration_dependencies:
required:
- d6_node_type
- d6_vocabulary_field

View File

@@ -0,0 +1,44 @@
id: d7_taxonomy_term
label: Taxonomy terms
audit: true
migration_tags:
- Drupal 7
- Content
deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver
source:
plugin: d7_taxonomy_term
process:
# If you are using this file to build a custom migration consider removing
# the tid and revision_id fields to allow incremental migrations.
tid: tid
revision_id: tid
vid:
plugin: migration_lookup
migration: d7_taxonomy_vocabulary
source: vid
name: name
'description/value': description
'description/format': format
weight: weight
# Only attempt to stub real (non-zero) parents.
parent_id:
-
plugin: skip_on_empty
method: process
source: parent
-
plugin: migration_lookup
migration: d7_taxonomy_term
parent:
plugin: default_value
default_value: 0
source: '@parent_id'
changed: timestamp
langcode: language
destination:
plugin: entity:taxonomy_term
migration_dependencies:
required:
- d7_taxonomy_vocabulary
optional:
- d7_field_instance

View File

@@ -0,0 +1,21 @@
id: d7_taxonomy_vocabulary
label: Taxonomy vocabularies
migration_tags:
- Drupal 7
- Configuration
source:
plugin: d7_taxonomy_vocabulary
process:
vid:
plugin: make_unique_entity_field
source: machine_name
entity_type: taxonomy_vocabulary
field: vid
length: 30
migrated: true
label: name
name: name
description: description
weight: weight
destination:
plugin: entity:taxonomy_vocabulary

View File

@@ -0,0 +1,9 @@
finished:
6:
taxonomy:
- core
- taxonomy
7:
taxonomy:
- core
- taxonomy

View File

@@ -0,0 +1,23 @@
id: taxonomy_settings
label: Taxonomy configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- taxonomy_override_selector
- taxonomy_terms_per_page_admin
- taxonomy_maintain_index_table
source_module: taxonomy
process:
override_selector: taxonomy_override_selector
terms_per_page_admin: taxonomy_terms_per_page_admin
maintain_index_table:
plugin: default_value
default_value: true
source: taxonomy_maintain_index_table
destination:
plugin: config
config_name: taxonomy.settings

View File

@@ -0,0 +1,75 @@
<?php
namespace Drupal\taxonomy\ContextProvider;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\taxonomy\Entity\Term;
/**
* Sets the current taxonomy term as a context on taxonomy term routes.
*/
class TermRouteContext implements ContextProviderInterface {
use StringTranslationTrait;
/**
* The route match object.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new TermRouteContext.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
$result = [];
$context_definition = EntityContextDefinition::create('taxonomy_term')->setRequired(FALSE);
$value = NULL;
if ($route_object = $this->routeMatch->getRouteObject()) {
$route_parameters = $route_object->getOption('parameters');
if (isset($route_parameters['taxonomy_term']) && $term = $this->routeMatch->getParameter('taxonomy_term')) {
$value = $term;
}
elseif ($this->routeMatch->getRouteName() == 'entity.taxonomy_term.add_form') {
$vocabulary = $this->routeMatch->getParameter('taxonomy_vocabulary');
$value = Term::create(['vid' => $vocabulary->id()]);
}
}
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['route']);
$context = new Context($context_definition, $value);
$context->addCacheableDependency($cacheability);
$result['taxonomy_term'] = $context;
return $result;
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
$context = EntityContext::fromEntityTypeId('taxonomy_term', $this->t('Term from URL'));
return ['taxonomy_term' => $context];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\taxonomy\Controller;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\VocabularyInterface;
/**
* Provides route responses for taxonomy.module.
*/
class TaxonomyController extends ControllerBase {
/**
* Returns a form to add a new term to a vocabulary.
*
* @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
* The vocabulary this term will be added to.
*
* @return array
* The taxonomy term add form.
*/
public function addForm(VocabularyInterface $taxonomy_vocabulary) {
$term = $this->entityTypeManager()->getStorage('taxonomy_term')->create(['vid' => $taxonomy_vocabulary->id()]);
return $this->entityFormBuilder()->getForm($term);
}
/**
* Route title callback.
*
* @param \Drupal\taxonomy\TermInterface $taxonomy_term
* The taxonomy term.
*
* @return array
* The term label as a render array.
*/
public function termTitle(TermInterface $taxonomy_term) {
return ['#markup' => $taxonomy_term->getName(), '#allowed_tags' => Xss::getHtmlTagList()];
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Drupal\taxonomy\Entity\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
use Symfony\Component\Routing\Route;
class VocabularyRouteProvider extends AdminHtmlRouteProvider {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$collection = parent::getRoutes($entity_type);
if ($reset_page_route = $this->getResetPageRoute($entity_type)) {
$collection->add("entity.taxonomy_vocabulary.reset_form", $reset_page_route);
}
if ($overview_page_route = $this->getOverviewPageRoute($entity_type)) {
$collection->add("entity.taxonomy_vocabulary.overview_form", $overview_page_route);
}
return $collection;
}
/**
* Gets the reset page route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getResetPageRoute(EntityTypeInterface $entity_type) {
$route = new Route('/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/reset');
$route->setDefault('_entity_form', 'taxonomy_vocabulary.reset');
$route->setDefault('_title', 'Reset');
$route->setRequirement('_entity_access', 'taxonomy_vocabulary.reset all weights');
$route->setOption('_admin_route', TRUE);
$route->setOption('parameters', [
'taxonomy_vocabulary' => [
'with_config_overrides' => TRUE,
],
]);
return $route;
}
/**
* Gets the overview page route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getOverviewPageRoute(EntityTypeInterface $entity_type) {
$route = new Route('/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/overview');
$route->setDefault('_title_callback', '\Drupal\Core\Entity\Controller\EntityController::title');
$route->setDefault('_form', 'Drupal\taxonomy\Form\OverviewTerms');
$route->setRequirement('_entity_access', 'taxonomy_vocabulary.access taxonomy overview');
$route->setOption('_admin_route', TRUE);
$route->setOption('parameters', [
'taxonomy_vocabulary' => [
'with_config_overrides' => TRUE,
],
]);
return $route;
}
}

View File

@@ -0,0 +1,286 @@
<?php
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\taxonomy\TermInterface;
use Drupal\user\StatusItem;
/**
* Defines the taxonomy term entity.
*
* @ContentEntityType(
* id = "taxonomy_term",
* label = @Translation("Taxonomy term"),
* label_collection = @Translation("Taxonomy terms"),
* label_singular = @Translation("taxonomy term"),
* label_plural = @Translation("taxonomy terms"),
* label_count = @PluralTranslation(
* singular = "@count taxonomy term",
* plural = "@count taxonomy terms",
* ),
* bundle_label = @Translation("Vocabulary"),
* handlers = {
* "storage" = "Drupal\taxonomy\TermStorage",
* "storage_schema" = "Drupal\taxonomy\TermStorageSchema",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
* "access" = "Drupal\taxonomy\TermAccessControlHandler",
* "views_data" = "Drupal\taxonomy\TermViewsData",
* "form" = {
* "default" = "Drupal\taxonomy\TermForm",
* "delete" = "Drupal\taxonomy\Form\TermDeleteForm",
* "revision-delete" = \Drupal\Core\Entity\Form\RevisionDeleteForm::class,
* "revision-revert" = \Drupal\Core\Entity\Form\RevisionRevertForm::class,
* },
* "route_provider" = {
* "revision" = \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class,
* },
* "translation" = "Drupal\taxonomy\TermTranslationHandler"
* },
* base_table = "taxonomy_term_data",
* data_table = "taxonomy_term_field_data",
* revision_table = "taxonomy_term_revision",
* revision_data_table = "taxonomy_term_field_revision",
* show_revision_ui = TRUE,
* translatable = TRUE,
* entity_keys = {
* "id" = "tid",
* "revision" = "revision_id",
* "bundle" = "vid",
* "label" = "name",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "published" = "status",
* },
* revision_metadata_keys = {
* "revision_user" = "revision_user",
* "revision_created" = "revision_created",
* "revision_log_message" = "revision_log_message",
* },
* bundle_entity_type = "taxonomy_vocabulary",
* field_ui_base_route = "entity.taxonomy_vocabulary.overview_form",
* common_reference_target = TRUE,
* links = {
* "canonical" = "/taxonomy/term/{taxonomy_term}",
* "delete-form" = "/taxonomy/term/{taxonomy_term}/delete",
* "edit-form" = "/taxonomy/term/{taxonomy_term}/edit",
* "create" = "/taxonomy/term",
* "revision" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/view",
* "revision-delete-form" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/delete",
* "revision-revert-form" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/revert",
* "version-history" = "/taxonomy/term/{taxonomy_term}/revisions",
* },
* permission_granularity = "bundle",
* collection_permission = "access taxonomy overview",
* constraints = {
* "TaxonomyHierarchy" = {}
* }
* )
*/
class Term extends EditorialContentEntityBase implements TermInterface {
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// See if any of the term's children are about to be become orphans.
$orphans = [];
/** @var \Drupal\taxonomy\TermInterface $term */
foreach ($entities as $tid => $term) {
if ($children = $storage->getChildren($term)) {
/** @var \Drupal\taxonomy\TermInterface $child */
foreach ($children as $child) {
$parent = $child->get('parent');
// Update child parents item list.
$parent->filter(function ($item) use ($tid) {
return $item->target_id != $tid;
});
// If the term has multiple parents, we don't delete it.
if ($parent->count()) {
$child->save();
}
else {
$orphans[] = $child;
}
}
}
}
if (!empty($orphans)) {
$storage->delete($orphans);
}
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Terms with no parents are mandatory children of <root>.
if (!$this->get('parent')->count()) {
$this->parent->target_id = 0;
}
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
// @todo Remove the usage of StatusItem in
// https://www.drupal.org/project/drupal/issues/2936864.
$fields['status']->getItemDefinition()->setClass(StatusItem::class);
$fields['tid']->setLabel(t('Term ID'))
->setDescription(t('The term ID.'));
$fields['uuid']->setDescription(t('The term UUID.'));
$fields['status']
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => TRUE,
],
'weight' => 100,
])
->setDisplayConfigurable('form', TRUE);
$fields['vid']->setLabel(t('Vocabulary'))
->setDescription(t('The vocabulary to which the term is assigned.'));
$fields['langcode']->setDescription(t('The term language code.'));
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setRequired(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE);
$fields['description'] = BaseFieldDefinition::create('text_long')
->setLabel(t('Description'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'text_default',
'weight' => 0,
])
->setDisplayConfigurable('view', TRUE)
->setDisplayOptions('form', [
'type' => 'text_textfield',
'weight' => 0,
])
->setDisplayConfigurable('form', TRUE);
$fields['weight'] = BaseFieldDefinition::create('integer')
->setLabel(t('Weight'))
->setDescription(t('The weight of this term in relation to other terms.'))
->setDefaultValue(0);
$fields['parent'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Term Parents'))
->setDescription(t('The parents of this term.'))
->setSetting('target_type', 'taxonomy_term')
->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the term was last edited.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE);
return $fields;
}
/**
* {@inheritdoc}
*/
public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
// Only terms in the same bundle can be a parent.
$fields['parent'] = clone $base_field_definitions['parent'];
$fields['parent']->setSetting('handler_settings', ['target_bundles' => [$bundle => $bundle]]);
return $fields;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->get('description')->value;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
$this->set('description', $description);
return $this;
}
/**
* {@inheritdoc}
*/
public function getFormat() {
return $this->get('description')->format;
}
/**
* {@inheritdoc}
*/
public function setFormat($format) {
$this->get('description')->format = $format;
return $this;
}
/**
* {@inheritdoc}
*/
public function getName() {
return $this->label() ?? '';
}
/**
* {@inheritdoc}
*/
public function setName($name) {
$this->set('name', $name);
return $this;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return (int) $this->get('weight')->value;
}
/**
* {@inheritdoc}
*/
public function setWeight($weight) {
$this->set('weight', $weight);
return $this;
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\taxonomy\VocabularyInterface;
/**
* Defines the taxonomy vocabulary entity.
*
* @ConfigEntityType(
* id = "taxonomy_vocabulary",
* label = @Translation("Taxonomy vocabulary"),
* label_singular = @Translation("vocabulary"),
* label_plural = @Translation("vocabularies"),
* label_collection = @Translation("Taxonomy"),
* label_count = @PluralTranslation(
* singular = "@count vocabulary",
* plural = "@count vocabularies"
* ),
* handlers = {
* "storage" = "Drupal\taxonomy\VocabularyStorage",
* "list_builder" = "Drupal\taxonomy\VocabularyListBuilder",
* "access" = "Drupal\taxonomy\VocabularyAccessControlHandler",
* "form" = {
* "default" = "Drupal\taxonomy\VocabularyForm",
* "reset" = "Drupal\taxonomy\Form\VocabularyResetForm",
* "delete" = "Drupal\taxonomy\Form\VocabularyDeleteForm",
* "overview" = "Drupal\taxonomy\Form\OverviewTerms"
* },
* "route_provider" = {
* "html" = "Drupal\taxonomy\Entity\Routing\VocabularyRouteProvider",
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProvider",
* }
* },
* admin_permission = "administer taxonomy",
* collection_permission = "access taxonomy overview",
* config_prefix = "vocabulary",
* bundle_of = "taxonomy_term",
* entity_keys = {
* "id" = "vid",
* "label" = "name",
* "weight" = "weight"
* },
* links = {
* "add-form" = "/admin/structure/taxonomy/add",
* "delete-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/delete",
* "reset-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/reset",
* "overview-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/overview",
* "edit-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}",
* "entity-permissions-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/overview/permissions",
* "collection" = "/admin/structure/taxonomy",
* },
* config_export = {
* "name",
* "vid",
* "description",
* "weight",
* "new_revision",
* }
* )
*/
class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface {
/**
* The taxonomy vocabulary ID.
*
* @var string
*/
protected $vid;
/**
* Name of the vocabulary.
*
* @var string
*/
protected $name;
/**
* Description of the vocabulary.
*
* @var string|null
*/
protected $description = NULL;
/**
* The weight of this vocabulary in relation to other vocabularies.
*
* @var int
*/
protected $weight = 0;
/**
* {@inheritdoc}
*/
public function id() {
return $this->vid;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description ?? '';
}
/**
* The default revision setting for a vocabulary.
*
* @var bool
*/
protected $new_revision = FALSE;
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
// Only load terms without a parent, child terms will get deleted too.
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $term_storage->loadMultiple($storage->getToplevelTids(array_keys($entities)));
$term_storage->delete($terms);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// Reset caches.
$storage->resetCache(array_keys($entities));
if (reset($entities)->isSyncing()) {
return;
}
$vocabularies = [];
foreach ($entities as $vocabulary) {
$vocabularies[$vocabulary->id()] = $vocabulary->id();
}
// Load all Taxonomy module fields and delete those which use only this
// vocabulary.
$field_storages = \Drupal::entityTypeManager()->getStorage('field_storage_config')->loadByProperties(['module' => 'taxonomy']);
foreach ($field_storages as $field_storage) {
$modified_storage = FALSE;
// Term reference fields may reference terms from more than one
// vocabulary.
foreach ($field_storage->getSetting('allowed_values') as $key => $allowed_value) {
if (isset($vocabularies[$allowed_value['vocabulary']])) {
$allowed_values = $field_storage->getSetting('allowed_values');
unset($allowed_values[$key]);
$field_storage->setSetting('allowed_values', $allowed_values);
$modified_storage = TRUE;
}
}
if ($modified_storage) {
$allowed_values = $field_storage->getSetting('allowed_values');
if (empty($allowed_values)) {
$field_storage->delete();
}
else {
// Update the field definition with the new allowed values.
$field_storage->save();
}
}
}
}
/**
* {@inheritdoc}
*/
public function setNewRevision($new_revision) {
$this->new_revision = $new_revision;
}
/**
* {@inheritdoc}
*/
public function shouldCreateNewRevision() {
return $this->new_revision;
}
}

View File

@@ -0,0 +1,609 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Pager\PagerManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\taxonomy\VocabularyInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides terms overview form for a taxonomy vocabulary.
*
* @internal
*/
class OverviewTerms extends FormBase {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The term storage handler.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $storageController;
/**
* The term list builder.
*
* @var \Drupal\Core\Entity\EntityListBuilderInterface
*/
protected $termListBuilder;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The pager manager.
*
* @var \Drupal\Core\Pager\PagerManagerInterface
*/
protected $pagerManager;
/**
* Constructs an OverviewTerms object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Pager\PagerManagerInterface $pager_manager
* The pager manager.
*/
public function __construct(ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, EntityRepositoryInterface $entity_repository, PagerManagerInterface $pager_manager) {
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entity_type_manager;
$this->storageController = $entity_type_manager->getStorage('taxonomy_term');
$this->termListBuilder = $entity_type_manager->getListBuilder('taxonomy_term');
$this->renderer = $renderer;
$this->entityRepository = $entity_repository;
$this->pagerManager = $pager_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('entity.repository'),
$container->get('pager.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_overview_terms';
}
/**
* Form constructor.
*
* Display a tree of all the terms in a vocabulary, with options to edit
* each one. The form is made drag and drop by the theme function.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
* The vocabulary to display the overview form for.
*
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?VocabularyInterface $taxonomy_vocabulary = NULL) {
$form_state->set(['taxonomy', 'vocabulary'], $taxonomy_vocabulary);
$vocabulary_hierarchy = $this->storageController->getVocabularyHierarchyType($taxonomy_vocabulary->id());
$parent_fields = FALSE;
$page = $this->pagerManager->findPage();
// Number of terms per page.
$page_increment = $this->config('taxonomy.settings')->get('terms_per_page_admin');
// Elements shown on this page.
$page_entries = 0;
// Elements at the root level before this page.
$before_entries = 0;
// Elements at the root level after this page.
$after_entries = 0;
// Elements at the root level on this page.
$root_entries = 0;
// Terms from previous and next pages are shown if the term tree would have
// been cut in the middle. Keep track of how many extra terms we show on
// each page of terms.
$back_step = NULL;
$forward_step = 0;
// An array of the terms to be displayed on this page.
$current_page = [];
$delta = 0;
$term_deltas = [];
// Terms are not loaded to avoid excessive memory consumption for large
// vocabularies. Needed terms are loaded explicitly afterward.
$tree = $this->storageController->loadTree($taxonomy_vocabulary->id(), 0, NULL, FALSE);
$tree_index = 0;
$complete_tree = NULL;
do {
// In case this tree is completely empty.
if (empty($tree[$tree_index])) {
break;
}
$delta++;
// Count entries before the current page.
if ($page && ($page * $page_increment) > $before_entries && !isset($back_step)) {
$before_entries++;
continue;
}
// Count entries after the current page.
elseif ($page_entries > $page_increment && isset($complete_tree)) {
$after_entries++;
continue;
}
// Do not let a term start the page that is not at the root.
$raw_term = $tree[$tree_index];
if (isset($raw_term->depth) && ($raw_term->depth > 0) && !isset($back_step)) {
$back_step = 0;
while ($parent_term = $tree[--$tree_index]) {
$before_entries--;
$back_step++;
if ($parent_term->depth == 0) {
$tree_index--;
// Jump back to the start of the root level parent.
continue 2;
}
}
}
$back_step = $back_step ?? 0;
// Continue rendering the tree until we reach the a new root item.
if ($page_entries >= $page_increment + $back_step + 1 && $raw_term->depth == 0 && $root_entries > 1) {
$complete_tree = TRUE;
// This new item at the root level is the first item on the next page.
$after_entries++;
continue;
}
if ($page_entries >= $page_increment + $back_step) {
$forward_step++;
}
// Finally, if we've gotten down this far, we're rendering a term on this
// page.
$page_entries++;
$term_deltas[$raw_term->tid] = isset($term_deltas[$raw_term->tid]) ? $term_deltas[$raw_term->tid] + 1 : 0;
$key = 'tid:' . $raw_term->tid . ':' . $term_deltas[$raw_term->tid];
// Keep track of the first term displayed on this page.
if ($page_entries == 1) {
$form['#first_tid'] = $raw_term->tid;
}
// Keep a variable to make sure at least 2 root elements are displayed.
if ($raw_term->parents[0] == 0) {
$root_entries++;
}
$current_page[$key] = $raw_term;
} while (isset($tree[++$tree_index]));
// Load all the terms we're going to display and set the weight and parents
// from the tree.
$terms = $this->storageController->loadMultiple(array_keys($term_deltas));
$current_page = array_map(function ($raw_term) use ($terms) {
$term = $terms[$raw_term->tid];
$term->depth = $raw_term->depth;
$term->parents = $raw_term->parents;
return $term;
}, $current_page);
// Because we didn't use a pager query, set the necessary pager variables.
$total_entries = $before_entries + $page_entries + $after_entries;
$this->pagerManager->createPager($total_entries, $page_increment);
// If this form was already submitted once, it's probably hit a validation
// error. Ensure the form is rebuilt in the same order as the user
// submitted.
$user_input = $form_state->getUserInput();
if (!empty($user_input['terms'])) {
// Get the POST order.
$order = array_flip(array_keys($user_input['terms']));
// Update our form with the new order.
$current_page = array_merge($order, $current_page);
foreach ($current_page as $key => $term) {
// Verify this is a term for the current page and set at the current
// depth.
if (is_array($user_input['terms'][$key]) && is_numeric($user_input['terms'][$key]['term']['tid'])) {
$current_page[$key]->depth = $user_input['terms'][$key]['term']['depth'];
}
else {
unset($current_page[$key]);
}
}
}
$args = [
'%capital_name' => Unicode::ucfirst($taxonomy_vocabulary->label()),
'%name' => $taxonomy_vocabulary->label(),
];
if ($this->currentUser()->hasPermission('administer taxonomy') || $this->currentUser()->hasPermission('edit terms in ' . $taxonomy_vocabulary->id())) {
$help_message = match ($vocabulary_hierarchy) {
VocabularyInterface::HIERARCHY_DISABLED => $this->t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', $args),
VocabularyInterface::HIERARCHY_SINGLE => $this->t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', $args),
VocabularyInterface::HIERARCHY_MULTIPLE => $this->t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', $args),
};
}
else {
$help_message = match ($vocabulary_hierarchy) {
VocabularyInterface::HIERARCHY_DISABLED => $this->t('%capital_name contains the following terms.', $args),
VocabularyInterface::HIERARCHY_SINGLE => $this->t('%capital_name contains terms grouped under parent terms', $args),
VocabularyInterface::HIERARCHY_MULTIPLE => $this->t('%capital_name contains terms with multiple parents.', $args),
};
}
// Get the IDs of the terms edited on the current page which have pending
// revisions.
$edited_term_ids = array_map(function ($item) {
return $item->id();
}, $current_page);
$pending_term_ids = array_intersect($this->storageController->getTermIdsWithPendingRevisions(), $edited_term_ids);
if ($pending_term_ids) {
$help_message = $this->formatPlural(
count($pending_term_ids),
'%capital_name contains 1 term with pending revisions. Drag and drop of terms with pending revisions is not supported, but you can re-enable drag-and-drop support by getting each term to a published state.',
'%capital_name contains @count terms with pending revisions. Drag and drop of terms with pending revisions is not supported, but you can re-enable drag-and-drop support by getting each term to a published state.',
$args
);
}
// Only allow access to change parents and reorder the tree if there are no
// pending revisions and there are no terms with multiple parents.
$update_tree_access = $taxonomy_vocabulary->access('reset all weights', NULL, TRUE);
$form['help'] = [
'#type' => 'container',
'message' => ['#markup' => $help_message],
];
$operations_access = !empty($pending_term_ids) || $vocabulary_hierarchy === VocabularyInterface::HIERARCHY_MULTIPLE;
if ($operations_access) {
$form['help']['#attributes']['class'] = ['messages', 'messages--warning'];
}
$errors = $form_state->getErrors();
$row_position = 0;
// Build the actual form.
$access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term');
$create_access = $access_control_handler->createAccess($taxonomy_vocabulary->id(), NULL, [], TRUE);
if ($create_access->isAllowed()) {
$empty = $this->t('No terms available. <a href=":link">Add term</a>.', [':link' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $taxonomy_vocabulary->id()])->toString()]);
}
else {
$empty = $this->t('No terms available.');
}
$form['terms'] = [
'#type' => 'table',
'#empty' => $empty,
'#header' => [
'term' => $this->t('Name'),
'status' => $this->t('Status'),
'operations' => $this->t('Operations'),
'weight' => !$operations_access ? $this->t('Weight') : NULL,
],
'#attributes' => [
'id' => 'taxonomy',
],
];
$this->renderer->addCacheableDependency($form['terms'], $create_access);
foreach ($current_page as $key => $term) {
$form['terms'][$key] = [
'term' => [],
'status' => [],
'operations' => [],
'weight' => $update_tree_access->isAllowed() ? [] : NULL,
];
/** @var \Drupal\Core\Entity\EntityInterface $term */
$term = $this->entityRepository->getTranslationFromContext($term);
$form['terms'][$key]['#term'] = $term;
$indentation = [];
if (isset($term->depth) && $term->depth > 0) {
$indentation = [
'#theme' => 'indentation',
'#size' => $term->depth,
];
}
$form['terms'][$key]['term'] = [
'#prefix' => !empty($indentation) ? $this->renderer->render($indentation) : '',
'#type' => 'link',
'#title' => $term->getName(),
'#url' => $term->toUrl(),
];
$form['terms'][$key]['status'] = [
'#type' => 'item',
'#markup' => ($term->isPublished()) ? t('Published') : t('Unpublished'),
];
// Add a special class for terms with pending revision so we can highlight
// them in the form.
$form['terms'][$key]['#attributes']['class'] = [];
if (in_array($term->id(), $pending_term_ids)) {
$form['terms'][$key]['#attributes']['class'][] = 'color-warning';
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term--pending-revision';
}
if ($update_tree_access->isAllowed() && count($tree) > 1) {
$parent_fields = TRUE;
$form['terms'][$key]['term']['tid'] = [
'#type' => 'hidden',
'#value' => $term->id(),
'#attributes' => [
'class' => ['term-id'],
],
];
$form['terms'][$key]['term']['parent'] = [
'#type' => 'hidden',
// Yes, default_value on a hidden. It needs to be changeable by the
// javascript.
'#default_value' => $term->parents[0],
'#attributes' => [
'class' => ['term-parent'],
],
];
$form['terms'][$key]['term']['depth'] = [
'#type' => 'hidden',
// Same as above, the depth is modified by javascript, so it's a
// default_value.
'#default_value' => $term->depth,
'#attributes' => [
'class' => ['term-depth'],
],
];
}
if ($update_tree_access->isAllowed()) {
$form['terms'][$key]['weight'] = [
'#type' => 'weight',
'#delta' => $delta,
'#title' => $this->t('Weight for added term'),
'#title_display' => 'invisible',
'#default_value' => $term->getWeight(),
'#attributes' => ['class' => ['term-weight']],
];
}
if ($operations = $this->termListBuilder->getOperations($term)) {
$form['terms'][$key]['operations'] = [
'#type' => 'operations',
'#links' => $operations,
];
}
if ($parent_fields) {
$form['terms'][$key]['#attributes']['class'][] = 'draggable';
}
// Add classes that mark which terms belong to previous and next pages.
if ($row_position < $back_step || $row_position >= $page_entries - $forward_step) {
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-preview';
}
if ($row_position !== 0 && $row_position !== count($tree) - 1) {
if ($row_position == $back_step - 1 || $row_position == $page_entries - $forward_step - 1) {
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-divider-top';
}
elseif ($row_position == $back_step || $row_position == $page_entries - $forward_step) {
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-divider-bottom';
}
}
// Add an error class if this row contains a form error.
foreach ($errors as $error_key => $error) {
if (str_starts_with($error_key, $key)) {
$form['terms'][$key]['#attributes']['class'][] = 'error';
}
}
$row_position++;
}
$this->renderer->addCacheableDependency($form['terms'], $update_tree_access);
if ($update_tree_access->isAllowed()) {
if ($parent_fields) {
$form['terms']['#tabledrag'][] = [
'action' => 'match',
'relationship' => 'parent',
'group' => 'term-parent',
'subgroup' => 'term-parent',
'source' => 'term-id',
'hidden' => FALSE,
];
$form['terms']['#tabledrag'][] = [
'action' => 'depth',
'relationship' => 'group',
'group' => 'term-depth',
'hidden' => FALSE,
];
$form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy';
$form['terms']['#attached']['drupalSettings']['taxonomy'] = [
'backStep' => $back_step,
'forwardStep' => $forward_step,
];
}
$form['terms']['#tabledrag'][] = [
'action' => 'order',
'relationship' => 'sibling',
'group' => 'term-weight',
];
}
if ($update_tree_access->isAllowed() && count($tree) > 1) {
$form['actions'] = ['#type' => 'actions', '#tree' => FALSE];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#button_type' => 'primary',
];
$form['actions']['reset_alphabetical'] = [
'#type' => 'submit',
'#submit' => ['::submitReset'],
'#value' => $this->t('Reset to alphabetical'),
];
}
$form['pager_pager'] = ['#type' => 'pager'];
return $form;
}
/**
* Form submission handler.
*
* Rather than using a textfield or weight field, this form depends entirely
* upon the order of form elements on the page to determine new weights.
*
* Because there might be hundreds or thousands of taxonomy terms that need to
* be ordered, terms are weighted from 0 to the number of terms in the
* vocabulary, rather than the standard -10 to 10 scale. Numbers are sorted
* lowest to highest, but are not necessarily sequential. Numbers may be
* skipped when a term has children so that reordering is minimal when a child
* is added or removed from a term.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Sort term order based on weight.
uasort($form_state->getValue('terms'), ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
$vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
$changed_terms = [];
// Terms are not loaded to avoid excessive memory consumption for large
// vocabularies. Needed terms are loaded explicitly afterward.
$tree = $this->storageController->loadTree($vocabulary->id(), 0, NULL, FALSE);
if (empty($tree)) {
return;
}
// Build a list of all terms that need to be updated on previous pages.
$weight = 0;
$raw_term = $tree[0];
$term_weights = [];
while ($raw_term->tid != $form['#first_tid']) {
if ($raw_term->parents[0] == 0 && $raw_term->weight != $weight) {
$term_weights[$raw_term->tid] = $weight;
}
$weight++;
$raw_term = $tree[$weight];
}
// Renumber the current page weights and assign any new parents.
$level_weights = [];
foreach ($form_state->getValue('terms') as $tid => $values) {
if (isset($form['terms'][$tid]['#term'])) {
$term = $form['terms'][$tid]['#term'];
// Give terms at the root level a weight in sequence with terms on previous pages.
if ($values['term']['parent'] == 0 && $term->getWeight() != $weight) {
$term->setWeight($weight);
$changed_terms[$term->id()] = $term;
}
// Terms not at the root level can safely start from 0 because they're all on this page.
elseif ($values['term']['parent'] > 0) {
$level_weights[$values['term']['parent']] = isset($level_weights[$values['term']['parent']]) ? $level_weights[$values['term']['parent']] + 1 : 0;
if ($level_weights[$values['term']['parent']] != $term->getWeight()) {
$term->setWeight($level_weights[$values['term']['parent']]);
$changed_terms[$term->id()] = $term;
}
}
// Update any changed parents.
if ($values['term']['parent'] != $term->parents[0]) {
$term->parent->target_id = $values['term']['parent'];
$changed_terms[$term->id()] = $term;
}
$weight++;
}
}
// Build a list of all terms that need to be updated on following pages.
for ($weight; $weight < count($tree); $weight++) {
$raw_term = $tree[$weight];
if ($raw_term->parents[0] == 0 && $raw_term->weight != $weight) {
$term_weights[$raw_term->tid] = $weight;
}
}
// Load all the items that need to be updated at once.
$terms = $this->storageController->loadMultiple(array_keys($term_weights));
foreach ($terms as $term) {
$term->setWeight($term_weights[$term->id()]);
$changed_terms[$term->id()] = $term;
}
if (!empty($changed_terms)) {
$pending_term_ids = $this->storageController->getTermIdsWithPendingRevisions();
// Force a form rebuild if any of the changed terms has a pending
// revision.
if (array_intersect_key(array_flip($pending_term_ids), $changed_terms)) {
$this->messenger()->addError($this->t('The terms with updated parents have been modified by another user, the changes could not be saved.'));
$form_state->setRebuild();
return;
}
// Save all updated terms.
foreach ($changed_terms as $term) {
$term->save();
}
$this->messenger()->addStatus($this->t('The configuration options have been saved.'));
}
}
/**
* Redirects to confirmation form for the reset action.
*/
public function submitReset(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
$vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
$form_state->setRedirectUrl($vocabulary->toUrl('reset-form'));
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Url;
/**
* Provides a deletion confirmation form for taxonomy term.
*
* @internal
*/
class TermDeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
// The cancel URL is the vocabulary collection, terms have no global
// list page.
return new Url('entity.taxonomy_vocabulary.collection');
}
/**
* {@inheritdoc}
*/
protected function getRedirectUrl() {
return $this->getCancelUrl();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Deleting a term will delete all its children if there are any. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
return $this->t('Deleted term %name.', ['%name' => $this->entity->label()]);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Entity\EntityDeleteForm;
/**
* Provides a deletion confirmation form for taxonomy vocabulary.
*
* @internal
*/
class VocabularyDeleteForm extends EntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_vocabulary_confirm_delete';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the vocabulary %title?', ['%title' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
return $this->t('Deleted vocabulary %name.', ['%name' => $this->entity->label()]);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides confirmation form for resetting a vocabulary to alphabetical order.
*
* @internal
*/
class VocabularyResetForm extends EntityConfirmFormBase {
/**
* The term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* Constructs a new VocabularyResetForm object.
*
* @param \Drupal\taxonomy\TermStorageInterface $term_storage
* The term storage.
*/
public function __construct(TermStorageInterface $term_storage) {
$this->termStorage = $term_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('taxonomy_term')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_vocabulary_confirm_reset_alphabetical';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to reset the vocabulary %title to alphabetical order?', ['%title' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl('overview-form');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Resetting a vocabulary will discard all custom ordering and sort items alphabetically.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Reset to alphabetical');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->termStorage->resetWeights($this->entity->id());
$this->messenger()->addStatus($this->t('Reset vocabulary %name to alphabetical order.', ['%name' => $this->entity->label()]));
$this->logger('taxonomy')->notice('Reset vocabulary %name to alphabetical order.', ['%name' => $this->entity->label()]);
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Drupal\taxonomy\Plugin\EntityReferenceSelection;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\Attribute\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Provides specific access control for the taxonomy_term entity type.
*/
#[EntityReferenceSelection(
id: "default:taxonomy_term",
label: new TranslatableMarkup("Taxonomy Term selection"),
entity_types: ["taxonomy_term"],
group: "default",
weight: 1
)]
class TermSelection extends DefaultSelection {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'sort' => [
'field' => 'name',
'direction' => 'asc',
],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Sorting is not possible for taxonomy terms because we use
// \Drupal\taxonomy\TermStorageInterface::loadTree() to retrieve matches.
$form['sort']['#access'] = FALSE;
return $form;
}
/**
* {@inheritdoc}
*/
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
if ($match || $limit) {
return parent::getReferenceableEntities($match, $match_operator, $limit);
}
$options = [];
$bundles = $this->entityTypeBundleInfo->getBundleInfo('taxonomy_term');
$bundle_names = $this->getConfiguration()['target_bundles'] ?: array_keys($bundles);
$has_admin_access = $this->currentUser->hasPermission('administer taxonomy');
$unpublished_terms = [];
foreach ($bundle_names as $bundle) {
if ($vocabulary = Vocabulary::load($bundle)) {
/** @var \Drupal\taxonomy\TermInterface[] $terms */
if ($terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vocabulary->id(), 0, NULL, TRUE)) {
foreach ($terms as $term) {
if (!$has_admin_access && (!$term->isPublished() || in_array($term->parent->target_id, $unpublished_terms))) {
$unpublished_terms[] = $term->id();
continue;
}
$options[$vocabulary->id()][$term->id()] = str_repeat('-', $term->depth) . Html::escape($this->entityRepository->getTranslationFromContext($term)->label());
}
}
}
}
return $options;
}
/**
* {@inheritdoc}
*/
public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS') {
if ($match) {
return parent::countReferenceableEntities($match, $match_operator);
}
$total = 0;
$referenceable_entities = $this->getReferenceableEntities($match, $match_operator, 0);
foreach ($referenceable_entities as $entities) {
$total += count($entities);
}
return $total;
}
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Adding the 'taxonomy_term_access' tag is sadly insufficient for terms:
// core requires us to also know about the concept of 'published' and
// 'unpublished'.
if (!$this->currentUser->hasPermission('administer taxonomy')) {
$query->condition('status', 1);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$term = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable term, it needs to published.
/** @var \Drupal\taxonomy\TermInterface $term */
$term->setPublished();
return $term;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('administer taxonomy')) {
$entities = array_filter($entities, function ($term) {
/** @var \Drupal\taxonomy\TermInterface $term */
return $term->isPublished();
});
}
return $entities;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\taxonomy\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
/**
* Plugin implementation of the 'entity reference taxonomy term RSS' formatter.
*/
#[FieldFormatter(
id: 'entity_reference_rss_category',
label: new TranslatableMarkup('RSS category'),
description: new TranslatableMarkup('Display reference to taxonomy term in RSS.'),
field_types: [
'entity_reference',
],
)]
class EntityReferenceTaxonomyTermRssFormatter extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$parent_entity = $items->getEntity();
$elements = [];
foreach ($this->getEntitiesToView($items, $langcode) as $entity) {
$parent_entity->rss_elements[] = [
'key' => 'category',
'value' => $entity->label(),
'attributes' => [
'domain' => $entity->id() ? Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $entity->id()], ['absolute' => TRUE])->toString() : '',
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
// This formatter is only available for taxonomy terms.
return $field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'taxonomy_term';
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\taxonomy\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
/**
* Validation constraint for changing the term hierarchy in pending revisions.
*/
#[Constraint(
id: 'TaxonomyHierarchy',
label: new TranslatableMarkup('Taxonomy term hierarchy', [], ['context' => 'Validation'])
)]
class TaxonomyTermHierarchyConstraint extends CompositeConstraintBase {
/**
* The default violation message.
*
* @var string
*/
public $message = 'You can only change the hierarchy for the <em>published</em> version of this term.';
/**
* {@inheritdoc}
*/
public function coversFields() {
return ['parent', 'weight'];
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\taxonomy\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Constraint validator for changing term parents in pending revisions.
*/
class TaxonomyTermHierarchyConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* Creates a new TaxonomyTermHierarchyConstraintValidator instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
$term_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
assert($term_storage instanceof TermStorageInterface);
// Newly created entities should be able to specify a parent.
if ($entity && $entity->isNew()) {
return;
}
$is_pending_revision = !$entity->isDefaultRevision();
$pending_term_ids = $term_storage->getTermIdsWithPendingRevisions();
$ancestors = $term_storage->loadAllParents($entity->id());
$ancestor_is_pending_revision = (bool) array_intersect_key($ancestors, array_flip($pending_term_ids));
$new_parents = array_column($entity->parent->getValue(), 'target_id');
$original_parents = array_keys($term_storage->loadParents($entity->id())) ?: [0];
if (($is_pending_revision || $ancestor_is_pending_revision) && $new_parents != $original_parents) {
$this->context->buildViolation($constraint->message)
->atPath('parent')
->addViolation();
}
$original = $term_storage->loadUnchanged($entity->id());
if (($is_pending_revision || $ancestor_is_pending_revision) && !$entity->weight->equals($original->weight)) {
$this->context->buildViolation($constraint->message)
->atPath('weight')
->addViolation();
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\migrate\Plugin\MigrationDeriverTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for Drupal 6 term node migrations based on vocabularies.
*/
class D6TermNodeDeriver extends DeriverBase implements ContainerDeriverInterface {
use MigrationDeriverTrait;
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* The migration plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $migrationPluginManager;
/**
* D6TermNodeDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID this derivative is for.
* @param \Drupal\Component\Plugin\PluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
*/
public function __construct($base_plugin_id, PluginManagerInterface $migration_plugin_manager) {
$this->basePluginId = $base_plugin_id;
$this->migrationPluginManager = $migration_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('plugin.manager.migration')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition, $base_plugin_definitions = NULL) {
try {
foreach (static::getSourcePlugin('d6_taxonomy_vocabulary') as $row) {
$source_vid = $row->getSourceProperty('vid');
$definition = $base_plugin_definition;
$definition['source']['vid'] = $source_vid;
// migrate_drupal_migration_plugins_alter() adds to this definition.
$this->derivatives[$source_vid] = $definition;
}
}
catch (\Exception $e) {
// It is possible no D6 tables are loaded so just eat exceptions.
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationDeriverTrait;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for Drupal 7 taxonomy term migrations based on vocabularies.
*/
class D7TaxonomyTermDeriver extends DeriverBase implements ContainerDeriverInterface {
use MigrationDeriverTrait;
use StringTranslationTrait;
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* The migration field discovery service.
*
* @var \Drupal\migrate_drupal\FieldDiscoveryInterface
*/
protected $fieldDiscovery;
/**
* D7TaxonomyTermDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID for the plugin ID.
* @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
* The migration field discovery service.
*/
public function __construct($base_plugin_id, FieldDiscoveryInterface $field_discovery) {
$this->basePluginId = $base_plugin_id;
$this->fieldDiscovery = $field_discovery;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('migrate_drupal.field_discovery')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$vocabulary_source_plugin = static::getSourcePlugin('d7_taxonomy_vocabulary');
try {
$vocabulary_source_plugin->checkRequirements();
}
catch (RequirementsException $e) {
// If the d7_taxonomy_vocabulary requirements failed, that means we do not
// have a Drupal source database configured - there is nothing to
// generate.
return $this->derivatives;
}
try {
foreach ($vocabulary_source_plugin as $row) {
$bundle = $row->getSourceProperty('machine_name');
$values = $base_plugin_definition;
$values['label'] = $this->t('@label (@type)', [
'@label' => $values['label'],
'@type' => $row->getSourceProperty('name'),
]);
$values['source']['bundle'] = $bundle;
$values['destination']['default_bundle'] = $bundle;
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
$this->fieldDiscovery->addBundleFieldProcesses($migration, 'taxonomy_term', $bundle);
$this->derivatives[$bundle] = $migration->getPluginDefinition();
}
}
catch (DatabaseExceptionWrapper $e) {
// Once we begin iterating the source plugin it is possible that the
// source tables will not exist. This can happen when the
// MigrationPluginManager gathers up the migration definitions but we do
// not actually have a Drupal 7 source database.
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\taxonomy\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Row;
/**
* Migration destination for taxonomy vocabulary.
*/
#[MigrateDestination('entity:taxonomy_vocabulary')]
class EntityTaxonomyVocabulary extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function getEntity(Row $row, array $old_destination_id_values) {
/** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
$vocabulary = parent::getEntity($row, $old_destination_id_values);
// Config schema does not allow description to be empty.
if (trim($vocabulary->getDescription()) === '') {
$vocabulary->set('description', NULL);
}
return $vocabulary;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\field;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
// cspeLL:ignore entityreference
/**
* MigrateField Plugin for Drupal 6 & Drupal 7 taxonomy term reference fields.
*/
#[MigrateField(
id: 'taxonomy_term_reference',
core: [6, 7],
type_map: [
'taxonomy_term_reference' => 'entity_reference',
],
source_module: 'taxonomy',
destination_module: 'core',
)]
class TaxonomyTermReference extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'taxonomy_term_reference_link' => 'entity_reference_label',
'taxonomy_term_reference_plain' => 'entity_reference_label',
'taxonomy_term_reference_rss_category' => 'entity_reference_label',
'i18n_taxonomy_term_reference_link' => 'entity_reference_label',
'i18n_taxonomy_term_reference_plain' => 'entity_reference_label',
'entityreference_entity_view' => 'entity_reference_entity_view',
];
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'tid',
],
];
$migration->setProcessOfProperty($field_name, $process);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Checks if the vocabulary being migrated is the one used for forums.
*
* Drupal 8 Forum is expecting specific machine names for its field and
* vocabulary names. This process plugin forces a given machine name to the
* field or vocabulary that is being migrated.
*
* The 'forum_vocabulary' source property is evaluated in the
* d6_taxonomy_vocabulary or d7_taxonomy_vocabulary source plugins and is set to
* true if the vocabulary vid being migrated is the same as the one in the
* 'forum_nav_vocabulary' variable on the source site.
*
* Example:
*
* @code
* process:
* field_name:
* plugin: forum_vocabulary
* machine_name: taxonomy_forums
* @endcode
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use
* \Drupal\forum\Plugin\migrate\process\ForumVocabulary instead.
*
* @see https://www.drupal.org/node/3387830
*/
class ForumVocabulary extends ProcessPluginBase {
/**
* Constructs a MigrationLookup object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
@trigger_error(__CLASS__ . 'is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use \Drupal\forum\Plugin\migrate\process\ForumVocabulary instead. See https://www.drupal.org/node/3387830', E_USER_DEPRECATED);
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if ($row->getSourceProperty('forum_vocabulary') && !empty($this->configuration['machine_name'])) {
$value = $this->configuration['machine_name'];
}
return $value;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Converts a Drupal 6 vocabulary ID to a target bundle array.
*/
#[MigrateProcess('target_bundle')]
class TargetBundle extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$target_bundle = [];
$vid = $row->get('@_vid');
$target_bundle[$vid] = $vid;
return $target_bundle;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
// cspell:ignore trid
/**
* Drupal 6 taxonomy term source from database.
*
* Available configuration keys:
* - bundle: (optional) The taxonomy vocabulary (vid) to filter terms retrieved
* from the source - can be an integer or an array. If omitted, all terms are
* retrieved.
*
* Examples:
*
* @code
* source:
* plugin: d6_taxonomy_term
* bundle: 0
* @endcode
*
* In this example terms of vocabulary with 'vid' equal to 0 are retrieved from
* the source database.
*
* @code
* source:
* plugin: d6_taxonomy_term
* bundle: [1, 3, 5]
* @endcode
*
* In this example terms of vocabularies with 'vid' one of 1, 3, 5 are retrieved
* from the source database.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @todo Support term_relation, term_synonym table if possible.
*
* @MigrateSource(
* id = "d6_taxonomy_term",
* source_module = "taxonomy"
* )
*/
class Term extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('term_data', 'td')
->fields('td')
->distinct()
->orderBy('td.tid');
if (isset($this->configuration['bundle'])) {
$query->condition('td.vid', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('The term ID.'),
'vid' => $this->t('Existing term VID'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'weight' => $this->t('Weight'),
'parent' => $this->t("The Drupal term IDs of the term's parents."),
];
if (isset($this->configuration['translations'])) {
$fields['language'] = $this->t('The term language.');
$fields['trid'] = $this->t('Translation ID.');
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Find parents for this row.
$parents = $this->select('term_hierarchy', 'th')
->fields('th', ['parent', 'tid'])
->condition('tid', $row->getSourceProperty('tid'))
->execute()
->fetchCol();
$row->setSourceProperty('parent', $parents);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['tid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
// cspell:ignore ltlanguage objectid
/**
* Drupal 6 i18n taxonomy terms source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\taxonomy\Plugin\migrate\source\d6\Term
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_term_localized_translation",
* source_module = "i18ntaxonomy"
* )
*/
class TermLocalizedTranslation extends Term {
/**
* {@inheritdoc}
*/
public function query() {
// Ideally, the query would return rows for each language for each taxonomy
// term with the translations for both the name and description or just the
// name translation or just the description translation. That query quickly
// became complex and would be difficult to maintain.
// Therefore, build a query based on i18nstrings table where each row has
// the translation for only one property, either name or description. The
// method prepareRow() is then used to obtain the translation for the other
// property.
$query = parent::query();
$query->addField('td', 'language', 'td.language');
// Add in the property, which is either name or description.
// Cast td.tid as char for PostgreSQL compatibility.
$query->leftJoin('i18n_strings', 'i18n', 'CAST([td].[tid] AS CHAR(255)) = [i18n].[objectid]');
$query->condition('i18n.type', 'term');
$query->addField('i18n', 'lid');
$query->addField('i18n', 'property');
// Add in the translation for the property.
$query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]');
$query->addField('lt', 'language', 'lt.language');
$query->addField('lt', 'translation');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$language = $row->getSourceProperty('ltlanguage');
$row->setSourceProperty('language', $language);
$tid = $row->getSourceProperty('tid');
// If this row has been migrated it is a duplicate then skip it.
if ($this->idMap->lookupDestinationIds(['tid' => $tid, 'language' => $language])) {
return FALSE;
}
// Save the translation for this property.
$property = $row->getSourceProperty('property');
$row->setSourceProperty($property . '_translated', $row->getSourceProperty('translation'));
// Get the translation, if one exists, for the property not already in the
// row.
$other_property = ($property == 'name') ? 'description' : 'name';
$query = $this->select('i18n_strings', 'i18n')
->fields('i18n', ['lid'])
->condition('i18n.type', 'term')
->condition('i18n.property', $other_property)
->condition('i18n.objectid', $tid);
$query->leftJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]');
$query->condition('lt.language', $language);
$query->addField('lt', 'translation');
$results = $query->execute()->fetchAssoc();
if ($results) {
$row->setSourceProperty($other_property . '_translated', $results['translation']);
}
else {
// The translation does not exist.
$row->setSourceProperty($other_property . '_translated', NULL);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'language' => $this->t('Language for this term.'),
'name_translated' => $this->t('Term name translation.'),
'description_translated' => $this->t('Term description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
return parent::getIds() + $ids;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 term/node relationships (current revision) source from database.
*
* Available configuration keys:
* - vid: (optional) The taxonomy vocabulary (vid) to filter terms retrieved
* from the source - should be an integer. If omitted, all terms are
* retrieved.
*
* Example:
*
* @code
* source:
* plugin: d6_term_node
* vid: 7
* @endcode
*
* In this example the relations between nodes and terms are retrieved from
* the source database. Source rows include only terms that belong to the
* vocabulary with 'vid' equal to 7.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_term_node",
* source_module = "taxonomy"
* )
*/
class TermNode extends DrupalSqlBase {
/**
* The join options between the node and the term node table.
*/
const JOIN = '[tn].[vid] = [n].[vid]';
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('term_node', 'tn')
->distinct()
->fields('tn', ['nid', 'vid'])
->fields('n', ['type']);
// Because this is an inner join it enforces the current revision.
$query->innerJoin('term_data', 'td', '[td].[tid] = [tn].[tid] AND [td].[vid] = :vid', [':vid' => $this->configuration['vid']]);
$query->innerJoin('node', 'n', static::JOIN);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'nid' => $this->t('The node revision ID.'),
'vid' => $this->t('The node revision ID.'),
'tid' => $this->t('The term ID.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Select the terms belonging to the revision selected.
$query = $this->select('term_node', 'tn')
->fields('tn', ['tid'])
->condition('n.nid', $row->getSourceProperty('nid'));
$query->join('node', 'n', static::JOIN);
$query->innerJoin('term_data', 'td', '[td].[tid] = [tn].[tid] AND [td].[vid] = :vid', [':vid' => $this->configuration['vid']]);
$row->setSourceProperty('tid', $query->execute()->fetchCol());
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['vid']['alias'] = 'tn';
return $ids;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
/**
* Drupal 6 term/node relationships (non-current revision) source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\taxonomy\Plugin\migrate\source\d6\TermNode
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_term_node_revision",
* source_module = "taxonomy"
* )
*/
class TermNodeRevision extends TermNode {
/**
* {@inheritdoc}
*/
const JOIN = 'tn.nid = n.nid AND tn.vid != n.vid';
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Drupal 6 vocabularies source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_taxonomy_vocabulary",
* source_module = "taxonomy"
* )
*/
class Vocabulary extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('vocabulary', 'v')
->fields('v', [
'vid',
'name',
'description',
'help',
'relations',
'hierarchy',
'multiple',
'required',
'tags',
'module',
'weight',
]);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'vid' => $this->t('The vocabulary ID.'),
'name' => $this->t('The name of the vocabulary.'),
'description' => $this->t('The description of the vocabulary.'),
'help' => $this->t('Help text to display for the vocabulary.'),
'relations' => $this->t('Whether or not related terms are enabled within the vocabulary. (0 = disabled, 1 = enabled)'),
'hierarchy' => $this->t('The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)'),
'multiple' => $this->t('Whether or not multiple terms from this vocabulary may be assigned to a node. (0 = disabled, 1 = enabled)'),
'required' => $this->t('Whether or not terms are required for nodes using this vocabulary. (0 = disabled, 1 = enabled)'),
'tags' => $this->t('Whether or not free tagging is enabled for the vocabulary. (0 = disabled, 1 = enabled)'),
'weight' => $this->t('The weight of the vocabulary in relation to other vocabularies.'),
'parents' => $this->t("The Drupal term IDs of the term's parents."),
'node_types' => $this->t('The names of the node types the vocabulary may be used with.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Find node types for this row.
$node_types = $this->select('vocabulary_node_types', 'nt')
->fields('nt', ['type', 'vid'])
->condition('vid', $row->getSourceProperty('vid'))
->execute()
->fetchCol();
$row->setSourceProperty('node_types', $node_types);
$row->setSourceProperty('cardinality', ($row->getSourceProperty('tags') == 1 || $row->getSourceProperty('multiple') == 1) ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : 1);
// If the vocabulary being migrated is the one defined in the
// 'forum_nav_vocabulary' variable, set the 'forum_vocabulary' source
// property to true so we know this is the vocabulary used by Forum.
if ($this->variableGet('forum_nav_vocabulary', 0) == $row->getSourceProperty('vid')) {
$row->setSourceProperty('forum_vocabulary', TRUE);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
// cspell:ignore localizable
/**
* Drupal 6 vocabularies with associated node types source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_taxonomy_vocabulary_per_type",
* source_module = "taxonomy"
* )
*/
class VocabularyPerType extends Vocabulary {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query->join('vocabulary_node_types', 'nt', '[v].[vid] = [nt].[vid]');
$query->fields('nt', ['type']);
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Get the i18n taxonomy translation setting for this vocabulary.
// 0 - No multilingual options
// 1 - Localizable terms. Run through the localization system.
// 2 - Predefined language for a vocabulary and its terms.
// 3 - Per-language terms, translatable (referencing terms with different
// languages) but not localizable.
$i18ntaxonomy_vocab = $this->variableGet('i18ntaxonomy_vocabulary', []);
$vid = $row->getSourceProperty('vid');
$i18ntaxonomy_vocabulary = FALSE;
if (array_key_exists($vid, $i18ntaxonomy_vocab)) {
$i18ntaxonomy_vocabulary = $i18ntaxonomy_vocab[$vid];
}
$row->setSourceProperty('i18ntaxonomy_vocabulary', $i18ntaxonomy_vocabulary);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['vid']['alias'] = 'nt';
$ids['type']['type'] = 'string';
return $ids;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
// cspell:ignore ltlanguage objectindex
/**
* Drupal 6 i18n vocabulary translations source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_taxonomy_vocabulary_translation",
* source_module = "i18ntaxonomy"
* )
*/
class VocabularyTranslation extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('vocabulary', 'v')
->fields('v')
->fields('i18n', ['lid', 'type', 'property', 'objectid'])
->fields('lt', ['lid', 'translation'])
->condition('i18n.type', 'vocabulary');
$query->addField('lt', 'language', 'lt.language');
// The i18n_strings table has two columns containing the object ID, objectid
// and objectindex. The objectid column is a text field. Therefore, for the
// join to work in PostgreSQL, use the objectindex field as this is numeric
// like the vid field.
$query->join('i18n_strings', 'i18n', '[v].[vid] = [i18n].[objectindex]');
$query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'vid' => $this->t('The vocabulary ID.'),
'language' => $this->t('Language for this field.'),
'property' => $this->t('Name of property being translated.'),
'translation' => $this->t('Translation of either the title or explanation.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// For ease of reading the migration use 'language' as the property name for
// the language.
$language = $row->getSourceProperty('ltlanguage');
$row->setSourceProperty('language', $language);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
return $ids;
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Drupal 7 taxonomy term source from database.
*
* Available configuration keys:
* - bundle: (optional) The taxonomy vocabulary (machine name) to filter terms
* retrieved from the source - can be a string or an array. If omitted, all
* terms are retrieved.
*
* Examples:
*
* @code
* source:
* plugin: d7_taxonomy_term
* bundle: tags
* @endcode
*
* In this example terms of 'tags' vocabulary are retrieved from the source
* database.
*
* @code
* source:
* plugin: d7_taxonomy_term
* bundle: [tags, forums]
* @endcode
*
* In this example terms of 'tags' and 'forums' vocabularies are retrieved
* from the source database.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @todo Support term_relation, term_synonym table if possible.
*
* @MigrateSource(
* id = "d7_taxonomy_term",
* source_module = "taxonomy"
* )
*/
class Term extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('taxonomy_term_data', 'td')
->fields('td')
->distinct()
->orderBy('tid');
$query->leftJoin('taxonomy_vocabulary', 'tv', '[td].[vid] = [tv].[vid]');
$query->addField('tv', 'machine_name');
if ($this->getDatabase()
->schema()
->fieldExists('taxonomy_vocabulary', 'i18n_mode')) {
$query->addField('tv', 'i18n_mode');
}
if (isset($this->configuration['bundle'])) {
$query->condition('tv.machine_name', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('The term ID.'),
'vid' => $this->t('Existing term VID'),
'machine_name' => $this->t('Vocabulary machine name'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'weight' => $this->t('Weight'),
'parent' => $this->t("The Drupal term IDs of the term's parents."),
'format' => $this->t("Format of the term description."),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$tid = $row->getSourceProperty('tid');
$vocabulary = $row->getSourceProperty('machine_name');
$default_language = (array) $this->variableGet('language_default', ['language' => 'en']);
// If this entity was translated using Entity Translation, we need to get
// its source language to get the field values in the right language.
// The translations will be migrated by the d7_node_entity_translation
// migration.
$translatable_vocabularies = array_keys(array_filter($this->variableGet('entity_translation_taxonomy', [])));
$entity_translatable = $this->isEntityTranslatable('taxonomy_term') && in_array($vocabulary, $translatable_vocabularies, TRUE);
if ($entity_translatable) {
$source_language = $this->getEntityTranslationSourceLanguage('taxonomy_term', $tid);
$language = $entity_translatable && $source_language ? $source_language : $default_language['language'];
}
// If this is an i18n translation use the default language when i18n_mode
// is localized.
if ($row->get('i18n_mode')) {
$language = ($row->get('i18n_mode') === '1') ? $default_language['language'] : $row->get('language');
}
$language = $language ?? $default_language['language'];
$row->setSourceProperty('language', $language);
// Get Field API field values.
foreach ($this->getFields('taxonomy_term', $vocabulary) as $field_name => $field) {
// Ensure we're using the right language if the entity and the field are
// translatable.
$field_language = $entity_translatable && $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('taxonomy_term', $field_name, $tid, NULL, $field_language));
}
// Find parents for this row.
$parents = $this->select('taxonomy_term_hierarchy', 'th')
->fields('th', ['parent', 'tid'])
->condition('tid', $row->getSourceProperty('tid'))
->execute()
->fetchCol();
$row->setSourceProperty('parent', $parents);
// If the term name or term description were replaced by real fields using
// the Drupal 7 Title module, use the fields value instead of the term name
// or term description.
if ($this->moduleExists('title')) {
$name_field = $row->getSourceProperty('name_field');
if (isset($name_field[0]['value'])) {
$row->setSourceProperty('name', $name_field[0]['value']);
}
$description_field = $row->getSourceProperty('description_field');
if (isset($description_field[0]['value'])) {
$row->setSourceProperty('description', $description_field[0]['value']);
}
if (isset($description_field[0]['format'])) {
$row->setSourceProperty('format', $description_field[0]['format']);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['tid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Drupal 7 taxonomy term entity translation source plugin.
*
* Available configuration keys:
* - bundle: (optional) The taxonomy vocabulary (machine name) to filter terms
* retrieved from the source - can be a string or an array. If omitted, all
* terms are retrieved.
*
* Examples:
*
* @code
* source:
* plugin: d7_taxonomy_term
* bundle: tags
* @endcode
*
* In this example terms of 'tags' vocabulary are retrieved from the source
* database.
*
* @code
* source:
* plugin: d7_taxonomy_term
* bundle: [tags, forums]
* @endcode
*
* In this example terms of 'tags' and 'forums' vocabularies are retrieved
* from the source database.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_taxonomy_term_entity_translation",
* source_module = "entity_translation"
* )
*/
class TermEntityTranslation extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('entity_translation', 'et')
->fields('et')
->fields('td', [
'name',
'description',
'format',
])
->fields('tv', [
'machine_name',
])
->condition('et.entity_type', 'taxonomy_term')
->condition('et.source', '', '<>');
$query->innerJoin('taxonomy_term_data', 'td', '[td].[tid] = [et].[entity_id]');
$query->innerJoin('taxonomy_vocabulary', 'tv', '[td].[vid] = [tv].[vid]');
if (isset($this->configuration['bundle'])) {
$query->condition('tv.machine_name', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$tid = $row->getSourceProperty('entity_id');
$vocabulary = $row->getSourceProperty('machine_name');
$language = $row->getSourceProperty('language');
// Get Field API field values.
foreach ($this->getFields('taxonomy_term', $vocabulary) as $field_name => $field) {
// Ensure we're using the right language if the entity is translatable.
$field_language = $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('taxonomy_term', $field_name, $tid, NULL, $field_language));
}
// If the term name or term description were replaced by real fields using
// the Drupal 7 Title module, use the fields value instead of the term name
// or term description.
if ($this->moduleExists('title')) {
$name_field = $row->getSourceProperty('name_field');
if (isset($name_field[0]['value'])) {
$row->setSourceProperty('name', $name_field[0]['value']);
}
$description_field = $row->getSourceProperty('description_field');
if (isset($description_field[0]['value'])) {
$row->setSourceProperty('description', $description_field[0]['value']);
}
if (isset($description_field[0]['format'])) {
$row->setSourceProperty('format', $description_field[0]['format']);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'entity_type' => $this->t('The entity type this translation relates to'),
'entity_id' => $this->t('The entity ID this translation relates to'),
'revision_id' => $this->t('The entity revision ID this translation relates to'),
'language' => $this->t('The target language for this translation.'),
'source' => $this->t('The source language from which this translation was created.'),
'uid' => $this->t('The author of this translation.'),
'status' => $this->t('Boolean indicating whether the translation is published (visible to non-administrators).'),
'translate' => $this->t('A boolean indicating whether this translation needs to be updated.'),
'created' => $this->t('The Unix timestamp when the translation was created.'),
'changed' => $this->t('The Unix timestamp when the translation was most recently saved.'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'format' => $this->t('Format of the term description.'),
'machine_name' => $this->t('Vocabulary machine name'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'entity_id' => [
'type' => 'integer',
'alias' => 'et',
],
'language' => [
'type' => 'string',
'alias' => 'et',
],
];
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
use Drupal\migrate\Row;
// cspell:ignore ltlanguage objectid
/**
* Drupal 7 i18n taxonomy terms source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\taxonomy\Plugin\migrate\source\d7\Term
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_term_localized_translation",
* source_module = "i18n_taxonomy"
* )
*/
class TermLocalizedTranslation extends Term {
use I18nQueryTrait;
/**
* {@inheritdoc}
*/
public function query() {
// Ideally, the query would return rows for each language for each taxonomy
// term with the translations for both the name and description or just the
// name translation or just the description translation. That query quickly
// became complex and would be difficult to maintain.
// Therefore, build a query based on i18nstrings table where each row has
// the translation for only one property, either name or description. The
// method prepareRow() is then used to obtain the translation for the other
// property.
$query = parent::query();
$query->addField('td', 'language', 'td.language');
// Add in the property, which is either name or description.
// Cast td.tid as char for PostgreSQL compatibility.
$query->leftJoin('i18n_string', 'i18n', 'CAST([td].[tid] AS CHAR(255)) = [i18n].[objectid]');
$query->condition('i18n.type', 'term');
$query->addField('i18n', 'lid');
$query->addField('i18n', 'property');
// Add in the translation for the property.
$query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]');
$query->addField('lt', 'language', 'lt.language');
$query->addField('lt', 'translation');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
if (!parent::prepareRow($row)) {
return FALSE;
}
// Override language with ltlanguage.
$language = $row->getSourceProperty('ltlanguage');
$row->setSourceProperty('language', $language);
// Set the i18n string table for use in I18nQueryTrait.
$this->i18nStringTable = 'i18n_string';
// Save the translation for the property already in the row.
$property_in_row = $row->getSourceProperty('property');
// Get the translation for the property not already in the row and save it
// in the row.
$property_not_in_row = ($property_in_row == 'name') ? 'description' : 'name';
return $this->getPropertyNotInRowTranslation($row, $property_not_in_row, 'tid', $this->idMap);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'language' => $this->t('Language for this term.'),
'name_translated' => $this->t('Term name translation.'),
'description_translated' => $this->t('Term description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
return parent::getIds() + $ids;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
/**
* Drupal 7 i18n taxonomy terms source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\taxonomy\Plugin\migrate\source\d7\Term
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_taxonomy_term_translation",
* source_module = "i18n_taxonomy"
* )
*/
class TermTranslation extends Term {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
if ($this->database->schema()->fieldExists('taxonomy_term_data', 'language')) {
$query->addField('td', 'language', 'td_language');
}
// Get data when the i18n_mode column exists and it is not the Drupal 7
// value I18N_MODE_NONE or I18N_MODE_LOCALIZE. Otherwise, return no data.
// @see https://git.drupalcode.org/project/i18n/-/blob/7.x-1.x/i18n.module#L26
if ($this->database->schema()->fieldExists('taxonomy_vocabulary', 'i18n_mode')) {
$query->addField('tv', 'i18n_mode');
$query->condition('tv.i18n_mode', ['0', '1'], 'NOT IN');
}
else {
$query->alwaysFalse();
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
if (!parent::prepareRow($row)) {
return FALSE;
}
$row->setSourceProperty('language', $row->getSourceProperty('td_language'));
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'language' => $this->t('Language for this term.'),
'name_translated' => $this->t('Term name translation.'),
'description_translated' => $this->t('Term description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'td';
return parent::getIds() + $ids;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 vocabularies source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_taxonomy_vocabulary",
* source_module = "taxonomy"
* )
*/
class Vocabulary extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('taxonomy_vocabulary', 'v')
->fields('v', [
'vid',
'name',
'description',
'hierarchy',
'module',
'weight',
'machine_name',
]);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'vid' => $this->t('The vocabulary ID.'),
'name' => $this->t('The name of the vocabulary.'),
'description' => $this->t('The description of the vocabulary.'),
'hierarchy' => $this->t('The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)'),
'module' => $this->t('Module responsible for the vocabulary.'),
'weight' => $this->t('The weight of the vocabulary in relation to other vocabularies.'),
'machine_name' => $this->t('Unique machine name of the vocabulary.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// If the vocabulary being migrated is the one defined in the
// 'forum_nav_vocabulary' variable, set the 'forum_vocabulary' source
// property to true so we know this is the vocabulary used by Forum.
if ($this->variableGet('forum_nav_vocabulary', 0) == $row->getSourceProperty('vid')) {
$row->setSourceProperty('forum_vocabulary', TRUE);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
// cspell:ignore objectid objectindex plid textgroup
/**
* Drupal 7 i18n vocabulary translations source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_taxonomy_vocabulary_translation",
* source_module = "i18n_taxonomy"
* )
*/
class VocabularyTranslation extends Vocabulary {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query->leftJoin('i18n_string', 'i18n', 'CAST ([v].[vid] AS CHAR(222)) = [i18n].[objectid]');
$query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]');
$query
->condition('type', 'vocabulary')
->fields('lt')
->fields('i18n');
$query->addField('lt', 'lid', 'lt_lid');
if ($this->getDatabase()
->schema()
->fieldExists('taxonomy_vocabulary', 'language')) {
$query->addField('v', 'language', 'v_language');
}
if ($this->getDatabase()
->schema()
->fieldExists('taxonomy_vocabulary', 'i18n_mode')) {
$query->addField('v', 'i18n_mode');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'i18n_mode' => $this->t('Internationalization mode.'),
'v_language' => $this->t('Language from the taxonomy_vocabulary table.'),
'property' => $this->t('Name of property being translated.'),
'type' => $this->t('Name of property being translated.'),
'objectid' => $this->t('Name of property being translated.'),
'lt_lid' => $this->t('Name of property being translated.'),
'translation' => $this->t('Translation of either the name or the description.'),
'lid' => $this->t('Language string ID'),
'textgroup' => $this->t('A module defined group of translations'),
'context' => $this->t('Full string ID for quick search: type:objectid:property.'),
'objectindex' => $this->t('Integer value of Object ID'),
'format' => $this->t('The {filter_format}.format of the string'),
'language' => $this->t('Language code from locales_target table'),
'plid' => $this->t('Parent lid'),
'plural' => $this->t('Plural index number'),
'i18n_status' => $this->t('Translation needs update'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids = [];
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
$ids['property']['type'] = 'string';
return parent::getIds() + $ids;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\taxonomy\Entity\Term;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\ManyToOne;
/**
* Allow taxonomy term ID(s) as argument.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'taxonomy_index_tid',
)]
class IndexTid extends ManyToOne {
public function titleQuery() {
$titles = [];
$terms = Term::loadMultiple($this->value);
foreach ($terms as $term) {
$titles[] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
}
return $titles;
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\taxonomy\TaxonomyIndexDepthQueryTrait;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler for taxonomy terms with depth.
*
* This handler is actually part of the node table and has some restrictions,
* because it uses a subquery to find nodes with.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'taxonomy_index_tid_depth',
)]
class IndexTidDepth extends ArgumentPluginBase implements ContainerFactoryPluginInterface {
use TaxonomyIndexDepthQueryTrait;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no
* replacement.
*
* @see https://www.drupal.org/node/3427843
*/
protected $termStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, protected EntityStorageInterface|EntityRepositoryInterface $entityRepository) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
if ($entityRepository instanceof EntityStorageInterface) {
// @phpstan-ignore-next-line
$this->termStorage = $entityRepository;
@trigger_error('Calling ' . __CLASS__ . '::__construct() with the $termStorage argument as \Drupal\Core\Entity\EntityStorageInterface is deprecated in drupal:10.3.0 and it will require Drupal\Core\Entity\EntityRepositoryInterface in drupal:11.0.0. See https://www.drupal.org/node/3427843', E_USER_DEPRECATED);
$this->entityRepository = \Drupal::service('entity.repository');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.repository')
);
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['depth'] = ['default' => 0];
$options['break_phrase'] = ['default' => FALSE];
$options['use_taxonomy_term_path'] = ['default' => FALSE];
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['depth'] = [
'#type' => 'weight',
'#title' => $this->t('Depth'),
'#default_value' => $this->options['depth'],
'#description' => $this->t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'),
];
$form['break_phrase'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow multiple values'),
'#description' => $this->t('If selected, users can enter multiple values in the form of 1+2+3. Due to the number of JOINs it would require, AND will be treated as OR with this filter.'),
'#default_value' => !empty($this->options['break_phrase']),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* Override defaultActions() to remove summary actions.
*/
protected function defaultActions($which = NULL) {
if ($which) {
if (in_array($which, ['ignore', 'not found', 'empty', 'default'])) {
return parent::defaultActions($which);
}
return;
}
$actions = parent::defaultActions();
unset($actions['summary asc']);
unset($actions['summary desc']);
unset($actions['summary asc by count']);
unset($actions['summary desc by count']);
return $actions;
}
public function query($group_by = FALSE) {
$this->ensureMyTable();
if (!empty($this->options['break_phrase'])) {
$break = static::breakString($this->argument);
if ($break->value === [-1]) {
return FALSE;
}
$tids = $break->value;
}
else {
$tids = $this->argument;
}
$this->addSubQueryJoin($tids);
}
public function title() {
$term = $this->entityRepository->getCanonical('taxonomy_term', $this->argument);
if (!empty($term)) {
return $term->label();
}
// @todo Review text.
return $this->t('No name');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
/**
* Argument handler for to modify depth for a previous term.
*
* This handler is actually part of the node table and has some restrictions,
* because it uses a subquery to find nodes with.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'taxonomy_index_tid_depth_modifier',
)]
class IndexTidDepthModifier extends ArgumentPluginBase {
public function buildOptionsForm(&$form, FormStateInterface $form_state) {}
public function query($group_by = FALSE) {}
public function preQuery() {
// We don't know our argument yet, but it's based upon our position:
$argument = $this->view->args[$this->position] ?? NULL;
if (!is_numeric($argument)) {
return;
}
if ($argument > 10) {
$argument = 10;
}
if ($argument < -10) {
$argument = -10;
}
// figure out which argument preceded us.
$keys = array_reverse(array_keys($this->view->argument));
$skip = TRUE;
foreach ($keys as $key) {
if ($key == $this->options['id']) {
$skip = FALSE;
continue;
}
if ($skip) {
continue;
}
if (empty($this->view->argument[$key])) {
continue;
}
$handler = &$this->view->argument[$key];
if (empty($handler->definition['accept depth modifier'])) {
continue;
}
// Finally!
$handler->options['depth'] = $argument;
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\EntityArgument;
/**
* Argument handler for basic taxonomy tid.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'taxonomy',
)]
class Taxonomy extends EntityArgument {}

View File

@@ -0,0 +1,16 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\EntityArgument;
/**
* Argument handler to accept a vocabulary id.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'vocabulary_vid',
)]
class VocabularyVid extends EntityArgument {}

View File

@@ -0,0 +1,239 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument_default;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\taxonomy\TermInterface;
use Drupal\views\Attribute\ViewsArgumentDefault;
use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
/**
* Taxonomy tid default argument.
*/
#[ViewsArgumentDefault(
id: 'taxonomy_tid',
title: new TranslatableMarkup('Taxonomy term ID from URL'),
)]
class Tid extends ArgumentDefaultPluginBase implements CacheableDependencyInterface {
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a new Tid instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition. *
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match, VocabularyStorageInterface $vocabulary_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['term_page'] = ['default' => TRUE];
$options['node'] = ['default' => FALSE];
$options['anyall'] = ['default' => ','];
$options['limit'] = ['default' => FALSE];
$options['vids'] = ['default' => []];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['term_page'] = [
'#type' => 'checkbox',
'#title' => $this->t('Load default filter from term page'),
'#default_value' => $this->options['term_page'],
];
$form['node'] = [
'#type' => 'checkbox',
'#title' => $this->t("Load default filter from node page, that's good for related taxonomy blocks"),
'#default_value' => $this->options['node'],
];
$form['limit'] = [
'#type' => 'checkbox',
'#title' => $this->t('Limit terms by vocabulary'),
'#default_value' => $this->options['limit'],
'#states' => [
'visible' => [
':input[name="options[argument_default][taxonomy_tid][node]"]' => ['checked' => TRUE],
],
],
];
$options = [];
$vocabularies = $this->vocabularyStorage->loadMultiple();
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
$form['vids'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Vocabularies'),
'#options' => $options,
'#default_value' => $this->options['vids'],
'#states' => [
'visible' => [
':input[name="options[argument_default][taxonomy_tid][limit]"]' => ['checked' => TRUE],
':input[name="options[argument_default][taxonomy_tid][node]"]' => ['checked' => TRUE],
],
],
];
$form['anyall'] = [
'#type' => 'radios',
'#title' => $this->t('Multiple-value handling'),
'#default_value' => $this->options['anyall'],
'#options' => [
',' => $this->t('Filter to items that share all terms'),
'+' => $this->t('Filter to items that share any term'),
],
'#states' => [
'visible' => [
':input[name="options[argument_default][taxonomy_tid][node]"]' => ['checked' => TRUE],
],
],
];
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state, &$options = []) {
// Filter unselected items so we don't unnecessarily store giant arrays.
$options['vids'] = array_filter($options['vids']);
}
/**
* {@inheritdoc}
*/
public function getArgument() {
// Load default argument from taxonomy page.
if (!empty($this->options['term_page'])) {
if (($taxonomy_term = $this->routeMatch->getParameter('taxonomy_term')) && $taxonomy_term instanceof TermInterface) {
return $taxonomy_term->id();
}
}
// Load default argument from node.
if (!empty($this->options['node'])) {
// Just check, if a node could be detected.
if (($node = $this->routeMatch->getParameter('node')) && $node instanceof NodeInterface) {
$taxonomy = [];
foreach ($node->getFieldDefinitions() as $field) {
if ($field->getType() == 'entity_reference' && $field->getSetting('target_type') == 'taxonomy_term') {
$taxonomy_terms = $node->{$field->getName()}->referencedEntities();
/** @var \Drupal\taxonomy\TermInterface $taxonomy_term */
foreach ($taxonomy_terms as $taxonomy_term) {
$taxonomy[$taxonomy_term->id()] = $taxonomy_term->bundle();
}
}
}
if (!empty($this->options['limit'])) {
$tids = [];
// filter by vocabulary
foreach ($taxonomy as $tid => $vocab) {
if (!empty($this->options['vids'][$vocab])) {
$tids[] = $tid;
}
}
return implode($this->options['anyall'], $tids);
}
// Return all tids.
else {
return implode($this->options['anyall'], array_keys($taxonomy));
}
}
}
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = parent::getCacheTags();
if (!empty($this->options['node'])) {
if (($node = $this->routeMatch->getParameter('node')) && $node instanceof NodeInterface) {
$tags = Cache::mergeTags($tags, $node->getCacheTags());
}
}
return $tags;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url'];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach ($this->vocabularyStorage->loadMultiple(array_keys($this->options['vids'])) as $vocabulary) {
$dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
}
return $dependencies;
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument_validator;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsArgumentValidator;
use Drupal\views\Plugin\views\argument_validator\Entity;
/**
* Validates whether a term name is a valid term argument.
*/
#[ViewsArgumentValidator(
id: 'taxonomy_term_name',
title: new TranslatableMarkup('Taxonomy term name'),
entity_type: 'taxonomy_term'
)]
class TermName extends Entity {
/**
* The taxonomy term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_type_bundle_info);
// Not handling exploding term names.
$this->multipleCapable = FALSE;
$this->termStorage = $entity_type_manager->getStorage('taxonomy_term');
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['transform'] = ['default' => FALSE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['transform'] = [
'#type' => 'checkbox',
'#title' => $this->t('Transform dashes in URL to spaces in term name filter values'),
'#default_value' => $this->options['transform'],
];
}
/**
* {@inheritdoc}
*/
public function validateArgument($argument) {
if ($this->options['transform']) {
$argument = str_replace('-', ' ', $argument);
$this->argument->argument = $argument;
}
// If bundles is set then restrict the loaded terms to the given bundles.
if (!empty($this->options['bundles'])) {
$terms = $this->termStorage->loadByProperties(['name' => $argument, 'vid' => $this->options['bundles']]);
}
else {
$terms = $this->termStorage->loadByProperties(['name' => $argument]);
}
// $terms are already bundle tested but we need to test access control.
foreach ($terms as $term) {
if ($this->validateEntity($term)) {
return TRUE;
}
}
return FALSE;
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Drupal\taxonomy\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\PrerenderList;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
/**
* Field handler to display all taxonomy terms of a node.
*
* @ingroup views_field_handlers
*/
#[ViewsField("taxonomy_index_tid")]
class TaxonomyIndexTid extends PrerenderList {
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a TaxonomyIndexTid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
parent::init($view, $display, $options);
// @todo Wouldn't it be possible to use $this->base_table and no if here?
if ($view->storage->get('base_table') == 'node_field_revision') {
$this->additional_fields['nid'] = ['table' => 'node_field_revision', 'field' => 'nid'];
}
else {
$this->additional_fields['nid'] = ['table' => 'node_field_data', 'field' => 'nid'];
}
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_taxonomy'] = ['default' => TRUE];
$options['limit'] = ['default' => FALSE];
$options['vids'] = ['default' => []];
return $options;
}
/**
* Provide "link to term" option.
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['link_to_taxonomy'] = [
'#title' => $this->t('Link this field to its term page'),
'#type' => 'checkbox',
'#default_value' => !empty($this->options['link_to_taxonomy']),
];
$form['limit'] = [
'#type' => 'checkbox',
'#title' => $this->t('Limit terms by vocabulary'),
'#default_value' => $this->options['limit'],
];
$options = [];
$vocabularies = $this->vocabularyStorage->loadMultiple();
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
$form['vids'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Vocabularies'),
'#options' => $options,
'#default_value' => $this->options['vids'],
'#states' => [
'visible' => [
':input[name="options[limit]"]' => ['checked' => TRUE],
],
],
];
parent::buildOptionsForm($form, $form_state);
}
/**
* Add this term to the query.
*/
public function query() {
$this->addAdditionalFields();
}
public function preRender(&$values) {
$vocabularies = $this->vocabularyStorage->loadMultiple();
$this->field_alias = $this->aliases['nid'];
$nids = [];
foreach ($values as $result) {
if (!empty($result->{$this->aliases['nid']})) {
$nids[] = $result->{$this->aliases['nid']};
}
}
if ($nids) {
$vids = array_filter($this->options['vids']);
if (empty($this->options['limit'])) {
$vids = [];
}
$result = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getNodeTerms($nids, $vids);
foreach ($result as $node_nid => $data) {
foreach ($data as $tid => $term) {
$this->items[$node_nid][$tid]['name'] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
$this->items[$node_nid][$tid]['tid'] = $tid;
$this->items[$node_nid][$tid]['vocabulary_vid'] = $term->bundle();
$this->items[$node_nid][$tid]['vocabulary'] = $vocabularies[$term->bundle()]->label();
if (!empty($this->options['link_to_taxonomy'])) {
$this->items[$node_nid][$tid]['make_link'] = TRUE;
$this->items[$node_nid][$tid]['path'] = 'taxonomy/term/' . $tid;
}
}
}
}
}
public function render_item($count, $item) {
return $item['name'];
}
protected function documentSelfTokens(&$tokens) {
$tokens['{{ ' . $this->options['id'] . '__tid' . ' }}'] = $this->t('The taxonomy term ID for the term.');
$tokens['{{ ' . $this->options['id'] . '__name' . ' }}'] = $this->t('The taxonomy term name for the term.');
$tokens['{{ ' . $this->options['id'] . '__vocabulary_vid' . ' }}'] = $this->t('The machine name for the vocabulary the term belongs to.');
$tokens['{{ ' . $this->options['id'] . '__vocabulary' . ' }}'] = $this->t('The name for the vocabulary the term belongs to.');
}
protected function addSelfTokens(&$tokens, $item) {
foreach (['tid', 'name', 'vocabulary_vid', 'vocabulary'] as $token) {
$tokens['{{ ' . $this->options['id'] . '__' . $token . ' }}'] = $item[$token] ?? '';
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\taxonomy\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\views\ResultRow;
/**
* Displays taxonomy term names and allows converting spaces to hyphens.
*
* @ingroup views_field_handlers
*/
#[ViewsField("term_name")]
class TermName extends EntityField {
/**
* {@inheritdoc}
*/
public function getItems(ResultRow $values) {
$items = parent::getItems($values);
if ($this->options['convert_spaces']) {
foreach ($items as &$item) {
// Replace spaces with hyphens.
$name = str_replace(' ', '-', $item['raw']->get('value')->getValue());
empty($this->options['settings']['link_to_entity']) ?
$item['rendered']['#context']['value'] = $name :
$item['rendered']['#title']['#context']['value'] = $name;
}
}
return $items;
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['convert_spaces'] = ['default' => FALSE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['convert_spaces'] = [
'#title' => $this->t('Convert spaces in term names to hyphens'),
'#type' => 'checkbox',
'#default_value' => !empty($this->options['convert_spaces']),
];
parent::buildOptionsForm($form, $form_state);
}
}

View File

@@ -0,0 +1,420 @@
<?php
namespace Drupal\taxonomy\Plugin\views\filter;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\TermStorageInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\filter\ManyToOne;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Filter by term id.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("taxonomy_index_tid")]
class TaxonomyIndexTid extends ManyToOne {
/**
* Stores the exposed input for this filter.
*
* @var array|null
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
public $validated_exposed_input = NULL;
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* The term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a TaxonomyIndexTid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
* @param \Drupal\taxonomy\TermStorageInterface $term_storage
* The term storage.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->vocabularyStorage = $vocabulary_storage;
$this->termStorage = $term_storage;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary'),
$container->get('entity_type.manager')->getStorage('taxonomy_term'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
parent::init($view, $display, $options);
if (!empty($this->definition['vocabulary'])) {
$this->options['vid'] = $this->definition['vocabulary'];
}
}
public function hasExtraOptions() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getValueOptions() {
return $this->valueOptions;
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['type'] = ['default' => 'textfield'];
$options['limit'] = ['default' => TRUE];
$options['vid'] = ['default' => ''];
$options['hierarchy'] = ['default' => FALSE];
$options['error_message'] = ['default' => TRUE];
return $options;
}
public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
$vocabularies = $this->vocabularyStorage->loadMultiple();
$options = [];
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
if ($this->options['limit']) {
// We only do this when the form is displayed.
if (empty($this->options['vid'])) {
$first_vocabulary = reset($vocabularies);
$this->options['vid'] = $first_vocabulary->id();
}
if (empty($this->definition['vocabulary'])) {
$form['vid'] = [
'#type' => 'radios',
'#title' => $this->t('Vocabulary'),
'#options' => $options,
'#description' => $this->t('Select which vocabulary to show terms for in the regular options.'),
'#default_value' => $this->options['vid'],
];
}
}
$form['type'] = [
'#type' => 'radios',
'#title' => $this->t('Selection type'),
'#options' => ['select' => $this->t('Dropdown'), 'textfield' => $this->t('Autocomplete')],
'#default_value' => $this->options['type'],
];
$form['hierarchy'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show hierarchy in dropdown'),
'#default_value' => !empty($this->options['hierarchy']),
'#states' => [
'visible' => [
':input[name="options[type]"]' => ['value' => 'select'],
],
],
];
}
protected function valueForm(&$form, FormStateInterface $form_state) {
$vocabulary = $this->vocabularyStorage->load($this->options['vid']);
if (empty($vocabulary) && $this->options['limit']) {
$form['markup'] = [
'#markup' => '<div class="js-form-item form-item">' . $this->t('An invalid vocabulary is selected. Change it in the options.') . '</div>',
];
return;
}
if ($this->options['type'] == 'textfield') {
$terms = $this->value ? Term::loadMultiple(($this->value)) : [];
$form['value'] = [
'#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
'#type' => 'textfield',
'#default_value' => EntityAutocomplete::getEntityLabels($terms),
];
if ($this->options['limit']) {
$form['value']['#type'] = 'entity_autocomplete';
$form['value']['#target_type'] = 'taxonomy_term';
$form['value']['#selection_settings']['target_bundles'] = [$vocabulary->id()];
$form['value']['#tags'] = TRUE;
$form['value']['#process_default_value'] = FALSE;
}
}
else {
if (!empty($this->options['hierarchy']) && $this->options['limit']) {
$tree = $this->termStorage->loadTree($vocabulary->id(), 0, NULL, TRUE);
$options = [];
if ($tree) {
foreach ($tree as $term) {
if (!$term->isPublished() && !$this->currentUser->hasPermission('administer taxonomy')) {
continue;
}
$choice = new \stdClass();
$choice->option = [$term->id() => str_repeat('-', $term->depth) . \Drupal::service('entity.repository')->getTranslationFromContext($term)->label()];
$options[] = $choice;
}
}
}
else {
$options = [];
$query = \Drupal::entityQuery('taxonomy_term')
->accessCheck(TRUE)
// @todo Sorting on vocabulary properties -
// https://www.drupal.org/node/1821274.
->sort('weight')
->sort('name')
->addTag('taxonomy_term_access');
if (!$this->currentUser->hasPermission('administer taxonomy')) {
$query->condition('status', 1);
}
if ($this->options['limit']) {
$query->condition('vid', $vocabulary->id());
}
$terms = Term::loadMultiple($query->execute());
foreach ($terms as $term) {
$options[$term->id()] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
}
}
$default_value = (array) $this->value;
if ($exposed = $form_state->get('exposed')) {
$identifier = $this->options['expose']['identifier'];
if (!empty($this->options['expose']['reduce'])) {
$options = $this->reduceValueOptions($options);
if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
$default_value = [];
}
}
if (empty($this->options['expose']['multiple'])) {
if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
$default_value = 'All';
}
elseif (empty($default_value)) {
$keys = array_keys($options);
$default_value = array_shift($keys);
}
// Due to #1464174 there is a chance that array('') was saved in the admin ui.
// Let's choose a safe default value.
elseif ($default_value == ['']) {
$default_value = 'All';
}
else {
$copy = $default_value;
$default_value = array_shift($copy);
}
}
}
$form['value'] = [
'#type' => 'select',
'#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
'#multiple' => TRUE,
'#options' => $options,
'#size' => min(9, count($options)),
'#default_value' => $default_value,
];
$user_input = $form_state->getUserInput();
if ($exposed && isset($identifier) && !isset($user_input[$identifier])) {
$user_input[$identifier] = $default_value;
$form_state->setUserInput($user_input);
}
}
if (!$form_state->get('exposed')) {
// Retain the helper option
$this->helper->buildOptionsForm($form, $form_state);
// Show help text if not exposed to end users.
$form['value']['#description'] = $this->t('Leave blank for all. Otherwise, the first selected term will be the default instead of "Any".');
}
}
protected function valueValidate($form, FormStateInterface $form_state) {
// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
return;
}
$tids = [];
if ($values = $form_state->getValue(['options', 'value'])) {
foreach ($values as $value) {
$tids[] = $value['target_id'];
}
}
$form_state->setValue(['options', 'value'], $tids);
}
public function acceptExposedInput($input) {
if (empty($this->options['exposed'])) {
return TRUE;
}
// We need to know the operator, which is normally set in
// \Drupal\views\Plugin\views\filter\FilterPluginBase::acceptExposedInput(),
// before we actually call the parent version of ourselves.
if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
$this->operator = $input[$this->options['expose']['operator_id']];
}
// If view is an attachment and is inheriting exposed filters, then assume
// exposed input has already been validated
if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) {
$this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
}
// If we're checking for EMPTY or NOT, we don't need any input, and we can
// say that our input conditions are met by just having the right operator.
if ($this->operator == 'empty' || $this->operator == 'not empty') {
return TRUE;
}
// If it's non-required and there's no value don't bother filtering.
if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
return FALSE;
}
$rc = parent::acceptExposedInput($input);
if ($rc) {
// If we have previously validated input, override.
if (isset($this->validated_exposed_input)) {
$this->value = $this->validated_exposed_input;
}
}
return $rc;
}
public function validateExposed(&$form, FormStateInterface $form_state) {
if (empty($this->options['exposed'])) {
return;
}
$identifier = $this->options['expose']['identifier'];
$input = $form_state->getValue($identifier);
if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
$this->validated_exposed_input = $this->options['group_info']['group_items'][$input]['value'];
return;
}
// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
if ($form_state->getValue($identifier) != 'All') {
$this->validated_exposed_input = (array) $form_state->getValue($identifier);
}
return;
}
if (empty($this->options['expose']['identifier'])) {
return;
}
if ($values = $form_state->getValue($identifier)) {
foreach ($values as $value) {
$this->validated_exposed_input[] = $value['target_id'];
}
}
}
protected function valueSubmit($form, FormStateInterface $form_state) {
// prevent array_filter from messing up our arrays in parent submit.
}
public function buildExposeForm(&$form, FormStateInterface $form_state) {
parent::buildExposeForm($form, $form_state);
if ($this->options['type'] != 'select') {
unset($form['expose']['reduce']);
}
$form['error_message'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display error message'),
'#default_value' => !empty($this->options['error_message']),
];
}
public function adminSummary() {
// set up $this->valueOptions for the parent summary
$this->valueOptions = [];
if ($this->value) {
$this->value = array_filter($this->value);
$terms = Term::loadMultiple($this->value);
foreach ($terms as $term) {
$this->valueOptions[$term->id()] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
}
}
return parent::adminSummary();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$vocabulary = $this->vocabularyStorage->load($this->options['vid']);
$dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
foreach ($this->termStorage->loadMultiple($this->options['value']) as $term) {
$dependencies[$term->getConfigDependencyKey()][] = $term->getConfigDependencyName();
}
return $dependencies;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\taxonomy\Plugin\views\filter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\TaxonomyIndexDepthQueryTrait;
use Drupal\views\Attribute\ViewsFilter;
/**
* Filter handler for taxonomy terms with depth.
*
* This handler is actually part of the node table and has some restrictions,
* because it uses a subquery to find nodes with.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("taxonomy_index_tid_depth")]
class TaxonomyIndexTidDepth extends TaxonomyIndexTid {
use TaxonomyIndexDepthQueryTrait;
public function operatorOptions($which = 'title') {
return [
'or' => $this->t('Is one of'),
];
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['depth'] = ['default' => 0];
return $options;
}
public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildExtraOptionsForm($form, $form_state);
$form['depth'] = [
'#type' => 'weight',
'#title' => $this->t('Depth'),
'#default_value' => $this->options['depth'],
'#description' => $this->t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'),
];
}
public function query() {
// If no filter values are present, then do nothing.
if (count($this->value) == 0) {
return;
}
elseif (count($this->value) == 1) {
// Sometimes $this->value is an array with a single element so convert it.
if (is_array($this->value)) {
$this->value = current($this->value);
}
}
// The normal use of ensureMyTable() here breaks Views.
// So instead we trick the filter into using the alias of the base table.
// See https://www.drupal.org/node/271833.
// If a relationship is set, we must use the alias it provides.
if (!empty($this->relationship)) {
$this->tableAlias = $this->relationship;
}
// If no relationship, then use the alias of the base table.
else {
$this->tableAlias = $this->query->ensureTable($this->view->storage->get('base_table'));
}
$this->addSubQueryJoin($this->value);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Drupal\taxonomy\Plugin\views\relationship;
use Drupal\Core\Database\Database;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
use Drupal\views\Attribute\ViewsRelationship;
use Drupal\views\Plugin\views\relationship\RelationshipPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Relationship handler to return the taxonomy terms of nodes.
*
* @ingroup views_relationship_handlers
*/
#[ViewsRelationship("node_term_data")]
class NodeTermData extends RelationshipPluginBase {
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a NodeTermData object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['vids'] = ['default' => []];
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$vocabularies = $this->vocabularyStorage->loadMultiple();
$options = [];
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
$form['vids'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Vocabularies'),
'#options' => $options,
'#default_value' => $this->options['vids'],
'#description' => $this->t('Choose which vocabularies you wish to relate. Remember that every term found will create a new record, so this relationship is best used on just one vocabulary that has only one term per node.'),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
// Transform the #type = checkboxes value to a numerically indexed array,
// because the config schema expects a sequence, not a mapping.
$vids = $form_state->getValue(['options', 'vids']);
$form_state->setValue(['options', 'vids'], array_values(array_filter($vids)));
}
/**
* Called to implement a relationship in a query.
*/
public function query() {
$this->ensureMyTable();
$def = $this->definition;
$def['table'] = 'taxonomy_term_field_data';
if (!array_filter($this->options['vids'])) {
$taxonomy_index = $this->query->addTable('taxonomy_index', $this->relationship);
$def['left_table'] = $taxonomy_index;
$def['left_field'] = 'tid';
$def['field'] = 'tid';
$def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER';
}
else {
// If vocabularies are supplied join a subselect instead
$def['left_table'] = $this->tableAlias;
$def['left_field'] = 'nid';
$def['field'] = 'nid';
$def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER';
$def['adjusted'] = TRUE;
$query = Database::getConnection()->select('taxonomy_term_field_data', 'td');
$query->addJoin($def['type'], 'taxonomy_index', 'tn', '[tn].[tid] = [td].[tid]');
$query->condition('td.vid', array_filter($this->options['vids']), 'IN');
if (empty($this->query->options['disable_sql_rewrite'])) {
$query->addTag('taxonomy_term_access');
}
$query->fields('td');
$query->fields('tn', ['nid']);
$def['table formula'] = $query;
}
$join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $def);
// use a short alias for this:
$alias = $def['table'] . '_' . $this->table;
$this->alias = $this->query->addRelationship($alias, $join, 'taxonomy_term_field_data', $this->relationship);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach ($this->options['vids'] as $vocabulary_id) {
if ($vocabulary = $this->vocabularyStorage->load($vocabulary_id)) {
$dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
}
}
return $dependencies;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\taxonomy\Plugin\views\wizard;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsWizard;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* Tests creating taxonomy views with the wizard.
*/
#[ViewsWizard(
id: 'taxonomy_term',
title: new TranslatableMarkup('Taxonomy terms'),
base_table: 'taxonomy_term_field_data'
)]
class TaxonomyTerm extends WizardPluginBase {
/**
* {@inheritdoc}
*/
protected function defaultDisplayOptions() {
$display_options = parent::defaultDisplayOptions();
// Add permission-based access control.
$display_options['access']['type'] = 'perm';
$display_options['access']['options']['perm'] = 'access content';
// Remove the default fields, since we are customizing them here.
unset($display_options['fields']);
/* Field: Taxonomy: Term */
$display_options['fields']['name']['id'] = 'name';
$display_options['fields']['name']['table'] = 'taxonomy_term_field_data';
$display_options['fields']['name']['field'] = 'name';
$display_options['fields']['name']['entity_type'] = 'taxonomy_term';
$display_options['fields']['name']['entity_field'] = 'name';
$display_options['fields']['name']['label'] = '';
$display_options['fields']['name']['alter']['alter_text'] = 0;
$display_options['fields']['name']['alter']['make_link'] = 0;
$display_options['fields']['name']['alter']['absolute'] = 0;
$display_options['fields']['name']['alter']['trim'] = 0;
$display_options['fields']['name']['alter']['word_boundary'] = 0;
$display_options['fields']['name']['alter']['ellipsis'] = 0;
$display_options['fields']['name']['alter']['strip_tags'] = 0;
$display_options['fields']['name']['alter']['html'] = 0;
$display_options['fields']['name']['hide_empty'] = 0;
$display_options['fields']['name']['empty_zero'] = 0;
$display_options['fields']['name']['type'] = 'string';
$display_options['fields']['name']['settings']['link_to_entity'] = 1;
$display_options['fields']['name']['plugin_id'] = 'term_name';
return $display_options;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\taxonomy;
use Drupal\views\Views;
/**
* Builds a performant depth subquery and adds it as a join to the query.
*
* This is performant because:
* - It creates multiple queries on taxonomy_index with inner joins to
* taxonomy_term__parent. These queries are combined together into a subquery
* using unions to select all the node IDs with the terms in the hierarchy.
* - It joins the resulting query to the main views query using an INNER JOIN.
*
* For example, if the $tids value is '718' and depth is configured to 2, the
* resulting JOIN to node_field_data will be:
* @code
* INNER JOIN (SELECT tn.nid AS nid
* FROM
* taxonomy_index tn
* WHERE tn.tid = '718' UNION SELECT tn.nid AS nid
* FROM
* taxonomy_index tn
* INNER JOIN taxonomy_term__parent th ON tn.tid = th.entity_id
* INNER JOIN taxonomy_term__parent th1 ON th.parent_target_id = th1.entity_id
* WHERE th1.entity_id = '718' UNION SELECT tn.nid AS nid
* FROM
* taxonomy_index tn
* INNER JOIN taxonomy_term__parent th ON tn.tid = th.entity_id
* INNER JOIN taxonomy_term__parent th1 ON th.parent_target_id = th1.entity_id
* INNER JOIN taxonomy_term__parent th2 ON th1.parent_target_id = th2.entity_id
* WHERE th2.entity_id = '718') taxonomy_index_depth ON node_field_data.nid = taxonomy_index_depth.nid
* @endcode
*/
trait TaxonomyIndexDepthQueryTrait {
/**
* Builds a performant depth subquery and adds it as a join to the query.
*
* @param string|array $tids
* The terms ID(s) to do a depth search for.
*/
protected function addSubQueryJoin($tids): void {
$connection = $this->query->getConnection();
$operator = is_array($tids) ? 'IN' : '=';
// Create the depth 0 subquery.
$subquery = $connection->select('taxonomy_index', 'tn');
$subquery->addField('tn', 'nid');
$subquery->condition('tn.tid', $tids, $operator);
if ($this->options['depth'] !== 0) {
// Set $left_field and $right_field depending on whether we are traversing
// up or down the hierarchy.
if ($this->options['depth'] > 0) {
$left_field = 'parent_target_id';
$right_field = 'entity_id';
}
else {
$left_field = 'entity_id';
$right_field = 'parent_target_id';
}
// Traverse the hierarchy to check the child or parent terms.
foreach (range(1, abs($this->options['depth'])) as $count) {
$union_query = $connection->select('taxonomy_index', 'tn');
$union_query->addField('tn', 'nid');
$left_join = "[tn].[tid]";
if ($this->options['depth'] > 0) {
$union_query->join('taxonomy_term__parent', "th", "$left_join = [th].[entity_id]");
$left_join = "[th].[$left_field]";
}
foreach (range(1, $count) as $inner_count) {
$union_query->join('taxonomy_term__parent', "th$inner_count", "$left_join = [th$inner_count].[$right_field]");
$left_join = "[th$inner_count].[$left_field]";
}
$union_query->condition("th$inner_count.entity_id", $tids, $operator);
$subquery->union($union_query);
}
}
// Add the subquery as a join.
$definition['left_table'] = $this->tableAlias;
$definition['left_field'] = $this->realField;
$definition['field'] = 'nid';
$definition['type'] = 'INNER';
$definition['adjusted'] = TRUE;
$definition['table formula'] = $subquery;
$join = Views::pluginManager('join')->createInstance('standard', $definition);
// There is no $base as we are joining to a query.
$this->query->addRelationship('taxonomy_index_depth', $join, NULL, $this->relationship);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\BundlePermissionHandlerTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\taxonomy\Entity\Vocabulary;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions of the taxonomy module.
*
* @see taxonomy.permissions.yml
*/
class TaxonomyPermissions implements ContainerInjectionInterface {
use BundlePermissionHandlerTrait;
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a TaxonomyPermissions instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager'));
}
/**
* Get taxonomy permissions.
*
* @return array
* Permissions array.
*/
public function permissions() {
return $this->generatePermissions(Vocabulary::loadMultiple(), [$this, 'buildPermissions']);
}
/**
* Builds a standard list of taxonomy term permissions for a given vocabulary.
*
* @param \Drupal\taxonomy\VocabularyInterface $vocabulary
* The vocabulary.
*
* @return array
* An array of permission names and descriptions.
*/
protected function buildPermissions(VocabularyInterface $vocabulary) {
$id = $vocabulary->id();
$args = ['%vocabulary' => $vocabulary->label()];
return [
"create terms in $id" => ['title' => $this->t('%vocabulary: Create terms', $args)],
"delete terms in $id" => ['title' => $this->t('%vocabulary: Delete terms', $args)],
"edit terms in $id" => ['title' => $this->t('%vocabulary: Edit terms', $args)],
"view term revisions in $id" => ['title' => $this->t('%vocabulary: View term revisions', $args)],
"revert term revisions in $id" => [
'title' => $this->t('%vocabulary: Revert term revisions', $args),
'description' => $this->t('To revert a revision you also need permission to edit the taxonomy term.'),
],
"delete term revisions in $id" => [
'title' => $this->t('%vocabulary: Delete term revisions', $args),
'description' => $this->t('To delete a revision you also need permission to delete the taxonomy term.'),
],
];
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the taxonomy term entity type.
*
* @see \Drupal\taxonomy\Entity\Term
*/
class TermAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($account->hasPermission('administer taxonomy')) {
return AccessResult::allowed()->cachePerPermissions();
}
switch ($operation) {
case 'view':
$access_result = AccessResult::allowedIf($account->hasPermission('access content') && $entity->isPublished())
->cachePerPermissions()
->addCacheableDependency($entity);
if (!$access_result->isAllowed()) {
$access_result->setReason("The 'access content' permission is required and the taxonomy term must be published.");
}
return $access_result;
case 'update':
if ($account->hasPermission("edit terms in {$entity->bundle()}")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'edit terms in {$entity->bundle()}' OR 'administer taxonomy'.");
case 'delete':
if ($account->hasPermission("delete terms in {$entity->bundle()}")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'delete terms in {$entity->bundle()}' OR 'administer taxonomy'.");
case 'view revision':
case 'view all revisions':
if ($account->hasPermission("view term revisions in {$entity->bundle()}") || $account->hasPermission("view all taxonomy revisions")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'view revisions in {$entity->bundle()}' OR 'view all taxonomy revisions'.");
case 'revert':
if (($account->hasPermission("revert term revisions in {$entity->bundle()}") && $account->hasPermission("edit terms in {$entity->bundle()}")) || $account->hasPermission("revert all taxonomy revisions")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'revert term revisions in {$entity->bundle()}' OR 'revert all taxonomy revisions'.");
case 'delete revision':
if (($account->hasPermission("delete term revisions in {$entity->bundle()}") && $account->hasPermission("delete terms in {$entity->bundle()}")) || $account->hasPermission("delete all taxonomy revisions")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'delete term revisions in {$entity->bundle()}' OR 'delete all taxonomy revisions'.");
default:
// No opinion.
return AccessResult::neutral()->cachePerPermissions();
}
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermissions($account, ["create terms in $entity_bundle", 'administer taxonomy'], 'OR');
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a custom taxonomy breadcrumb builder that uses the term hierarchy.
*/
class TermBreadcrumbBuilder implements BreadcrumbBuilderInterface {
use StringTranslationTrait;
/**
* The entity repository manager.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs the TermBreadcrumbBuilder.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository) {
$this->entityTypeManager = $entity_type_manager;
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() == 'entity.taxonomy_term.canonical'
&& $route_match->getParameter('taxonomy_term') instanceof TermInterface;
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = new Breadcrumb();
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
$term = $route_match->getParameter('taxonomy_term');
// Breadcrumb needs to have terms cacheable metadata as a cacheable
// dependency even though it is not shown in the breadcrumb because e.g. its
// parent might have changed.
$breadcrumb->addCacheableDependency($term);
// @todo This overrides any other possible breadcrumb and is a pure
// hard-coded presumption. Make this behavior configurable per
// vocabulary or term.
$parents = $this->entityTypeManager->getStorage('taxonomy_term')->loadAllParents($term->id());
// Remove current term being accessed.
array_shift($parents);
foreach (array_reverse($parents) as $term) {
$term = $this->entityRepository->getTranslationFromContext($term);
$breadcrumb->addCacheableDependency($term);
$breadcrumb->addLink(Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]));
}
// This breadcrumb builder is based on a route parameter, and hence it
// depends on the 'route' cache context.
$breadcrumb->addCacheContexts(['route']);
return $breadcrumb;
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Base for handler for taxonomy term edit forms.
*
* @internal
*/
class TermForm extends ContentEntityForm {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$term = $this->entity;
$vocab_storage = $this->entityTypeManager->getStorage('taxonomy_vocabulary');
/** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
$taxonomy_storage = $this->entityTypeManager->getStorage('taxonomy_term');
$vocabulary = $vocab_storage->load($term->bundle());
$parent = $this->getParentIds($term);
$form_state->set(['taxonomy', 'parent'], $parent);
$form_state->set(['taxonomy', 'vocabulary'], $vocabulary);
$form['relations'] = [
'#type' => 'details',
'#title' => $this->t('Relations'),
'#open' => $taxonomy_storage->getVocabularyHierarchyType($vocabulary->id()) == VocabularyInterface::HIERARCHY_MULTIPLE,
'#weight' => 10,
];
// \Drupal\taxonomy\TermStorageInterface::loadTree() and
// \Drupal\taxonomy\TermStorageInterface::loadParents() may contain large
// numbers of items so we check for taxonomy.settings:override_selector
// before loading the full vocabulary. Contrib modules can then intercept
// before hook_form_alter to provide scalable alternatives.
if (!$this->config('taxonomy.settings')->get('override_selector')) {
$exclude = [];
if (!$term->isNew()) {
$children = $taxonomy_storage->loadTree($vocabulary->id(), $term->id());
// A term can't be the child of itself, nor of its children.
foreach ($children as $child) {
$exclude[] = $child->tid;
}
$exclude[] = $term->id();
}
$tree = $taxonomy_storage->loadTree($vocabulary->id());
$options = ['<' . $this->t('root') . '>'];
if (empty($parent)) {
$parent = [0];
}
foreach ($tree as $item) {
if (!in_array($item->tid, $exclude)) {
$options[$item->tid] = str_repeat('-', $item->depth) . $item->name;
}
}
}
else {
$options = ['<' . $this->t('root') . '>'];
$parent = [0];
}
if ($this->getRequest()->query->has('parent')) {
$parent = array_values(array_intersect(
array_keys($options),
(array) $this->getRequest()->query->all()['parent'],
));
}
$form['relations']['parent'] = [
'#type' => 'select',
'#title' => $this->t('Parent terms'),
'#options' => $options,
'#default_value' => $parent,
'#multiple' => TRUE,
];
$form['relations']['weight'] = [
'#type' => 'textfield',
'#title' => $this->t('Weight'),
'#size' => 6,
'#default_value' => $term->getWeight(),
'#description' => $this->t('Terms are displayed in ascending order by weight.'),
'#required' => TRUE,
];
$form['vid'] = [
'#type' => 'value',
'#value' => $vocabulary->id(),
];
$form['tid'] = [
'#type' => 'value',
'#value' => $term->id(),
];
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$element = parent::actions($form, $form_state);
if (!$this->getRequest()->query->has('destination')) {
$element['overview'] = [
'#type' => 'submit',
'#value' => $this->t('Save and go to list'),
'#weight' => 20,
'#submit' => array_merge($element['submit']['#submit'], ['::overview']),
'#access' => $this->currentUser()->hasPermission('access taxonomy overview'),
];
}
return $element;
}
/**
* Form submission handler for the 'overview' action.
*
* @param array[] $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function overview(array $form, FormStateInterface $form_state): void {
$vocabulary = $this->entityTypeManager->getStorage('taxonomy_vocabulary')
->load($form_state->getValue('vid'));
$form_state->setRedirectUrl($vocabulary->toUrl('overview-form'));
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Ensure numeric values.
if ($form_state->hasValue('weight') && !is_numeric($form_state->getValue('weight'))) {
$form_state->setErrorByName('weight', $this->t('Weight value must be numeric.'));
}
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
$term = parent::buildEntity($form, $form_state);
// Prevent leading and trailing spaces in term names.
$term->setName(trim($term->getName()));
// Assign parents with proper delta values starting from 0.
$term->parent = array_values($form_state->getValue('parent'));
return $term;
}
/**
* {@inheritdoc}
*/
protected function getEditedFieldNames(FormStateInterface $form_state) {
return array_merge(['parent', 'weight'], parent::getEditedFieldNames($form_state));
}
/**
* {@inheritdoc}
*/
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
// Manually flag violations of fields not handled by the form display. This
// is necessary as entity form displays only flag violations for fields
// contained in the display.
// @see ::form()
foreach ($violations->getByField('parent') as $violation) {
$form_state->setErrorByName('parent', $violation->getMessage());
}
foreach ($violations->getByField('weight') as $violation) {
$form_state->setErrorByName('weight', $violation->getMessage());
}
parent::flagViolations($violations, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$term = $this->entity;
$result = $term->save();
$edit_link = $term->toLink($this->t('Edit'), 'edit-form')->toString();
$view_link = $term->toLink()->toString();
switch ($result) {
case SAVED_NEW:
$this->messenger()->addStatus($this->t('Created new term %term.', ['%term' => $view_link]));
$this->logger('taxonomy')->info('Created new term %term.', ['%term' => $term->getName(), 'link' => $edit_link]);
break;
case SAVED_UPDATED:
$this->messenger()->addStatus($this->t('Updated term %term.', ['%term' => $view_link]));
$this->logger('taxonomy')->info('Updated term %term.', ['%term' => $term->getName(), 'link' => $edit_link]);
$form_state->setRedirect('entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]);
break;
}
$current_parent_count = count($form_state->getValue('parent'));
// Root doesn't count if it's the only parent.
if ($current_parent_count == 1 && $form_state->hasValue(['parent', 0])) {
$form_state->setValue('parent', []);
}
$form_state->setValue('tid', $term->id());
$form_state->set('tid', $term->id());
}
/**
* Returns term parent IDs, including the root.
*
* @param \Drupal\taxonomy\TermInterface $term
* The taxonomy term entity.
*
* @return array
* A list if parent term IDs.
*/
protected function getParentIds(TermInterface $term): array {
$parent = [];
// Get the parent directly from the term as
// \Drupal\taxonomy\TermStorageInterface::loadParents() excludes the root.
foreach ($term->get('parent') as $item) {
$parent[] = (int) $item->target_id;
}
return $parent;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionLogInterface;
/**
* Provides an interface defining a taxonomy term entity.
*/
interface TermInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface, RevisionLogInterface {
/**
* Gets the term description.
*
* @return string
* The term description.
*/
public function getDescription();
/**
* Sets the term description.
*
* @param string $description
* The term description.
*
* @return $this
*/
public function setDescription($description);
/**
* Gets the text format name for the term description.
*
* @return string
* The text format name.
*/
public function getFormat();
/**
* Sets the text format name for the term description.
*
* @param string $format
* The text format name.
*
* @return $this
*/
public function setFormat($format);
/**
* Gets the term name.
*
* @return string
* The term name.
*/
public function getName();
/**
* Sets the term name.
*
* @param string $name
* The term name.
*
* @return $this
*/
public function setName($name);
/**
* Gets the term weight.
*
* @return int
* The term weight.
*/
public function getWeight();
/**
* Sets the term weight.
*
* @param int $weight
* The term weight.
*
* @return $this
*/
public function setWeight($weight);
}

View File

@@ -0,0 +1,473 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\Sql\TableMappingInterface;
/**
* Defines a Controller class for taxonomy terms.
*/
class TermStorage extends SqlContentEntityStorage implements TermStorageInterface {
/**
* Array of term parents keyed by vocabulary ID and child term ID.
*
* @var array
*/
protected $treeParents = [];
/**
* Array of term ancestors keyed by vocabulary ID and parent term ID.
*
* @var array
*/
protected $treeChildren = [];
/**
* Array of terms in a tree keyed by vocabulary ID and term ID.
*
* @var array
*/
protected $treeTerms = [];
/**
* Array of loaded trees keyed by a cache id matching tree arguments.
*
* @var array
*/
protected $trees = [];
/**
* Term ancestry keyed by ancestor term ID, keyed by term ID.
*
* @var \Drupal\taxonomy\TermInterface[][]
*/
protected $ancestors;
/**
* The type of hierarchy allowed within a vocabulary.
*
* Possible values:
* - VocabularyInterface::HIERARCHY_DISABLED: No parents.
* - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*
* @var int[]
* An array of one the possible values above, keyed by vocabulary ID.
*/
protected $vocabularyHierarchyType;
/**
* {@inheritdoc}
*
* @param array $values
* An array of values to set, keyed by property name. A value for the
* vocabulary ID ('vid') is required.
*/
public function create(array $values = []) {
// Save new terms with no parents by default.
if (empty($values['parent'])) {
$values['parent'] = [0];
}
$entity = parent::create($values);
return $entity;
}
/**
* {@inheritdoc}
*/
public function resetCache(?array $ids = NULL) {
$this->ancestors = [];
$this->treeChildren = [];
$this->treeParents = [];
$this->treeTerms = [];
$this->trees = [];
$this->vocabularyHierarchyType = [];
parent::resetCache($ids);
}
/**
* {@inheritdoc}
*/
public function deleteTermHierarchy($tids) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. It is a no-op since 8.6.0. Parent references are automatically cleared when deleting a taxonomy term. See https://www.drupal.org/node/2936675', E_USER_DEPRECATED);
}
/**
* {@inheritdoc}
*/
public function updateTermHierarchy(EntityInterface $term) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. It is a no-op since 8.6.0. Parent references are automatically updated when updating a taxonomy term. See https://www.drupal.org/node/2936675', E_USER_DEPRECATED);
}
/**
* {@inheritdoc}
*/
public function loadParents($tid) {
$terms = [];
/** @var \Drupal\taxonomy\TermInterface $term */
if ($tid && $term = $this->load($tid)) {
foreach ($this->getParents($term) as $id => $parent) {
// This method currently doesn't return the <root> parent.
// @see https://www.drupal.org/node/2019905
if (!empty($id)) {
$terms[$id] = $parent;
}
}
}
return $terms;
}
/**
* Returns a list of parents of this term.
*
* @return \Drupal\taxonomy\TermInterface[]
* The parent taxonomy term entities keyed by term ID. If this term has a
* <root> parent, that item is keyed with 0 and will have NULL as value.
*
* @internal
* @todo Refactor away when TreeInterface is introduced.
*/
protected function getParents(TermInterface $term) {
$parents = $ids = [];
// Cannot use $this->get('parent')->referencedEntities() here because that
// strips out the '0' reference.
foreach ($term->get('parent') as $item) {
if ($item->target_id == 0) {
// The <root> parent.
$parents[0] = NULL;
continue;
}
$ids[] = $item->target_id;
}
// @todo Better way to do this? AND handle the NULL/0 parent?
// Querying the terms again so that the same access checks are run when
// getParents() is called as in Drupal version prior to 8.3.
$loaded_parents = [];
if ($ids) {
$query = \Drupal::entityQuery('taxonomy_term')
->accessCheck(TRUE)
->condition('tid', $ids, 'IN');
$loaded_parents = static::loadMultiple($query->execute());
}
return $parents + $loaded_parents;
}
/**
* {@inheritdoc}
*/
public function loadAllParents($tid) {
/** @var \Drupal\taxonomy\TermInterface $term */
return (!empty($tid) && $term = $this->load($tid)) ? $this->getAncestors($term) : [];
}
/**
* Returns all ancestors of this term.
*
* @return \Drupal\taxonomy\TermInterface[]
* A list of ancestor taxonomy term entities keyed by term ID.
*
* @internal
* @todo Refactor away when TreeInterface is introduced.
*/
protected function getAncestors(TermInterface $term) {
if (!isset($this->ancestors[$term->id()])) {
$this->ancestors[$term->id()] = [$term->id() => $term];
$search[] = $term->id();
while ($tid = array_shift($search)) {
foreach ($this->getParents(static::load($tid)) as $id => $parent) {
if ($parent && !isset($this->ancestors[$term->id()][$id])) {
$this->ancestors[$term->id()][$id] = $parent;
$search[] = $id;
}
}
}
}
return $this->ancestors[$term->id()];
}
/**
* {@inheritdoc}
*/
public function loadChildren($tid, $vid = NULL) {
/** @var \Drupal\taxonomy\TermInterface $term */
return (!empty($tid) && $term = $this->load($tid)) ? $this->getChildren($term) : [];
}
/**
* Returns all children terms of this term.
*
* @return \Drupal\taxonomy\TermInterface[]
* A list of children taxonomy term entities keyed by term ID.
*
* @internal
* @todo Refactor away when TreeInterface is introduced.
*/
public function getChildren(TermInterface $term) {
$query = \Drupal::entityQuery('taxonomy_term')
->accessCheck(TRUE)
->condition('parent', $term->id());
return static::loadMultiple($query->execute());
}
/**
* {@inheritdoc}
*/
public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {
$cache_key = implode(':', func_get_args());
if (!isset($this->trees[$cache_key])) {
// We cache trees, so it's not CPU-intensive to call on a term and its
// children, too.
if (!isset($this->treeChildren[$vid])) {
$this->treeChildren[$vid] = [];
$this->treeParents[$vid] = [];
$this->treeTerms[$vid] = [];
$query = $this->database->select($this->getDataTable(), 't');
$query->join('taxonomy_term__parent', 'p', '[t].[tid] = [p].[entity_id]');
$query->addExpression('[parent_target_id]', 'parent');
$result = $query
->addTag('taxonomy_term_access')
->fields('t')
->condition('t.vid', $vid)
->condition('t.default_langcode', 1)
->orderBy('t.weight')
->orderBy('t.name')
->execute();
foreach ($result as $term) {
$this->treeChildren[$vid][$term->parent][] = $term->tid;
$this->treeParents[$vid][$term->tid][] = $term->parent;
$this->treeTerms[$vid][$term->tid] = $term;
}
}
// Load full entities, if necessary. The entity controller statically
// caches the results.
$term_entities = [];
if ($load_entities) {
$term_entities = $this->loadMultiple(array_keys($this->treeTerms[$vid]));
}
$max_depth = (!isset($max_depth)) ? count($this->treeChildren[$vid]) : $max_depth;
$tree = [];
// Keeps track of the parents we have to process, the last entry is used
// for the next processing step.
$process_parents = [];
$process_parents[] = $parent;
// Loops over the parent terms and adds its children to the tree array.
// Uses a loop instead of a recursion, because it's more efficient.
while (count($process_parents)) {
$parent = array_pop($process_parents);
// The number of parents determines the current depth.
$depth = count($process_parents);
if ($max_depth > $depth && !empty($this->treeChildren[$vid][$parent])) {
$has_children = FALSE;
$child = current($this->treeChildren[$vid][$parent]);
do {
if (empty($child)) {
break;
}
$term = $load_entities ? $term_entities[$child] : $this->treeTerms[$vid][$child];
if (isset($this->treeParents[$vid][$load_entities ? $term->id() : $term->tid])) {
// Clone the term so that the depth attribute remains correct
// in the event of multiple parents.
$term = clone $term;
}
$term->depth = $depth;
if (!$load_entities) {
unset($term->parent);
}
$tid = $load_entities ? $term->id() : $term->tid;
$term->parents = $this->treeParents[$vid][$tid];
$tree[] = $term;
if (!empty($this->treeChildren[$vid][$tid])) {
$has_children = TRUE;
// We have to continue with this parent later.
$process_parents[] = $parent;
// Use the current term as parent for the next iteration.
$process_parents[] = $tid;
// Reset pointers for child lists because we step in there more
// often with multi parents.
reset($this->treeChildren[$vid][$tid]);
// Move pointer so that we get the correct term the next time.
next($this->treeChildren[$vid][$parent]);
break;
}
} while ($child = next($this->treeChildren[$vid][$parent]));
if (!$has_children) {
// We processed all terms in this hierarchy-level, reset pointer
// so that this function works the next time it gets called.
reset($this->treeChildren[$vid][$parent]);
}
}
}
$this->trees[$cache_key] = $tree;
}
return $this->trees[$cache_key];
}
/**
* {@inheritdoc}
*/
public function nodeCount($vid) {
$query = $this->database->select('taxonomy_index', 'ti');
$query->addExpression('COUNT(DISTINCT [ti].[nid])');
$query->leftJoin($this->getBaseTable(), 'td', '[ti].[tid] = [td].[tid]');
$query->condition('td.vid', $vid);
$query->addTag('vocabulary_node_count');
return $query->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function resetWeights($vid) {
$this->database->update($this->getDataTable())
->fields(['weight' => 0])
->condition('vid', $vid)
->execute();
}
/**
* {@inheritdoc}
*/
public function getNodeTerms(array $nids, array $vids = [], $langcode = NULL) {
$query = $this->database->select($this->getDataTable(), 'td');
$query->innerJoin('taxonomy_index', 'tn', '[td].[tid] = [tn].[tid]');
$query->fields('td', ['tid']);
$query->addField('tn', 'nid', 'node_nid');
$query->orderby('td.weight');
$query->orderby('td.name');
$query->condition('tn.nid', $nids, 'IN');
$query->addTag('taxonomy_term_access');
if (!empty($vids)) {
$query->condition('td.vid', $vids, 'IN');
}
if (!empty($langcode)) {
$query->condition('td.langcode', $langcode);
}
$results = [];
$all_tids = [];
foreach ($query->execute() as $term_record) {
$results[$term_record->node_nid][] = $term_record->tid;
$all_tids[] = $term_record->tid;
}
$all_terms = $this->loadMultiple($all_tids);
$terms = [];
foreach ($results as $nid => $tids) {
foreach ($tids as $tid) {
$terms[$nid][$tid] = $all_terms[$tid];
}
}
return $terms;
}
/**
* {@inheritdoc}
*/
public function getTermIdsWithPendingRevisions() {
$table_mapping = $this->getTableMapping();
$id_field = $table_mapping->getColumnNames($this->entityType->getKey('id'))['value'];
$revision_field = $table_mapping->getColumnNames($this->entityType->getKey('revision'))['value'];
$rta_field = $table_mapping->getColumnNames($this->entityType->getKey('revision_translation_affected'))['value'];
$langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value'];
$revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value'];
$query = $this->database->select($this->getRevisionDataTable(), 'tfr');
$query->fields('tfr', [$id_field]);
$query->addExpression("MAX([tfr].[$revision_field])", $revision_field);
$query->join($this->getRevisionTable(), 'tr', "[tfr].[$revision_field] = [tr].[$revision_field] AND [tr].[$revision_default_field] = 0");
$inner_select = $this->database->select($this->getRevisionDataTable(), 't');
$inner_select->condition("t.$rta_field", '1');
$inner_select->fields('t', [$id_field, $langcode_field]);
$inner_select->addExpression("MAX([t].[$revision_field])", $revision_field);
$inner_select
->groupBy("t.$id_field")
->groupBy("t.$langcode_field");
$query->join($inner_select, 'mr', "[tfr].[$revision_field] = [mr].[$revision_field] AND [tfr].[$langcode_field] = [mr].[$langcode_field]");
$query->groupBy("tfr.$id_field");
return $query->execute()->fetchAllKeyed(1, 0);
}
/**
* {@inheritdoc}
*/
public function getVocabularyHierarchyType($vid) {
// Return early if we already computed this value.
if (isset($this->vocabularyHierarchyType[$vid])) {
return $this->vocabularyHierarchyType[$vid];
}
$parent_field_storage = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId)['parent'];
$table_mapping = $this->getTableMapping();
$target_id_column = $table_mapping->getFieldColumnName($parent_field_storage, 'target_id');
$delta_column = $table_mapping->getFieldColumnName($parent_field_storage, TableMappingInterface::DELTA);
$query = $this->database->select($table_mapping->getFieldTableName('parent'), 'p');
$query->addExpression("MAX([$target_id_column])", 'max_parent_id');
$query->addExpression("MAX([$delta_column])", 'max_delta');
$query->condition('bundle', $vid);
$result = $query->execute()->fetchAll();
// If all the terms have the same parent, the parent can only be root (0).
if ((int) $result[0]->max_parent_id === 0) {
$this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_DISABLED;
}
// If no term has a delta higher than 0, no term has multiple parents.
elseif ((int) $result[0]->max_delta === 0) {
$this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_SINGLE;
}
else {
$this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_MULTIPLE;
}
return $this->vocabularyHierarchyType[$vid];
}
/**
* {@inheritdoc}
*/
public function __sleep() {
/** @var string[] $vars */
$vars = parent::__sleep();
// Do not serialize static cache.
unset($vars['ancestors'], $vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees'], $vars['vocabularyHierarchyType']);
return $vars;
}
/**
* {@inheritdoc}
*/
public function __wakeup() {
parent::__wakeup();
// Initialize static caches.
$this->ancestors = [];
$this->treeChildren = [];
$this->treeParents = [];
$this->treeTerms = [];
$this->trees = [];
$this->vocabularyHierarchyType = [];
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for taxonomy_term entity storage classes.
*/
interface TermStorageInterface extends ContentEntityStorageInterface {
/**
* Removed reference to terms from term_hierarchy.
*
* @param array $tids
* Array of terms that need to be removed from hierarchy.
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Parent
* references are automatically cleared when deleting a taxonomy term.
*
* @see https://www.drupal.org/node/2936675
*/
public function deleteTermHierarchy($tids);
/**
* Updates terms hierarchy information with the hierarchy trail of it.
*
* @param \Drupal\Core\Entity\EntityInterface $term
* Term entity that needs to be added to term hierarchy information.
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Parent
* references are automatically updated when updating a taxonomy term.
*
* @see https://www.drupal.org/node/2936675
*/
public function updateTermHierarchy(EntityInterface $term);
/**
* Finds all parents of a given term ID.
*
* @param int $tid
* Term ID to retrieve parents for.
*
* @return \Drupal\taxonomy\TermInterface[]
* An array of term objects which are the parents of the term $tid.
*/
public function loadParents($tid);
/**
* Finds all ancestors of a given term ID.
*
* @param int $tid
* Term ID to retrieve ancestors for.
*
* @return \Drupal\taxonomy\TermInterface[]
* An array of term objects which are the ancestors of the term $tid.
*/
public function loadAllParents($tid);
/**
* Finds all children of a term ID.
*
* @param int $tid
* Term ID to retrieve children for.
* @param string $vid
* An optional vocabulary ID to restrict the child search.
*
* @return \Drupal\taxonomy\TermInterface[]
* An array of term objects that are the children of the term $tid.
*/
public function loadChildren($tid, $vid = NULL);
/**
* Finds all terms in a given vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve terms for.
* @param int $parent
* The term ID under which to generate the tree. If 0, generate the tree
* for the entire vocabulary.
* @param int $max_depth
* The number of levels of the tree to return. Leave NULL to return all
* levels.
* @param bool $load_entities
* If TRUE, a full entity load will occur on the term objects. Otherwise
* they are partial objects queried directly from the {taxonomy_term_data}
* table to save execution time and memory consumption when listing large
* numbers of terms. Defaults to FALSE.
*
* @return object[]|\Drupal\taxonomy\TermInterface[]
* A numerically indexed array of term objects that are the children of the
* vocabulary $vid.
*/
public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE);
/**
* Count the number of nodes in a given vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve terms for.
*
* @return int
* A count of the nodes in a given vocabulary ID.
*/
public function nodeCount($vid);
/**
* Reset the weights for a given vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve terms for.
*/
public function resetWeights($vid);
/**
* Returns all terms used to tag some given nodes.
*
* @param array $nids
* Node IDs to retrieve terms for.
* @param array $vids
* (optional) an array of vocabulary IDs to restrict the term search.
* Defaults to empty array.
* @param string $langcode
* (optional) A language code to restrict the term search. Defaults to NULL.
*
* @return array
* An array of nids and the term entities they were tagged with.
*/
public function getNodeTerms(array $nids, array $vids = [], $langcode = NULL);
/**
* Returns the hierarchy type for a specific vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve the hierarchy type for.
*
* @return int
* The vocabulary hierarchy.
* Possible values:
* - VocabularyInterface::HIERARCHY_DISABLED: No parents.
* - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*/
public function getVocabularyHierarchyType($vid);
/**
* Gets a list of term IDs with pending revisions.
*
* @return int[]
* An array of term IDs which have pending revisions, keyed by their
* revision IDs.
*
* @internal
*/
public function getTermIdsWithPendingRevisions();
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the term schema handler.
*/
class TermStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
if ($data_table = $this->storage->getDataTable()) {
$schema[$data_table]['indexes'] += [
'taxonomy_term__tree' => ['vid', 'weight', 'name'],
'taxonomy_term__vid_name' => ['vid', 'name'],
];
}
$schema['taxonomy_index'] = [
'description' => 'Maintains denormalized information about node/term relationships.',
'fields' => [
'nid' => [
'description' => 'The {node}.nid this record tracks.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'tid' => [
'description' => 'The term ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'status' => [
'description' => 'Boolean indicating whether the node is published (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
],
'sticky' => [
'description' => 'Boolean indicating whether the node is sticky.',
'type' => 'int',
'not null' => FALSE,
'default' => 0,
'size' => 'tiny',
],
'created' => [
'description' => 'The Unix timestamp when the node was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
],
'primary key' => ['nid', 'tid'],
'indexes' => [
'term_node' => ['tid', 'status', 'sticky', 'created'],
],
'foreign keys' => [
'tracked_node' => [
'table' => 'node',
'columns' => ['nid' => 'nid'],
],
'term' => [
'table' => 'taxonomy_term_data',
'columns' => ['tid' => 'tid'],
],
],
];
return $schema;
}
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == 'taxonomy_term_field_data') {
// Remove unneeded indexes.
unset($schema['indexes']['taxonomy_term_field__vid__target_id']);
unset($schema['indexes']['taxonomy_term_field__description__format']);
switch ($field_name) {
case 'weight':
// Improves the performance of the taxonomy_term__tree index defined
// in getEntitySchema().
$schema['fields'][$field_name]['not null'] = TRUE;
break;
case 'name':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
}
}
return $schema;
}
/**
* {@inheritdoc}
*/
protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ?ContentEntityTypeInterface $entity_type = NULL) {
$dedicated_table_schema = parent::getDedicatedTableSchema($storage_definition, $entity_type);
// Add an index on 'bundle', 'delta' and 'parent_target_id' columns to
// increase the performance of the query from
// \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType().
if ($storage_definition->getName() === 'parent') {
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $this->storage->getTableMapping();
$dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
unset($dedicated_table_schema[$dedicated_table_name]['indexes']['bundle']);
$dedicated_table_schema[$dedicated_table_name]['indexes']['bundle_delta_target_id'] = [
'bundle',
'delta',
$table_mapping->getFieldColumnName($storage_definition, 'target_id'),
];
}
return $dedicated_table_schema;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\content_translation\ContentTranslationHandler;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines the translation handler for terms.
*/
class TermTranslationHandler extends ContentTranslationHandler {
/**
* {@inheritdoc}
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
parent::entityFormAlter($form, $form_state, $entity);
$form['content_translation']['status']['#access'] = !isset($form['content_translation']);
$form['actions']['submit']['#submit'][] = [$this, 'entityFormSave'];
}
/**
* Form submission handler for TermTranslationHandler::entityFormAlter().
*
* This handles the save action.
*
* @see \Drupal\Core\Entity\EntityForm::build()
*/
public function entityFormSave(array $form, FormStateInterface $form_state) {
if ($this->getSourceLangcode($form_state)) {
$entity = $form_state->getFormObject()->getEntity();
// We need a redirect here, otherwise we would get an access denied page,
// since the current URL would be preserved and we would try to add a
// translation for a language that already has a translation.
$form_state->setRedirectUrl($entity->toUrl('edit-form'));
}
}
/**
* {@inheritdoc}
*/
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
if ($form_state->hasValue('content_translation')) {
$translation = &$form_state->getValue('content_translation');
$translation['status'] = $entity->isPublished();
}
parent::entityFormEntityBuild($entity_type, $entity, $form, $form_state);
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Drupal\taxonomy;
use Drupal\views\EntityViewsData;
/**
* Provides the views data for the taxonomy entity type.
*/
class TermViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['taxonomy_term_field_data']['table']['base']['help'] = $this->t('Taxonomy terms are attached to nodes.');
$data['taxonomy_term_field_data']['table']['base']['access query tag'] = 'taxonomy_term_access';
$data['taxonomy_term_field_data']['table']['wizard_id'] = 'taxonomy_term';
$data['taxonomy_term_field_data']['table']['join'] = [
// This is provided for the many_to_one argument.
'taxonomy_index' => [
'field' => 'tid',
'left_field' => 'tid',
],
];
$data['taxonomy_term_field_data']['tid']['help'] = $this->t('The tid of a taxonomy term.');
$data['taxonomy_term_field_data']['tid']['argument']['id'] = 'taxonomy';
$data['taxonomy_term_field_data']['tid']['argument']['name field'] = 'name';
$data['taxonomy_term_field_data']['tid']['argument']['zero is null'] = TRUE;
$data['taxonomy_term_field_data']['tid']['filter']['id'] = 'taxonomy_index_tid';
$data['taxonomy_term_field_data']['tid']['filter']['title'] = $this->t('Term');
$data['taxonomy_term_field_data']['tid']['filter']['help'] = $this->t('Taxonomy term chosen from autocomplete or select widget.');
$data['taxonomy_term_field_data']['tid']['filter']['hierarchy table'] = 'taxonomy_term__parent';
$data['taxonomy_term_field_data']['tid']['filter']['numeric'] = TRUE;
$data['taxonomy_term_field_data']['tid_raw'] = [
'title' => $this->t('Term ID'),
'help' => $this->t('The tid of a taxonomy term.'),
'real field' => 'tid',
'filter' => [
'id' => 'numeric',
'allow empty' => TRUE,
],
];
$data['taxonomy_term_field_data']['tid_representative'] = [
'relationship' => [
'title' => $this->t('Representative node'),
'label' => $this->t('Representative node'),
'help' => $this->t('Obtains a single representative node for each term, according to a chosen sort criterion.'),
'id' => 'groupwise_max',
'relationship field' => 'tid',
'outer field' => 'taxonomy_term_field_data.tid',
'argument table' => 'taxonomy_term_field_data',
'argument field' => 'tid',
'base' => 'node_field_data',
'field' => 'nid',
'relationship' => 'node_field_data:term_node_tid',
],
];
$data['taxonomy_term_field_data']['vid']['help'] = $this->t('Filter the results of "Taxonomy: Term" to a particular vocabulary.');
$data['taxonomy_term_field_data']['vid']['field']['help'] = t('The vocabulary name.');
$data['taxonomy_term_field_data']['vid']['argument']['id'] = 'vocabulary_vid';
$data['taxonomy_term_field_data']['vid']['sort']['title'] = t('Vocabulary ID');
$data['taxonomy_term_field_data']['vid']['sort']['help'] = t('The raw vocabulary ID.');
$data['taxonomy_term_field_data']['name']['field']['id'] = 'term_name';
$data['taxonomy_term_field_data']['name']['argument']['many to one'] = TRUE;
$data['taxonomy_term_field_data']['name']['argument']['empty field name'] = $this->t('Uncategorized');
$data['taxonomy_term_field_data']['description__value']['field']['click sortable'] = FALSE;
$data['taxonomy_term_field_data']['changed']['title'] = $this->t('Updated date');
$data['taxonomy_term_field_data']['changed']['help'] = $this->t('The date the term was last updated.');
$data['taxonomy_term_field_data']['changed_fulldate'] = [
'title' => $this->t('Updated date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'changed',
'id' => 'date_fulldate',
],
];
$data['taxonomy_term_field_data']['changed_year_month'] = [
'title' => $this->t('Updated year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year_month',
],
];
$data['taxonomy_term_field_data']['changed_year'] = [
'title' => $this->t('Updated year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year',
],
];
$data['taxonomy_term_field_data']['changed_month'] = [
'title' => $this->t('Updated month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'changed',
'id' => 'date_month',
],
];
$data['taxonomy_term_field_data']['changed_day'] = [
'title' => $this->t('Updated day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'changed',
'id' => 'date_day',
],
];
$data['taxonomy_term_field_data']['changed_week'] = [
'title' => $this->t('Updated week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'changed',
'id' => 'date_week',
],
];
$data['taxonomy_index']['table']['group'] = $this->t('Taxonomy term');
$data['taxonomy_index']['table']['join'] = [
'taxonomy_term_field_data' => [
// links directly to taxonomy_term_field_data via tid
'left_field' => 'tid',
'field' => 'tid',
],
'node_field_data' => [
// links directly to node via nid
'left_field' => 'nid',
'field' => 'nid',
],
'taxonomy_term__parent' => [
'left_field' => 'entity_id',
'field' => 'tid',
],
];
$data['taxonomy_index']['nid'] = [
'title' => $this->t('Content with term'),
'help' => $this->t('Relate all content tagged with a term.'),
'relationship' => [
'id' => 'standard',
'base' => 'node_field_data',
'base field' => 'nid',
'label' => $this->t('node'),
'skip base' => 'node_field_data',
],
];
// @todo This stuff needs to move to a node field since really it's all
// about nodes.
$data['taxonomy_index']['tid'] = [
'group' => $this->t('Content'),
'title' => $this->t('Has taxonomy term ID'),
'help' => $this->t('Display content if it has the selected taxonomy terms.'),
'argument' => [
'id' => 'taxonomy_index_tid',
'name table' => 'taxonomy_term_field_data',
'name field' => 'name',
'empty field name' => $this->t('Uncategorized'),
'numeric' => TRUE,
'skip base' => 'taxonomy_term_field_data',
],
'filter' => [
'title' => $this->t('Has taxonomy term'),
'id' => 'taxonomy_index_tid',
'hierarchy table' => 'taxonomy_term__parent',
'numeric' => TRUE,
'skip base' => 'taxonomy_term_field_data',
'allow empty' => TRUE,
],
];
$data['taxonomy_index']['status'] = [
'title' => $this->t('Publish status'),
'help' => $this->t('Whether or not the content related to a term is published.'),
'filter' => [
'id' => 'boolean',
'label' => $this->t('Published status'),
'type' => 'yes-no',
],
];
$data['taxonomy_index']['sticky'] = [
'title' => $this->t('Sticky status'),
'help' => $this->t('Whether or not the content related to a term is sticky.'),
'filter' => [
'id' => 'boolean',
'label' => $this->t('Sticky status'),
'type' => 'yes-no',
],
'sort' => [
'id' => 'standard',
'help' => $this->t('Whether or not the content related to a term is sticky. To list sticky content first, set this to descending.'),
],
];
$data['taxonomy_index']['created'] = [
'title' => $this->t('Post date'),
'help' => $this->t('The date the content related to a term was posted.'),
'sort' => [
'id' => 'date',
],
'filter' => [
'id' => 'date',
],
];
// Link to self through left.parent = right.tid (going down in depth).
$data['taxonomy_term__parent']['table']['join']['taxonomy_term__parent'] = [
'left_field' => 'entity_id',
'field' => 'parent_target_id',
];
$data['taxonomy_term__parent']['parent_target_id']['help'] = $this->t('The parent term of the term. This can produce duplicate entries if you are using a vocabulary that allows multiple parents.');
$data['taxonomy_term__parent']['parent_target_id']['relationship']['label'] = $this->t('Parent');
$data['taxonomy_term__parent']['parent_target_id']['argument']['id'] = 'taxonomy';
return $data;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the taxonomy vocabulary entity type.
*
* @see \Drupal\taxonomy\Entity\Vocabulary
*/
class VocabularyAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected $viewLabelOperation = TRUE;
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'view label':
return AccessResult::allowedIfHasPermissions($account, [
'view vocabulary labels',
'access taxonomy overview',
'administer taxonomy',
], 'OR');
case 'access taxonomy overview':
case 'view':
return AccessResult::allowedIfHasPermissions($account, ['access taxonomy overview', 'administer taxonomy'], 'OR');
case 'reset all weights':
return AccessResult::allowedIfHasPermissions($account, [
'administer taxonomy',
'edit terms in ' . $entity->id(),
], 'OR');
default:
return parent::checkAccess($entity, $operation, $account);
}
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Entity\ContentLanguageSettings;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form for vocabulary edit forms.
*
* @internal
*/
class VocabularyForm extends BundleEntityFormBase {
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a new vocabulary form.
*
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(VocabularyStorageInterface $vocabulary_storage) {
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\taxonomy\VocabularyInterface $entity */
$entity = parent::buildEntity($form, $form_state);
// The description cannot be an empty string.
if (trim($form_state->getValue('description')) === '') {
$entity->set('description', NULL);
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$vocabulary = $this->entity;
if ($vocabulary->isNew()) {
$form['#title'] = $this->t('Add vocabulary');
}
else {
$form['#title'] = $this->t('Edit vocabulary');
}
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#default_value' => $vocabulary->label(),
'#maxlength' => 255,
'#required' => TRUE,
];
$form['vid'] = [
'#type' => 'machine_name',
'#default_value' => $vocabulary->id(),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#machine_name' => [
'exists' => [$this, 'exists'],
'source' => ['name'],
],
];
$form['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Description'),
'#default_value' => $vocabulary->getDescription(),
];
$form['revision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Create new revision'),
'#default_value' => $vocabulary->shouldCreateNewRevision(),
'#description' => $this->t('Create a new revision by default for this vocabulary.'),
];
// $form['langcode'] is not wrapped in an
// if ($this->moduleHandler->moduleExists('language')) check because the
// language_select form element works also without the language module being
// installed. https://www.drupal.org/node/1749954 documents the new element.
$form['langcode'] = [
'#type' => 'language_select',
'#title' => $this->t('Vocabulary language'),
'#languages' => LanguageInterface::STATE_ALL,
'#default_value' => $vocabulary->language()->getId(),
];
if ($this->moduleHandler->moduleExists('language')) {
$form['default_terms_language'] = [
'#type' => 'details',
'#title' => $this->t('Term language'),
'#open' => TRUE,
];
$form['default_terms_language']['default_language'] = [
'#type' => 'language_configuration',
'#entity_information' => [
'entity_type' => 'taxonomy_term',
'bundle' => $vocabulary->id(),
],
'#default_value' => ContentLanguageSettings::loadByEntityTypeBundle('taxonomy_term', $vocabulary->id()),
];
}
// Set the hierarchy to "multiple parents" by default. This simplifies the
// vocabulary form and standardizes the term form.
$form['hierarchy'] = [
'#type' => 'value',
'#value' => '0',
];
$form = parent::form($form, $form_state);
return $this->protectBundleIdElement($form);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$vocabulary = $this->entity;
$vocabulary->setNewRevision($form_state->getValue(['revision']));
// Prevent leading and trailing spaces in vocabulary names.
$vocabulary->set('name', trim($vocabulary->label()));
$status = $vocabulary->save();
$edit_link = $this->entity->toLink($this->t('Edit'), 'edit-form')->toString();
switch ($status) {
case SAVED_NEW:
$this->messenger()->addStatus($this->t('Created new vocabulary %name.', ['%name' => $vocabulary->label()]));
$this->logger('taxonomy')->notice('Created new vocabulary %name.', ['%name' => $vocabulary->label(), 'link' => $edit_link]);
$form_state->setRedirectUrl($vocabulary->toUrl('overview-form'));
break;
case SAVED_UPDATED:
$this->messenger()->addStatus($this->t('Updated vocabulary %name.', ['%name' => $vocabulary->label()]));
$this->logger('taxonomy')->notice('Updated vocabulary %name.', ['%name' => $vocabulary->label(), 'link' => $edit_link]);
$form_state->setRedirectUrl($vocabulary->toUrl('collection'));
break;
}
$form_state->setValue('vid', $vocabulary->id());
$form_state->set('vid', $vocabulary->id());
}
/**
* Determines if the vocabulary already exists.
*
* @param string $vid
* The vocabulary ID.
*
* @return bool
* TRUE if the vocabulary exists, FALSE otherwise.
*/
public function exists($vid) {
$action = $this->vocabularyStorage->load($vid);
return !empty($action);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
/**
* Provides an interface defining a taxonomy vocabulary entity.
*/
interface VocabularyInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface {
/**
* Denotes that no term in the vocabulary has a parent.
*/
const HIERARCHY_DISABLED = 0;
/**
* Denotes that one or more terms in the vocabulary has a single parent.
*/
const HIERARCHY_SINGLE = 1;
/**
* Denotes that one or more terms in the vocabulary have multiple parents.
*/
const HIERARCHY_MULTIPLE = 2;
/**
* Returns the vocabulary description.
*
* @return string
* The vocabulary description.
*/
public function getDescription();
/**
* Sets whether a new revision should be created by default.
*
* @param bool $new_revision
* TRUE if a new revision should be created by default.
*/
public function setNewRevision($new_revision);
}

View File

@@ -0,0 +1,214 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of taxonomy vocabulary entities.
*
* @see \Drupal\taxonomy\Entity\Vocabulary
*/
class VocabularyListBuilder extends DraggableListBuilder {
/**
* {@inheritdoc}
*/
protected $entitiesKey = 'vocabularies';
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new VocabularyListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(
EntityTypeInterface $entity_type,
AccountInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
RendererInterface $renderer,
MessengerInterface $messenger,
) {
parent::__construct($entity_type, $entity_type_manager->getStorage($entity_type->id()));
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->renderer = $renderer;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_overview_vocabularies';
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
if (isset($operations['edit'])) {
$operations['edit']['title'] = t('Edit vocabulary');
}
if (isset($operations['delete'])) {
$operations['delete']['title'] = $this->t('Delete vocabulary');
}
if ($entity->access('access taxonomy overview')) {
$operations['list'] = [
'title' => t('List terms'),
'weight' => 0,
'url' => $entity->toUrl('overview-form'),
];
}
$taxonomy_term_access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term');
if ($taxonomy_term_access_control_handler->createAccess($entity->id())) {
$operations['add'] = [
'title' => t('Add terms'),
'weight' => 10,
'url' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $entity->id()]),
];
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = t('Vocabulary name');
$header['description'] = t('Description');
if ($this->currentUser->hasPermission('administer vocabularies') && !empty($this->weightKey)) {
$header['weight'] = t('Weight');
}
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['description']['data'] = ['#markup' => $entity->getDescription()];
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function render() {
$entities = $this->load();
// If there are not multiple vocabularies, disable dragging by unsetting the
// weight key.
if (count($entities) <= 1) {
unset($this->weightKey);
}
$build = parent::render();
// If the weight key was unset then the table is in the 'table' key,
// otherwise in vocabularies. The empty message is only needed if the table
// is possibly empty, so there is no need to support the vocabularies key
// here.
if (isset($build['table'])) {
$access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_vocabulary');
$create_access = $access_control_handler->createAccess(NULL, NULL, [], TRUE);
$this->renderer->addCacheableDependency($build['table'], $create_access);
if ($create_access->isAllowed()) {
$build['table']['#empty'] = t('No vocabularies available. <a href=":link">Add vocabulary</a>.', [
':link' => Url::fromRoute('entity.taxonomy_vocabulary.add_form')->toString(),
]);
}
else {
$build['table']['#empty'] = t('No vocabularies available.');
}
}
return $build;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['vocabularies']['#attributes'] = ['id' => 'taxonomy'];
$form['actions']['submit']['#value'] = t('Save');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->messenger->addStatus($this->t('The configuration options have been saved.'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
/**
* Defines a storage handler class for taxonomy vocabularies.
*/
class VocabularyStorage extends ConfigEntityStorage implements VocabularyStorageInterface {
/**
* {@inheritdoc}
*/
public function getToplevelTids($vids) {
$tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(TRUE)
->condition('vid', $vids, 'IN')
->condition('parent.target_id', 0)
->execute();
return array_values($tids);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
/**
* Defines an interface for vocabulary entity storage classes.
*/
interface VocabularyStorageInterface extends ConfigEntityStorageInterface {
/**
* Gets top-level term IDs of vocabularies.
*
* @param array $vids
* Array of vocabulary IDs.
*
* @return array
* Array of top-level term IDs.
*/
public function getToplevelTids($vids);
}

View File

@@ -0,0 +1,14 @@
name: Taxonomy
type: module
description: 'Enables the categorization of content.'
package: Core
# version: VERSION
dependencies:
- drupal:node
- drupal:text
configure: entity.taxonomy_vocabulary.collection
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,37 @@
<?php
/**
* @file
* Install, update and uninstall functions for the taxonomy module.
*/
use Drupal\Core\Entity\Form\RevisionDeleteForm;
use Drupal\Core\Entity\Form\RevisionRevertForm;
use Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Implements hook_update_last_removed().
*/
function taxonomy_update_last_removed() {
return 8702;
}
/**
* Update entity definition to handle revision routes.
*/
function taxonomy_update_10100(&$sandbox = NULL): TranslatableMarkup {
$entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
$definition = $entityDefinitionUpdateManager->getEntityType('taxonomy_term');
$routeProviders = $definition->get('route_provider');
$routeProviders['revision'] = RevisionHtmlRouteProvider::class;
$definition
->setFormClass('revision-delete', RevisionDeleteForm::class)
->setFormClass('revision-revert', RevisionRevertForm::class)
->set('route_provider', $routeProviders)
->setLinkTemplate('revision-delete-form', '/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term}/delete')
->setLinkTemplate('revision-revert-form', '/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term}/revert')
->setLinkTemplate('version-history', '/taxonomy/term/{taxonomy_term}/revisions');
$entityDefinitionUpdateManager->updateEntityType($definition);
return \t('Added revision routes to Taxonomy Term entity type.');
}

View File

@@ -0,0 +1,62 @@
/**
* @file
* Taxonomy behaviors.
*/
(function ($, Drupal) {
/**
* Reorder taxonomy terms.
*
* This behavior is dependent on the tableDrag behavior, since it uses the
* objects initialized in that behavior to update the row.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the drag behavior to an applicable table element.
*/
Drupal.behaviors.termDrag = {
attach(context, settings) {
const backStep = settings.taxonomy.backStep;
const forwardStep = settings.taxonomy.forwardStep;
// Get the taxonomy tableDrag object.
const tableDrag = Drupal.tableDrag.taxonomy;
const $table = $('#taxonomy');
const rows = $table.find('tr').length;
// When a row is swapped, keep previous and next page classes set.
tableDrag.row.prototype.onSwap = function (swappedRow) {
$table
.find('tr.taxonomy-term-preview')
.removeClass('taxonomy-term-preview');
$table
.find('tr.taxonomy-term-divider-top')
.removeClass('taxonomy-term-divider-top');
$table
.find('tr.taxonomy-term-divider-bottom')
.removeClass('taxonomy-term-divider-bottom');
const tableBody = $table[0].tBodies[0];
if (backStep) {
for (let n = 0; n < backStep; n++) {
$(tableBody.rows[n]).addClass('taxonomy-term-preview');
}
$(tableBody.rows[backStep - 1]).addClass('taxonomy-term-divider-top');
$(tableBody.rows[backStep]).addClass('taxonomy-term-divider-bottom');
}
if (forwardStep) {
for (let k = rows - forwardStep - 1; k < rows - 1; k++) {
$(tableBody.rows[k]).addClass('taxonomy-term-preview');
}
$(tableBody.rows[rows - forwardStep - 2]).addClass(
'taxonomy-term-divider-top',
);
$(tableBody.rows[rows - forwardStep - 1]).addClass(
'taxonomy-term-divider-bottom',
);
}
};
},
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,12 @@
drupal.taxonomy:
version: VERSION
js:
taxonomy.js: {}
css:
component:
css/taxonomy.theme.css: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/drupal.tabledrag

View File

@@ -0,0 +1,11 @@
entity.taxonomy_vocabulary.add_form:
route_name: entity.taxonomy_vocabulary.add_form
title: 'Add vocabulary'
appears_on:
- entity.taxonomy_vocabulary.collection
entity.taxonomy_term.add_form:
route_name: entity.taxonomy_term.add_form
title: 'Add term'
appears_on:
- entity.taxonomy_vocabulary.overview_form

View File

@@ -0,0 +1,17 @@
entity.taxonomy_term.edit_form:
title: Edit
group: taxonomy_term
route_name: entity.taxonomy_term.edit_form
weight: 10
entity.taxonomy_term.delete_form:
title: Delete
group: taxonomy_term
route_name: entity.taxonomy_term.delete_form
weight: 20
entity.taxonomy_vocabulary.delete_form:
title: Delete
group: taxonomy_vocabulary
route_name: entity.taxonomy_vocabulary.delete_form
weight: 20

View File

@@ -0,0 +1,5 @@
entity.taxonomy_vocabulary.collection:
title: Taxonomy
parent: system.admin_structure
description: 'Manage tagging, categorization, and classification of your content.'
route_name: entity.taxonomy_vocabulary.collection

View File

@@ -0,0 +1,24 @@
entity.taxonomy_term.canonical:
title: 'View'
route_name: entity.taxonomy_term.canonical
base_route: entity.taxonomy_term.canonical
entity.taxonomy_term.edit_form:
title: 'Edit'
route_name: entity.taxonomy_term.edit_form
base_route: entity.taxonomy_term.canonical
entity.taxonomy_term.delete_form:
title: 'Delete'
route_name: entity.taxonomy_term.delete_form
base_route: entity.taxonomy_term.canonical
entity.taxonomy_vocabulary.overview_form:
title: 'List'
route_name: entity.taxonomy_vocabulary.overview_form
base_route: entity.taxonomy_vocabulary.overview_form
entity.taxonomy_vocabulary.edit_form:
title: 'Edit'
route_name: entity.taxonomy_vocabulary.edit_form
base_route: entity.taxonomy_vocabulary.overview_form

View File

@@ -0,0 +1,309 @@
<?php
/**
* @file
* Enables the organization of content into categories.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\VocabularyInterface;
/**
* Implements hook_help().
*/
function taxonomy_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.taxonomy':
$field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Taxonomy module allows users who have permission to create and edit content to categorize (tag) content of that type. Users who have the <em>Administer vocabularies and terms</em> <a href=":permissions" title="Taxonomy module permissions">permission</a> can add <em>vocabularies</em> that contain a set of related <em>terms</em>. The terms in a vocabulary can either be pre-set by an administrator or built gradually as content is added and edited. Terms may be organized hierarchically if desired.', [':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'taxonomy'])->toString()]) . '</p>';
$output .= '<p>' . t('For more information, see the <a href=":taxonomy">online documentation for the Taxonomy module</a>.', [':taxonomy' => 'https://www.drupal.org/docs/8/core/modules/taxonomy']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Managing vocabularies') . '</dt>';
$output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission can add and edit vocabularies from the <a href=":taxonomy_admin">Taxonomy administration page</a>. Vocabularies can be deleted from their <em>Edit vocabulary</em> page. Users with the <em>Taxonomy term: Administer fields</em> permission may add additional fields for terms in that vocabulary using the <a href=":field_ui">Field UI module</a>.', [':taxonomy_admin' => Url::fromRoute('entity.taxonomy_vocabulary.collection')->toString(), ':field_ui' => $field_ui_url]) . '</dd>';
$output .= '<dt>' . t('Managing terms') . '</dt>';
$output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission or the <em>Edit terms</em> permission for a particular vocabulary can add, edit, and organize the terms in a vocabulary from a vocabulary\'s term listing page, which can be accessed by going to the <a href=":taxonomy_admin">Taxonomy administration page</a> and clicking <em>List terms</em> in the <em>Operations</em> column. Users must have the <em>Administer vocabularies and terms</em> permission or the <em>Delete terms</em> permission for a particular vocabulary to delete terms.', [':taxonomy_admin' => Url::fromRoute('entity.taxonomy_vocabulary.collection')->toString()]) . ' </dd>';
$output .= '<dt>' . t('Classifying entity content') . '</dt>';
$output .= '<dd>' . t('A user with the <em>Administer fields</em> permission for a certain entity type may add <em>Taxonomy term</em> reference fields to the entity type, which will allow entities to be classified using taxonomy terms. See the <a href=":entity_reference">Entity Reference help</a> for more information about reference fields. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them.', [':field_ui' => $field_ui_url, ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':entity_reference' => Url::fromRoute('help.page', ['name' => 'entity_reference'])->toString()]) . '</dd>';
$output .= '<dt>' . t('Adding new terms during content creation') . '</dt>';
$output .= '<dd>' . t("Allowing users to add new terms gradually builds a vocabulary as content is added and edited. Users can add new terms if either of the two <em>Autocomplete</em> widgets is chosen for the Taxonomy term reference field in the <em>Manage form display</em> page for the field. You will also need to enable the <em>Create referenced entities if they don't already exist</em> option, and restrict the field to one vocabulary.") . '</dd>';
$output .= '<dt>' . t('Configuring displays and form displays') . '</dt>';
$output .= '<dd>' . t('See the <a href=":entity_reference">Entity Reference help</a> page for the field widgets and formatters that can be configured for any reference field on the <em>Manage display</em> and <em>Manage form display</em> pages. Taxonomy additionally provides an <em>RSS category</em> formatter that displays nothing when the entity item is displayed as HTML, but displays an RSS category instead of a list when the entity item is displayed in an RSS feed.', [':entity_reference' => Url::fromRoute('help.page', ['name' => 'entity_reference'])->toString()]) . '</li>';
$output .= '</ul>';
$output .= '</dd>';
$output .= '</dl>';
return $output;
case 'entity.taxonomy_vocabulary.collection':
$output = '<p>' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
return $output;
}
}
/**
* Implements hook_theme().
*/
function taxonomy_theme() {
return [
'taxonomy_term' => [
'render element' => 'elements',
],
];
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function taxonomy_theme_suggestions_taxonomy_term(array $variables) {
$suggestions = [];
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $variables['elements']['#taxonomy_term'];
$suggestions[] = 'taxonomy_term__' . $term->bundle();
$suggestions[] = 'taxonomy_term__' . $term->id();
return $suggestions;
}
/**
* Implements hook_local_tasks_alter().
*
* @todo Evaluate removing as part of https://www.drupal.org/node/2358923.
*/
function taxonomy_local_tasks_alter(&$local_tasks) {
$local_task_key = 'config_translation.local_tasks:entity.taxonomy_vocabulary.config_translation_overview';
if (isset($local_tasks[$local_task_key])) {
// The config_translation module expects the base route to be
// entity.taxonomy_vocabulary.edit_form like it is for other configuration
// entities. Taxonomy uses the overview_form as the base route.
$local_tasks[$local_task_key]['base_route'] = 'entity.taxonomy_vocabulary.overview_form';
}
}
/**
* Prepares variables for taxonomy term templates.
*
* Default template: taxonomy-term.html.twig.
*
* By default this function performs special preprocessing to move the name
* base field out of the elements array into a separate variable. This
* preprocessing is skipped if:
* - a module makes the field's display configurable via the field UI by means
* of BaseFieldDefinition::setDisplayConfigurable()
* - AND the additional entity type property
* 'enable_base_field_custom_preprocess_skipping' has been set using
* hook_entity_type_build().
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the taxonomy term and any
* fields attached to the term. Properties used:
* - #taxonomy_term: A \Drupal\taxonomy\TermInterface object.
* - #view_mode: The current view mode for this taxonomy term, e.g.
* 'full' or 'teaser'.
* - attributes: HTML attributes for the containing element.
*/
function template_preprocess_taxonomy_term(&$variables) {
$variables['view_mode'] = $variables['elements']['#view_mode'];
$variables['term'] = $variables['elements']['#taxonomy_term'];
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $variables['term'];
$variables['url'] = !$term->isNew() ? $term->toUrl()->toString() : NULL;
// Make name field available separately. Skip this custom preprocessing if
// the field display is configurable and skipping has been enabled.
// @todo https://www.drupal.org/project/drupal/issues/3015623
// Eventually delete this code and matching template lines. Using
// $variables['content'] is more flexible and consistent.
$skip_custom_preprocessing = $term->getEntityType()->get('enable_base_field_custom_preprocess_skipping');
if (!$skip_custom_preprocessing || !$term->getFieldDefinition('name')->isDisplayConfigurable('view')) {
// We use name here because that is what appears in the UI.
$variables['name'] = $variables['elements']['name'];
unset($variables['elements']['name']);
}
$variables['page'] = $variables['view_mode'] == 'full' && taxonomy_term_is_page($term);
// Helpful $content variable for templates.
$variables['content'] = [];
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Implements hook_entity_operation().
*/
function taxonomy_entity_operation(EntityInterface $term) {
$operations = [];
if ($term instanceof Term && $term->access('create')) {
$operations['add-child'] = [
'title' => t('Add child'),
'weight' => 10,
'url' => Url::fromRoute(
'entity.taxonomy_term.add_form',
['taxonomy_vocabulary' => $term->bundle()],
['query' => ['parent' => $term->id()]],
),
];
}
return $operations;
}
/**
* Returns whether the current page is the page of the passed-in term.
*
* @param \Drupal\taxonomy\Entity\Term $term
* A taxonomy term entity.
*/
function taxonomy_term_is_page(Term $term) {
if (\Drupal::routeMatch()->getRouteName() == 'entity.taxonomy_term.canonical' && $page_term_id = \Drupal::routeMatch()->getRawParameter('taxonomy_term')) {
return $page_term_id == $term->id();
}
return FALSE;
}
/**
* @defgroup taxonomy_index Taxonomy indexing
* @{
* Functions to maintain taxonomy indexing.
*
* Taxonomy uses default field storage to store canonical relationships
* between terms and fieldable entities. However its most common use case
* requires listing all content associated with a term or group of terms
* sorted by creation date. To avoid slow queries due to joining across
* multiple node and field tables with various conditions and order by criteria,
* we maintain a denormalized table with all relationships between terms,
* published nodes and common sort criteria such as status, sticky and created.
* When using other field storage engines or alternative methods of
* denormalizing this data you should set the
* taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes in
* SQL.
*/
/**
* Implements hook_ENTITY_TYPE_insert() for node entities.
*/
function taxonomy_node_insert(EntityInterface $node) {
// Add taxonomy index entries for the node.
taxonomy_build_node_index($node);
}
/**
* Builds and inserts taxonomy index entries for a given node.
*
* The index lists all terms that are related to a given node entity, and is
* therefore maintained at the entity level.
*
* @param \Drupal\node\Entity\Node $node
* The node entity.
*/
function taxonomy_build_node_index($node) {
// We maintain a denormalized table of term/node relationships, containing
// only data for current, published nodes.
if (!\Drupal::config('taxonomy.settings')->get('maintain_index_table') || !(\Drupal::entityTypeManager()->getStorage('node') instanceof SqlContentEntityStorage)) {
return;
}
$status = $node->isPublished();
$sticky = (int) $node->isSticky();
// We only maintain the taxonomy index for published nodes.
if ($status && $node->isDefaultRevision()) {
// Collect a unique list of all the term IDs from all node fields.
$tid_all = [];
$entity_reference_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
foreach ($node->getFieldDefinitions() as $field) {
$field_name = $field->getName();
$class = $field->getItemDefinition()->getClass();
$is_entity_reference_class = ($class === $entity_reference_class) || is_subclass_of($class, $entity_reference_class);
if ($is_entity_reference_class && $field->getSetting('target_type') == 'taxonomy_term') {
foreach ($node->getTranslationLanguages() as $language) {
foreach ($node->getTranslation($language->getId())->$field_name as $item) {
if (!$item->isEmpty()) {
$tid_all[$item->target_id] = $item->target_id;
}
}
}
}
}
// Insert index entries for all the node's terms.
if (!empty($tid_all)) {
$connection = \Drupal::database();
foreach ($tid_all as $tid) {
$connection->merge('taxonomy_index')
->keys(['nid' => $node->id(), 'tid' => $tid, 'status' => $node->isPublished()])
->fields(['sticky' => $sticky, 'created' => $node->getCreatedTime()])
->execute();
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_update() for node entities.
*/
function taxonomy_node_update(EntityInterface $node) {
// If we're not dealing with the default revision of the node, do not make any
// change to the taxonomy index.
if (!$node->isDefaultRevision()) {
return;
}
taxonomy_delete_node_index($node);
taxonomy_build_node_index($node);
}
/**
* Implements hook_ENTITY_TYPE_predelete() for node entities.
*/
function taxonomy_node_predelete(EntityInterface $node) {
// Clean up the {taxonomy_index} table when nodes are deleted.
taxonomy_delete_node_index($node);
}
/**
* Deletes taxonomy index entries for a given node.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node entity.
*/
function taxonomy_delete_node_index(EntityInterface $node) {
if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
\Drupal::database()->delete('taxonomy_index')->condition('nid', $node->id())->execute();
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
*/
function taxonomy_taxonomy_term_delete(Term $term) {
if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
// Clean up the {taxonomy_index} table when terms are deleted.
\Drupal::database()->delete('taxonomy_index')->condition('tid', $term->id())->execute();
}
}
/**
* @} End of "defgroup taxonomy_index".
*/
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function taxonomy_taxonomy_vocabulary_presave(VocabularyInterface $vocabulary) {
// Vocabularies' `description` field must be stored as NULL at the config
// level if it is empty.
// @see taxonomy_post_update_set_vocabulary_description_to_null()
if (trim($vocabulary->getDescription()) === '') {
$vocabulary->set('description', NULL);
}
}

View File

@@ -0,0 +1,21 @@
administer taxonomy:
title: 'Administer vocabularies and terms'
access taxonomy overview:
title: 'Access the taxonomy vocabulary overview page'
description: 'Get an overview of all taxonomy vocabularies.'
revert all taxonomy revisions:
title: 'Revert all term revisions'
delete all taxonomy revisions:
title: 'Delete all term revisions'
view all taxonomy revisions:
title: 'View all term revisions'
view vocabulary labels:
title: 'View vocabulary labels'
permission_callbacks:
- Drupal\taxonomy\TaxonomyPermissions::permissions

View File

@@ -0,0 +1,45 @@
<?php
/**
* @file
* Post update functions for Taxonomy.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\taxonomy\VocabularyInterface;
/**
* Implements hook_removed_post_updates().
*/
function taxonomy_removed_post_updates() {
return [
'taxonomy_post_update_clear_views_data_cache' => '9.0.0',
'taxonomy_post_update_clear_entity_bundle_field_definitions_cache' => '9.0.0',
'taxonomy_post_update_handle_publishing_status_addition_in_views' => '9.0.0',
'taxonomy_post_update_remove_hierarchy_from_vocabularies' => '9.0.0',
'taxonomy_post_update_make_taxonomy_term_revisionable' => '9.0.0',
'taxonomy_post_update_configure_status_field_widget' => '9.0.0',
'taxonomy_post_update_clear_views_argument_validator_plugins_cache' => '10.0.0',
];
}
/**
* Re-save Taxonomy configurations with new_revision config.
*/
function taxonomy_post_update_set_new_revision(&$sandbox = NULL) {
\Drupal::classResolver(ConfigEntityUpdater::class)
->update($sandbox, 'taxonomy_vocabulary', function () {
return TRUE;
});
}
/**
* Converts empty `description` in vocabularies to NULL.
*/
function taxonomy_post_update_set_vocabulary_description_to_null(array &$sandbox): void {
\Drupal::classResolver(ConfigEntityUpdater::class)
->update($sandbox, 'taxonomy_vocabulary', function (VocabularyInterface $vocabulary): bool {
// @see taxonomy_taxonomy_vocabulary_presave()
return trim($vocabulary->getDescription()) === '';
});
}

View File

@@ -0,0 +1,39 @@
entity.taxonomy_term.add_form:
path: '/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/add'
defaults:
_controller: '\Drupal\taxonomy\Controller\TaxonomyController::addForm'
_title: 'Add term'
requirements:
_entity_create_access: 'taxonomy_term:{taxonomy_vocabulary}'
entity.taxonomy_term.edit_form:
path: '/taxonomy/term/{taxonomy_term}/edit'
defaults:
_entity_form: 'taxonomy_term.default'
_title: 'Edit term'
options:
_admin_route: TRUE
requirements:
_entity_access: 'taxonomy_term.update'
taxonomy_term: \d+
entity.taxonomy_term.delete_form:
path: '/taxonomy/term/{taxonomy_term}/delete'
defaults:
_entity_form: 'taxonomy_term.delete'
_title: 'Delete term'
options:
_admin_route: TRUE
requirements:
_entity_access: 'taxonomy_term.delete'
taxonomy_term: \d+
entity.taxonomy_term.canonical:
path: '/taxonomy/term/{taxonomy_term}'
defaults:
_entity_view: 'taxonomy_term.full'
_title: 'Taxonomy term'
_title_callback: '\Drupal\taxonomy\Controller\TaxonomyController::termTitle'
requirements:
_entity_access: 'taxonomy_term.view'
taxonomy_term: \d+

View File

@@ -0,0 +1,11 @@
services:
taxonomy_term.breadcrumb:
class: Drupal\taxonomy\TermBreadcrumbBuilder
arguments: ['@entity_type.manager', '@entity.repository']
tags:
- { name: breadcrumb_builder, priority: 1002 }
taxonomy_term.taxonomy_term_route_context:
class: Drupal\taxonomy\ContextProvider\TermRouteContext
arguments: ['@current_route_match']
tags:
- { name: 'context_provider' }

View File

@@ -0,0 +1,223 @@
<?php
/**
* @file
* Builds placeholder replacement tokens for taxonomy terms and vocabularies.
*/
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Implements hook_token_info().
*/
function taxonomy_token_info() {
$types['term'] = [
'name' => t("Taxonomy terms"),
'description' => t("Tokens related to taxonomy terms."),
'needs-data' => 'term',
];
$types['vocabulary'] = [
'name' => t("Vocabularies"),
'description' => t("Tokens related to taxonomy vocabularies."),
'needs-data' => 'vocabulary',
];
// Taxonomy term related variables.
$term['tid'] = [
'name' => t("Term ID"),
'description' => t("The unique ID of the taxonomy term."),
];
$term['name'] = [
'name' => t("Name"),
'description' => t("The name of the taxonomy term."),
];
$term['description'] = [
'name' => t("Description"),
'description' => t("The optional description of the taxonomy term."),
];
$term['node-count'] = [
'name' => t("Node count"),
'description' => t("The number of nodes tagged with the taxonomy term."),
];
$term['url'] = [
'name' => t("URL"),
'description' => t("The URL of the taxonomy term."),
];
// Taxonomy vocabulary related variables.
$vocabulary['vid'] = [
'name' => t("Vocabulary ID"),
'description' => t("The unique ID of the taxonomy vocabulary."),
];
$vocabulary['name'] = [
'name' => t("Name"),
'description' => t("The name of the taxonomy vocabulary."),
];
$vocabulary['description'] = [
'name' => t("Description"),
'description' => t("The optional description of the taxonomy vocabulary."),
];
$vocabulary['node-count'] = [
'name' => t("Node count"),
'description' => t("The number of nodes tagged with terms belonging to the taxonomy vocabulary."),
];
$vocabulary['term-count'] = [
'name' => t("Term count"),
'description' => t("The number of terms belonging to the taxonomy vocabulary."),
];
// Chained tokens for taxonomies
$term['vocabulary'] = [
'name' => t("Vocabulary"),
'description' => t("The vocabulary the taxonomy term belongs to."),
'type' => 'vocabulary',
];
$term['parent'] = [
'name' => t("Parent term"),
'description' => t("The parent term of the taxonomy term, if one exists."),
'type' => 'term',
];
$term['changed'] = [
'name' => t("Date changed"),
'description' => t("The date the taxonomy was most recently updated."),
'type' => 'date',
];
return [
'types' => $types,
'tokens' => [
'term' => $term,
'vocabulary' => $vocabulary,
],
];
}
/**
* Implements hook_tokens().
*/
function taxonomy_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$token_service = \Drupal::token();
if (isset($options['langcode'])) {
$url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
$langcode = $options['langcode'];
}
else {
$langcode = LanguageInterface::LANGCODE_DEFAULT;
}
$replacements = [];
if ($type == 'term' && !empty($data['term'])) {
$term = $data['term'];
$term = \Drupal::service('entity.repository')->getTranslationFromContext($term, $options['langcode'] ?? NULL);
foreach ($tokens as $name => $original) {
switch ($name) {
case 'tid':
$replacements[$original] = $term->id();
break;
case 'name':
$replacements[$original] = $term->label();
break;
case 'description':
// "processed" returns a \Drupal\Component\Render\MarkupInterface via
// check_markup().
$replacements[$original] = $term->description->processed;
break;
case 'url':
$replacements[$original] = $term->toUrl('canonical', ['absolute' => TRUE])->toString();
break;
case 'node-count':
$query = \Drupal::database()->select('taxonomy_index');
$query->condition('tid', $term->id());
$query->addTag('term_node_count');
$count = $query->countQuery()->execute()->fetchField();
$replacements[$original] = $count;
break;
case 'vocabulary':
$vocabulary = Vocabulary::load($term->bundle());
$bubbleable_metadata->addCacheableDependency($vocabulary);
$replacements[$original] = $vocabulary->label();
break;
case 'parent':
$taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
if ($parents = $taxonomy_storage->loadParents($term->id())) {
$parent = array_pop($parents);
$bubbleable_metadata->addCacheableDependency($parent);
$replacements[$original] = $parent->getName();
}
break;
case 'changed':
$date_format = DateFormat::load('medium');
$bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = \Drupal::service('date.formatter')->format($term->getChangedTime(), 'medium', '', NULL, $langcode);
break;
}
}
if ($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'vocabulary')) {
$vocabulary = Vocabulary::load($term->bundle());
$replacements += $token_service->generate('vocabulary', $vocabulary_tokens, ['vocabulary' => $vocabulary], $options, $bubbleable_metadata);
}
if (($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'parent'))) {
$taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
if ($parents = $taxonomy_storage->loadParents($term->id())) {
$parent = array_pop($parents);
$replacements += $token_service->generate('term', $vocabulary_tokens, ['term' => $parent], $options, $bubbleable_metadata);
}
}
if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
$replacements += $token_service->generate('date', $changed_tokens, ['date' => $term->getChangedTime()], $options, $bubbleable_metadata);
}
}
elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) {
$vocabulary = $data['vocabulary'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'vid':
$replacements[$original] = $vocabulary->id();
break;
case 'name':
$replacements[$original] = $vocabulary->label();
break;
case 'description':
$build = ['#markup' => $vocabulary->getDescription()];
// @todo Fix in https://www.drupal.org/node/2577827
$replacements[$original] = \Drupal::service('renderer')->renderInIsolation($build);
break;
case 'term-count':
$replacements[$original] = \Drupal::entityQuery('taxonomy_term')
->accessCheck(TRUE)
->condition('vid', $vocabulary->id())
->addTag('vocabulary_term_count')
->count()
->execute();
break;
case 'node-count':
$taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$replacements[$original] = $taxonomy_storage->nodeCount($vocabulary->id());
break;
}
}
}
return $replacements;
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* @file
* Provides views data for taxonomy.module.
*/
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_views_data_alter().
*/
function taxonomy_views_data_alter(&$data) {
$data['node_field_data']['term_node_tid'] = [
'title' => t('Taxonomy terms on node'),
'help' => t('Relate nodes to taxonomy terms, specifying which vocabulary or vocabularies to use. This relationship will cause duplicated records if there are multiple terms.'),
'relationship' => [
'id' => 'node_term_data',
'label' => t('term'),
'base' => 'taxonomy_term_field_data',
],
'field' => [
'title' => t('All taxonomy terms'),
'help' => t('Display all taxonomy terms associated with a node from specified vocabularies.'),
'id' => 'taxonomy_index_tid',
'no group by' => TRUE,
'click sortable' => FALSE,
],
];
$data['node_field_data']['term_node_tid_depth'] = [
'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth.'),
'real field' => 'nid',
'argument' => [
'title' => t('Has taxonomy term ID (with depth)'),
'id' => 'taxonomy_index_tid_depth',
'accept depth modifier' => TRUE,
],
'filter' => [
'title' => t('Has taxonomy terms (with depth)'),
'id' => 'taxonomy_index_tid_depth',
],
];
$data['node_field_data']['term_node_tid_depth_modifier'] = [
'title' => t('Has taxonomy term ID depth modifier'),
'help' => t('Allows the "depth" for Taxonomy: Term ID (with depth) to be modified via an additional contextual filter value.'),
'argument' => [
'id' => 'taxonomy_index_tid_depth_modifier',
],
];
}
/**
* Implements hook_field_views_data_alter().
*
* Views integration for entity reference fields which reference taxonomy terms.
* Adds a term relationship to the default field data.
*
* @see views_field_default_views_data()
*/
function taxonomy_field_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
if ($field_storage->getType() == 'entity_reference' && $field_storage->getSetting('target_type') == 'taxonomy_term') {
foreach ($data as $table_name => $table_data) {
foreach ($table_data as $field_name => $field_data) {
if (isset($field_data['filter']) && $field_name != 'delta') {
$data[$table_name][$field_name]['filter']['id'] = 'taxonomy_index_tid';
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
{#
/**
* @file
* Default theme implementation to display a taxonomy term.
*
* Available variables:
* - url: URL of the current term.
* - name: (optional) Name of the current term.
* - content: Items for the content of the term (fields and description).
* Use 'content' to print them all, or print a subset such as
* 'content.description'. Use the following code to exclude the
* printing of a given child element:
* @code
* {{ content|without('description') }}
* @endcode
* - attributes: HTML attributes for the wrapper.
* - page: Flag for the full page state.
* - term: The taxonomy term entity, including:
* - id: The ID of the taxonomy term.
* - bundle: Machine name of the current vocabulary.
* - view_mode: View mode, e.g. 'full', 'teaser', etc.
*
* @see template_preprocess_taxonomy_term()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ title_prefix }}
{% if name and not page %}
<h2><a href="{{ url }}">{{ name }}</a></h2>
{% endif %}
{{ title_suffix }}
{{ content }}
</div>

View File

@@ -0,0 +1,24 @@
<?php
/**
* @file
* Empties the description of the `tags` vocabulary.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$data = $connection->select('config')
->condition('name', 'taxonomy.vocabulary.tags')
->fields('config', ['data'])
->execute()
->fetchField();
$data = unserialize($data);
$data['description'] = "\n";
$connection->update('config')
->condition('name', 'taxonomy.vocabulary.tags')
->fields([
'data' => serialize($data),
])
->execute();

View File

@@ -0,0 +1,12 @@
name: 'Taxonomy CRUD tests'
type: module
description: 'Provides 3rd party settings for vocabulary.'
package: Testing
# version: VERSION
dependencies:
- drupal:taxonomy
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* Provides hook implementations for testing purposes.
*/
use Drupal\taxonomy\VocabularyInterface;
/**
* Implements hook_ENTITY_TYPE_presave() for taxonomy_vocabulary entities.
*/
function taxonomy_crud_taxonomy_vocabulary_presave(VocabularyInterface $vocabulary) {
$vocabulary->setThirdPartySetting('taxonomy_crud', 'foo', 'bar');
}

View File

@@ -0,0 +1,10 @@
name: 'Taxonomy term configurable display module tests'
type: module
description: 'Support module for taxonomy_term \Drupal\Core\Field\BaseFieldDefinition::setDisplayConfigurable() testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,26 @@
<?php
/**
* @file
* Tests configurable displays for taxonomy_term base fields.
*/
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Implements hook_entity_base_field_info_alter().
*/
function taxonomy_term_display_configurable_test_entity_base_field_info_alter(&$base_field_definitions, EntityTypeInterface $entity_type) {
if ($entity_type->id() === 'taxonomy_term') {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
$base_field_definitions['name']->setDisplayConfigurable('view', TRUE);
}
}
/**
* Implements hook_entity_type_build().
*/
function taxonomy_term_display_configurable_test_entity_type_build(array &$entity_types) {
// Allow skipping of extra preprocessing for configurable display.
$entity_types['taxonomy_term']->set('enable_base_field_custom_preprocess_skipping', TRUE);
}

View File

@@ -0,0 +1,32 @@
id: taxonomy_term_stub_test
label: Taxonomy term stub
migration_tags:
- Import and rollback test
source:
plugin: embedded_data
data_rows:
-
id: 1
vocab: 1
name: music
parent: 2
ids:
id:
type: integer
process:
tid: id
vid: vocab
name: name
weight: weight
parent:
plugin: migration_lookup
migration: taxonomy_term_stub_test
source: parent
destination:
plugin: entity:taxonomy_term
migration_dependencies:
required:
- vocabularies
provider:
- migrate_drupal
- taxonomy

View File

@@ -0,0 +1,13 @@
name: 'Taxonomy Migrate stub test'
type: module
description: 'Provides a migration plugin for stub testing.'
package: Testing
# version: VERSION
dependencies:
- drupal:taxonomy
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

Some files were not shown because too many files have changed in this diff Show More