diff --git a/core/modules/taxonomy/src/Hook/TaxonomyEntityHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyEntityHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..f1b031d4a265b4a444c551180c20a1cb2cd5e0d9 --- /dev/null +++ b/core/modules/taxonomy/src/Hook/TaxonomyEntityHooks.php @@ -0,0 +1,184 @@ +<?php + +namespace Drupal\taxonomy\Hook; + +use Drupal\taxonomy\Entity\Term; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\Sql\SqlContentEntityStorage; +use Drupal\node\NodeInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; + +/** + * Entity hook implementations for taxonomy. + */ +class TaxonomyEntityHooks { + + use StringTranslationTrait; + + public function __construct( + protected ConfigFactoryInterface $configFactory, + protected Connection $database, + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + /** + * Returns the maintain_index_table configuration value. + */ + protected function shouldMaintainIndexTable(): bool { + return (bool) $this->configFactory->get('taxonomy.settings')->get('maintain_index_table'); + } + + /** + * Builds and inserts taxonomy index entries for a given node. + * + * The index lists all terms that are related to a given node entity, and is + * therefore maintained at the entity level. + * + * @param \Drupal\node\NodeInterface $node + * The node entity. + */ + protected function buildNodeIndex(NodeInterface $node): void { + // We maintain a denormalized table of term/node relationships, containing + // only data for current, published nodes. + if (!$this->shouldMaintainIndexTable() || !($this->entityTypeManager->getStorage('node') instanceof SqlContentEntityStorage)) { + return; + } + + $status = $node->isPublished(); + $sticky = (int) $node->isSticky(); + // We only maintain the taxonomy index for published nodes. + if ($status && $node->isDefaultRevision()) { + // Collect a unique list of all the term IDs from all node fields. + $tid_all = []; + $entity_reference_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem'; + foreach ($node->getFieldDefinitions() as $field) { + $field_name = $field->getName(); + $class = $field->getItemDefinition()->getClass(); + $is_entity_reference_class = ($class === $entity_reference_class) || is_subclass_of($class, $entity_reference_class); + if ($is_entity_reference_class && $field->getSetting('target_type') == 'taxonomy_term') { + foreach ($node->getTranslationLanguages() as $language) { + foreach ($node->getTranslation($language->getId())->$field_name as $item) { + if (!$item->isEmpty()) { + $tid_all[$item->target_id] = $item->target_id; + } + } + } + } + } + // Insert index entries for all the node's terms. + if (!empty($tid_all)) { + foreach ($tid_all as $tid) { + $this->database->merge('taxonomy_index') + ->keys(['nid' => $node->id(), 'tid' => $tid, 'status' => $node->isPublished()]) + ->fields(['sticky' => $sticky, 'created' => $node->getCreatedTime()]) + ->execute(); + } + } + } + } + + /** + * Deletes taxonomy index entries for a given node. + * + * @param \Drupal\node\NodeInterface $node + * The node entity. + */ + protected function deleteNodeIndex(NodeInterface $node): void { + if ($this->shouldMaintainIndexTable()) { + $this->database->delete('taxonomy_index')->condition('nid', $node->id())->execute(); + } + } + + /** + * Implements hook_entity_operation(). + */ + #[Hook('entity_operation')] + public function entityOperation(EntityInterface $term): array { + $operations = []; + if ($term instanceof Term && $term->access('create')) { + $operations['add-child'] = [ + 'title' => $this->t('Add child'), + 'weight' => 10, + 'url' => Url::fromRoute('entity.taxonomy_term.add_form', [ + 'taxonomy_vocabulary' => $term->bundle(), + ], [ + 'query' => [ + 'parent' => $term->id(), + ], + ]), + ]; + } + return $operations; + } + + /** + * @defgroup taxonomy_index Taxonomy indexing + * @{ + * Functions to maintain taxonomy indexing. + * + * Taxonomy uses default field storage to store canonical relationships + * between terms and fieldable entities. However its most common use case + * requires listing all content associated with a term or group of terms + * sorted by creation date. To avoid slow queries due to joining across + * multiple node and field tables with various conditions and order by + * criteria, we maintain a denormalized table with all relationships between + * terms, published nodes and common sort criteria such as status, sticky and + * created. When using other field storage engines or alternative methods of + * denormalizing this data you should set the + * taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes + * in SQL. + */ + + /** + * Implements hook_ENTITY_TYPE_insert() for node entities. + */ + #[Hook('node_insert')] + public function nodeInsert(EntityInterface $node): void { + // Add taxonomy index entries for the node. + $this->buildNodeIndex($node); + } + + /** + * Implements hook_ENTITY_TYPE_update() for node entities. + */ + #[Hook('node_update')] + public function nodeUpdate(EntityInterface $node): void { + // If we're not dealing with the default revision of the node, do not make any + // change to the taxonomy index. + if (!$node->isDefaultRevision()) { + return; + } + $this->deleteNodeIndex($node); + $this->buildNodeIndex($node); + } + + /** + * Implements hook_ENTITY_TYPE_predelete() for node entities. + */ + #[Hook('node_predelete')] + public function nodePredelete(EntityInterface $node): void { + // Clean up the {taxonomy_index} table when nodes are deleted. + $this->deleteNodeIndex($node); + } + + /** + * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities. + */ + #[Hook('taxonomy_term_delete')] + public function taxonomyTermDelete(Term $term): void { + if ($this->shouldMaintainIndexTable()) { + // Clean up the {taxonomy_index} table when terms are deleted. + $this->database->delete('taxonomy_index')->condition('tid', $term->id())->execute(); + } + } + + // phpcs:ignore Drupal.Commenting.InlineComment.DocBlock + /** + * @} End of "defgroup taxonomy_index". + */ +} diff --git a/core/modules/taxonomy/src/Hook/TaxonomyHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyHelpHooks.php similarity index 57% rename from core/modules/taxonomy/src/Hook/TaxonomyHooks.php rename to core/modules/taxonomy/src/Hook/TaxonomyHelpHooks.php index 4cc0a6f6eb613ce32ebdeb0edf1dd40b14a25b49..2692e0d4e22d7584d2e3658e6585732286e49061 100644 --- a/core/modules/taxonomy/src/Hook/TaxonomyHooks.php +++ b/core/modules/taxonomy/src/Hook/TaxonomyHelpHooks.php @@ -2,28 +2,29 @@ namespace Drupal\taxonomy\Hook; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\taxonomy\Entity\Term; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** - * Hook implementations for taxonomy. + * Help hook implementation for the Taxonomy module. */ -class TaxonomyHooks { +class TaxonomyHelpHooks { use StringTranslationTrait; + public function __construct(protected ModuleHandlerInterface $moduleHandler) {} + /** * Implements hook_help(). */ #[Hook('help')] - public function help($route_name, RouteMatchInterface $route_match): ?string { + public function help(string $route_name, RouteMatchInterface $route_match): string { switch ($route_name) { case 'help.page.taxonomy': - $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#'; + $field_ui_url = $this->moduleHandler->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#'; $output = ''; $output .= '<h2>' . $this->t('About') . '</h2>'; $output .= '<p>' . $this->t('The Taxonomy module allows users who have permission to create and edit content to categorize (tag) content of that type. Users who have the <em>Administer vocabularies and terms</em> <a href=":permissions" title="Taxonomy module permissions">permission</a> can add <em>vocabularies</em> that contain a set of related <em>terms</em>. The terms in a vocabulary can either be pre-set by an administrator or built gradually as content is added and edited. Terms may be organized hierarchically if desired.', [ @@ -69,114 +70,10 @@ public function help($route_name, RouteMatchInterface $route_match): ?string { case 'entity.taxonomy_vocabulary.collection': $output = '<p>' . $this->t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>'; return $output; - } - return NULL; - } - - /** - * Implements hook_theme(). - */ - #[Hook('theme')] - public function theme() : array { - return ['taxonomy_term' => ['render element' => 'elements']]; - } - - /** - * Implements hook_local_tasks_alter(). - * - * @todo Evaluate removing as part of https://www.drupal.org/node/2358923. - */ - #[Hook('local_tasks_alter')] - public function localTasksAlter(&$local_tasks): void { - $local_task_key = 'config_translation.local_tasks:entity.taxonomy_vocabulary.config_translation_overview'; - if (isset($local_tasks[$local_task_key])) { - // The config_translation module expects the base route to be - // entity.taxonomy_vocabulary.edit_form like it is for other configuration - // entities. Taxonomy uses the overview_form as the base route. - $local_tasks[$local_task_key]['base_route'] = 'entity.taxonomy_vocabulary.overview_form'; - } - } - - /** - * Implements hook_entity_operation(). - */ - #[Hook('entity_operation')] - public function entityOperation(EntityInterface $term): array { - $operations = []; - if ($term instanceof Term && $term->access('create')) { - $operations['add-child'] = [ - 'title' => $this->t('Add child'), - 'weight' => 10, - 'url' => Url::fromRoute('entity.taxonomy_term.add_form', [ - 'taxonomy_vocabulary' => $term->bundle(), - ], [ - 'query' => [ - 'parent' => $term->id(), - ], - ]), - ]; - } - return $operations; - } - /** - * @defgroup taxonomy_index Taxonomy indexing - * @{ - * Functions to maintain taxonomy indexing. - * - * Taxonomy uses default field storage to store canonical relationships - * between terms and fieldable entities. However its most common use case - * requires listing all content associated with a term or group of terms - * sorted by creation date. To avoid slow queries due to joining across - * multiple node and field tables with various conditions and order by - * criteria, we maintain a denormalized table with all relationships between - * terms, published nodes and common sort criteria such as status, sticky and - * created. When using other field storage engines or alternative methods of - * denormalizing this data you should set the - * taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes - * in SQL. - */ - - /** - * Implements hook_ENTITY_TYPE_insert() for node entities. - */ - #[Hook('node_insert')] - public function nodeInsert(EntityInterface $node): void { - // Add taxonomy index entries for the node. - taxonomy_build_node_index($node); - } - - /** - * Implements hook_ENTITY_TYPE_update() for node entities. - */ - #[Hook('node_update')] - public function nodeUpdate(EntityInterface $node): void { - // If we're not dealing with the default revision of the node, do not make - // any change to the taxonomy index. - if (!$node->isDefaultRevision()) { - return; - } - taxonomy_delete_node_index($node); - taxonomy_build_node_index($node); - } - - /** - * Implements hook_ENTITY_TYPE_predelete() for node entities. - */ - #[Hook('node_predelete')] - public function nodePredelete(EntityInterface $node): void { - // Clean up the {taxonomy_index} table when nodes are deleted. - taxonomy_delete_node_index($node); - } - - /** - * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities. - */ - #[Hook('taxonomy_term_delete')] - public function taxonomyTermDelete(Term $term): void { - if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) { - // Clean up the {taxonomy_index} table when terms are deleted. - \Drupal::database()->delete('taxonomy_index')->condition('tid', $term->id())->execute(); + default: + $output = ''; + return $output; } } diff --git a/core/modules/taxonomy/src/Hook/TaxonomyMenuHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyMenuHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..65e949cde1510871e9de225222492dd957deb4bc --- /dev/null +++ b/core/modules/taxonomy/src/Hook/TaxonomyMenuHooks.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\taxonomy\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Menu hook implementations for taxonomy. + */ +class TaxonomyMenuHooks { + + /** + * Implements hook_local_tasks_alter(). + * + * @todo Evaluate removing as part of https://www.drupal.org/node/2358923. + */ + #[Hook('local_tasks_alter')] + public function localTasksAlter(array &$local_tasks): void { + $local_task_key = 'config_translation.local_tasks:entity.taxonomy_vocabulary.config_translation_overview'; + if (isset($local_tasks[$local_task_key])) { + // The config_translation module expects the base route to be + // entity.taxonomy_vocabulary.edit_form like it is for other configuration + // entities. Taxonomy uses the overview_form as the base route. + $local_tasks[$local_task_key]['base_route'] = 'entity.taxonomy_vocabulary.overview_form'; + } + } + +} diff --git a/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..e62ba91ee7915f6a1c202071abeab9d5f6afb409 --- /dev/null +++ b/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\taxonomy\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Implements theme hook implementations for the Taxonomy module. + */ +class TaxonomyThemeHooks { + + /** + * Implements hook_theme(). + */ + #[Hook('theme')] + public function taxonomyTheme(): array { + return [ + 'taxonomy_term' => [ + 'render element' => 'elements', + ], + ]; + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_taxonomy_term')] + public function themeSuggestionsTaxonomyTerm(array $variables): array { + $suggestions = []; + + /** @var \Drupal\taxonomy\TermInterface $term */ + $term = $variables['elements']['#taxonomy_term']; + + $suggestions[] = 'taxonomy_term__' . $term->bundle(); + $suggestions[] = 'taxonomy_term__' . $term->id(); + + return $suggestions; + } + +} diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 04c00e36b6c25dc3f18830edcedb8ad44be65d11..ecc5fa0c4dbee04805ff6db9d7b4c3569e33ef1f 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -9,21 +9,6 @@ use Drupal\Core\Render\Element; use Drupal\taxonomy\Entity\Term; -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function taxonomy_theme_suggestions_taxonomy_term(array $variables): array { - $suggestions = []; - - /** @var \Drupal\taxonomy\TermInterface $term */ - $term = $variables['elements']['#taxonomy_term']; - - $suggestions[] = 'taxonomy_term__' . $term->bundle(); - $suggestions[] = 'taxonomy_term__' . $term->id(); - - return $suggestions; -} - /** * Prepares variables for taxonomy term templates. * @@ -99,6 +84,7 @@ function taxonomy_term_is_page(Term $term) { * The node entity. */ function taxonomy_build_node_index($node): void { + @trigger_error('taxonomy_build_node_index() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. See https://www.drupal.org/node/3515362', E_USER_DEPRECATED); // We maintain a denormalized table of term/node relationships, containing // only data for current, published nodes. if (!\Drupal::config('taxonomy.settings')->get('maintain_index_table') || !(\Drupal::entityTypeManager()->getStorage('node') instanceof SqlContentEntityStorage)) { @@ -146,11 +132,8 @@ function taxonomy_build_node_index($node): void { * The node entity. */ function taxonomy_delete_node_index(EntityInterface $node): void { + @trigger_error('taxonomy_delete_node_index() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. See https://www.drupal.org/node/3515362', E_USER_DEPRECATED); if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) { \Drupal::database()->delete('taxonomy_index')->condition('nid', $node->id())->execute(); } } - -/** - * @} End of "defgroup taxonomy_index". - */