clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

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,151 @@
<?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',
],
];
// 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);
}