Chris@0: moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#'; Chris@0: $output = ''; Chris@0: $output .= '
' . 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 Administer vocabularies and terms permission can add vocabularies that contain a set of related terms. The terms in a vocabulary can either be pre-set by an administrator or built gradually as content is added and edited. Terms may be organized hierarchically if desired.', [':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-taxonomy'])->toString()]) . '
'; Chris@0: $output .= '' . t('For more information, see the online documentation for the Taxonomy module.', [':taxonomy' => 'https://www.drupal.org/documentation/modules/taxonomy/']) . '
'; Chris@0: $output .= '' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '
'; Chris@0: return $output; Chris@18: } Chris@18: } Chris@0: Chris@18: /** Chris@18: * Implements hook_entity_type_alter(). Chris@18: */ Chris@18: function taxonomy_entity_type_alter(array &$entity_types) { Chris@18: // @todo Moderation is disabled for taxonomy terms until when we have an UI Chris@18: // for them. Chris@18: // @see https://www.drupal.org/project/drupal/issues/2899923 Chris@18: $entity_types['taxonomy_term']->setHandlerClass('moderation', ''); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Entity URI callback. Chris@0: */ Chris@0: function taxonomy_term_uri($term) { Chris@0: return new Url('entity.taxonomy_term.canonical', [ Chris@0: 'taxonomy_term' => $term->id(), Chris@0: ]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_page_attachments_alter(). Chris@0: */ Chris@0: function taxonomy_page_attachments_alter(array &$page) { Chris@0: $route_match = \Drupal::routeMatch(); Chris@0: if ($route_match->getRouteName() == 'entity.taxonomy_term.canonical' && ($term = $route_match->getParameter('taxonomy_term')) && $term instanceof TermInterface) { Chris@0: foreach ($term->uriRelationships() as $rel) { Chris@0: // Set the URI relationships, like canonical. Chris@0: $page['#attached']['html_head_link'][] = [ Chris@0: [ Chris@0: 'rel' => $rel, Chris@18: 'href' => $term->toUrl($rel)->toString(), Chris@0: ], Chris@0: TRUE, Chris@0: ]; Chris@0: Chris@0: // Set the term path as the canonical URL to prevent duplicate content. Chris@0: if ($rel == 'canonical') { Chris@0: // Set the non-aliased canonical path as a default shortlink. Chris@0: $page['#attached']['html_head_link'][] = [ Chris@0: [ Chris@0: 'rel' => 'shortlink', Chris@18: 'href' => $term->toUrl($rel, ['alias' => TRUE])->toString(), Chris@0: ], Chris@0: TRUE, Chris@0: ]; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_theme(). Chris@0: */ Chris@0: function taxonomy_theme() { Chris@0: return [ Chris@0: 'taxonomy_term' => [ Chris@0: 'render element' => 'elements', Chris@0: ], Chris@0: ]; Chris@0: } Chris@0: Chris@0: /** Chris@18: * Checks the hierarchy flag of a vocabulary. Chris@0: * Chris@18: * Checks the current parents of all terms in a vocabulary. If no term has Chris@18: * parent terms then the vocabulary will be given a hierarchy of Chris@0: * VocabularyInterface::HIERARCHY_DISABLED. If any term has a single parent then Chris@0: * the vocabulary will be given a hierarchy of Chris@0: * VocabularyInterface::HIERARCHY_SINGLE. If any term has multiple parents then Chris@0: * the vocabulary will be given a hierarchy of Chris@0: * VocabularyInterface::HIERARCHY_MULTIPLE. Chris@0: * Chris@0: * @param \Drupal\taxonomy\VocabularyInterface $vocabulary Chris@0: * A taxonomy vocabulary entity. Chris@0: * @param $changed_term Chris@0: * An array of the term structure that was updated. Chris@0: * Chris@18: * @return int Chris@0: * An integer that represents the level of the vocabulary's hierarchy. Chris@18: * Chris@18: * @deprecated in Drupal 8.7.x. Will be removed before Drupal 9.0.0. Use Chris@18: * \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType() instead. Chris@0: */ Chris@0: function taxonomy_check_vocabulary_hierarchy(VocabularyInterface $vocabulary, $changed_term) { Chris@18: @trigger_error('taxonomy_check_vocabulary_hierarchy() is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.x. Use \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType() instead.', E_USER_DEPRECATED); Chris@18: return \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getVocabularyHierarchyType($vocabulary->id()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Generates an array which displays a term detail page. Chris@0: * Chris@0: * @param \Drupal\taxonomy\Entity\Term $term Chris@0: * A taxonomy term object. Chris@0: * @param string $view_mode Chris@0: * View mode; e.g., 'full', 'teaser', etc. Chris@0: * @param string $langcode Chris@0: * (optional) A language code to use for rendering. Defaults to the global Chris@0: * content language of the current request. Chris@0: * Chris@0: * @return array Chris@16: * A $page element suitable for use by Chris@16: * \Drupal\Core\Render\RendererInterface::render(). Chris@0: */ Chris@0: function taxonomy_term_view(Term $term, $view_mode = 'full', $langcode = NULL) { Chris@0: return entity_view($term, $view_mode, $langcode); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Constructs a drupal_render() style array from an array of loaded terms. Chris@0: * Chris@0: * @param array $terms Chris@0: * An array of taxonomy terms as returned by Term::loadMultiple(). Chris@0: * @param string $view_mode Chris@0: * View mode; e.g., 'full', 'teaser', etc. Chris@0: * @param string $langcode Chris@0: * (optional) A language code to use for rendering. Defaults to the global Chris@0: * content language of the current request. Chris@0: * Chris@0: * @return array Chris@16: * An array in the format expected by Chris@16: * \Drupal\Core\Render\RendererInterface::render(). Chris@0: */ Chris@0: function taxonomy_term_view_multiple(array $terms, $view_mode = 'full', $langcode = NULL) { Chris@0: return entity_view_multiple($terms, $view_mode, $langcode); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_theme_suggestions_HOOK(). Chris@0: */ Chris@0: function taxonomy_theme_suggestions_taxonomy_term(array $variables) { Chris@0: $suggestions = []; Chris@0: Chris@0: /** @var \Drupal\taxonomy\TermInterface $term */ Chris@0: $term = $variables['elements']['#taxonomy_term']; Chris@0: Chris@0: $suggestions[] = 'taxonomy_term__' . $term->bundle(); Chris@0: $suggestions[] = 'taxonomy_term__' . $term->id(); Chris@0: Chris@0: return $suggestions; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for taxonomy term templates. Chris@0: * Chris@0: * Default template: taxonomy-term.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - elements: An associative array containing the taxonomy term and any Chris@0: * fields attached to the term. Properties used: Chris@0: * - #taxonomy_term: A \Drupal\taxonomy\TermInterface object. Chris@0: * - #view_mode: The current view mode for this taxonomy term, e.g. Chris@0: * 'full' or 'teaser'. Chris@0: * - attributes: HTML attributes for the containing element. Chris@0: */ Chris@0: function template_preprocess_taxonomy_term(&$variables) { Chris@0: $variables['view_mode'] = $variables['elements']['#view_mode']; Chris@0: $variables['term'] = $variables['elements']['#taxonomy_term']; Chris@0: /** @var \Drupal\taxonomy\TermInterface $term */ Chris@0: $term = $variables['term']; Chris@0: Chris@18: $variables['url'] = $term->toUrl()->toString(); Chris@0: // We use name here because that is what appears in the UI. Chris@0: $variables['name'] = $variables['elements']['name']; Chris@0: unset($variables['elements']['name']); Chris@0: $variables['page'] = $variables['view_mode'] == 'full' && taxonomy_term_is_page($term); Chris@0: Chris@0: // Helpful $content variable for templates. Chris@0: $variables['content'] = []; Chris@0: foreach (Element::children($variables['elements']) as $key) { Chris@0: $variables['content'][$key] = $variables['elements'][$key]; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether the current page is the page of the passed-in term. Chris@0: * Chris@0: * @param \Drupal\taxonomy\Entity\Term $term Chris@0: * A taxonomy term entity. Chris@0: */ Chris@0: function taxonomy_term_is_page(Term $term) { Chris@0: if (\Drupal::routeMatch()->getRouteName() == 'entity.taxonomy_term.canonical' && $page_term_id = \Drupal::routeMatch()->getRawParameter('taxonomy_term')) { Chris@0: return $page_term_id == $term->id(); Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Clear all static cache variables for terms. Chris@0: */ Chris@0: function taxonomy_terms_static_reset() { Chris@0: \Drupal::entityManager()->getStorage('taxonomy_term')->resetCache(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Clear all static cache variables for vocabularies. Chris@0: * Chris@0: * @param $ids Chris@0: * An array of ids to reset in the entity cache. Chris@0: */ Chris@0: function taxonomy_vocabulary_static_reset(array $ids = NULL) { Chris@0: \Drupal::entityManager()->getStorage('taxonomy_vocabulary')->resetCache($ids); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get names for all taxonomy vocabularies. Chris@0: * Chris@0: * @return array Chris@0: * A list of existing vocabulary IDs. Chris@0: */ Chris@0: function taxonomy_vocabulary_get_names() { Chris@0: $names = &drupal_static(__FUNCTION__); Chris@0: Chris@0: if (!isset($names)) { Chris@0: $names = []; Chris@0: $config_names = \Drupal::configFactory()->listAll('taxonomy.vocabulary.'); Chris@0: foreach ($config_names as $config_name) { Chris@0: $id = substr($config_name, strlen('taxonomy.vocabulary.')); Chris@0: $names[$id] = $id; Chris@0: } Chris@0: } Chris@0: Chris@0: return $names; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Try to map a string to an existing term, as for glossary use. Chris@0: * Chris@0: * Provides a case-insensitive and trimmed mapping, to maximize the Chris@0: * likelihood of a successful match. Chris@0: * Chris@0: * @param $name Chris@0: * Name of the term to search for. Chris@0: * @param $vocabulary Chris@0: * (optional) Vocabulary machine name to limit the search. Defaults to NULL. Chris@0: * Chris@0: * @return Chris@0: * An array of matching term objects. Chris@0: */ Chris@0: function taxonomy_term_load_multiple_by_name($name, $vocabulary = NULL) { Chris@0: $values = ['name' => trim($name)]; Chris@0: if (isset($vocabulary)) { Chris@0: $vocabularies = taxonomy_vocabulary_get_names(); Chris@0: if (isset($vocabularies[$vocabulary])) { Chris@0: $values['vid'] = $vocabulary; Chris@0: } Chris@0: else { Chris@0: // Return an empty array when filtering by a non-existing vocabulary. Chris@0: return []; Chris@0: } Chris@0: } Chris@0: return entity_load_multiple_by_properties('taxonomy_term', $values); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Load multiple taxonomy terms based on certain conditions. Chris@0: * Chris@0: * This function should be used whenever you need to load more than one term Chris@0: * from the database. Terms are loaded into memory and will not require Chris@0: * database access if loaded again during the same page request. Chris@0: * Chris@0: * @param array $tids Chris@0: * (optional) An array of entity IDs. If omitted, all entities are loaded. Chris@0: * Chris@0: * @return array Chris@0: * An array of taxonomy term entities, indexed by tid. When no results are Chris@0: * found, an empty array is returned. Chris@18: * Chris@18: * @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use Chris@18: * \Drupal\taxonomy\Entity\Term::loadMultiple(). Chris@18: * Chris@18: * @see https://www.drupal.org/node/2266845 Chris@0: */ Chris@0: function taxonomy_term_load_multiple(array $tids = NULL) { Chris@18: @trigger_error('taxonomy_term_load_multiple() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\taxonomy\Entity\Term::loadMultiple(). See https://www.drupal.org/node/2266845', E_USER_DEPRECATED); Chris@0: return Term::loadMultiple($tids); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Loads multiple taxonomy vocabularies based on certain conditions. Chris@0: * Chris@0: * This function should be used whenever you need to load more than one Chris@0: * vocabulary from the database. Terms are loaded into memory and will not Chris@0: * require database access if loaded again during the same page request. Chris@0: * Chris@0: * @param array $vids Chris@0: * (optional) An array of entity IDs. If omitted, all entities are loaded. Chris@0: * Chris@0: * @return array Chris@0: * An array of vocabulary objects, indexed by vid. Chris@18: * Chris@18: * @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use Chris@18: * \Drupal\taxonomy\Entity\Vocabulary::loadMultiple(). Chris@18: * Chris@18: * @see https://www.drupal.org/node/2266845 Chris@0: */ Chris@0: function taxonomy_vocabulary_load_multiple(array $vids = NULL) { Chris@18: @trigger_error('taxonomy_vocabulary_load_multiple() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\taxonomy\Entity\Vocabulary::loadMultiple(). See https://www.drupal.org/node/2266845', E_USER_DEPRECATED); Chris@0: return Vocabulary::loadMultiple($vids); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return the taxonomy vocabulary entity matching a vocabulary ID. Chris@0: * Chris@0: * @param int $vid Chris@0: * The vocabulary's ID. Chris@0: * Chris@0: * @return \Drupal\taxonomy\Entity\Vocabulary|null Chris@0: * The taxonomy vocabulary entity, if exists, NULL otherwise. Results are Chris@0: * statically cached. Chris@18: * Chris@18: * @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use Chris@18: * \Drupal\taxonomy\Entity\Vocabulary::load(). Chris@18: * Chris@18: * @see https://www.drupal.org/node/2266845 Chris@0: */ Chris@0: function taxonomy_vocabulary_load($vid) { Chris@18: @trigger_error('taxonomy_vocabulary_load() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\taxonomy\Entity\Vocabulary::load(). See https://www.drupal.org/node/2266845', E_USER_DEPRECATED); Chris@0: return Vocabulary::load($vid); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return the taxonomy term entity matching a term ID. Chris@0: * Chris@0: * @param $tid Chris@0: * A term's ID Chris@0: * Chris@0: * @return \Drupal\taxonomy\Entity\Term|null Chris@0: * A taxonomy term entity, or NULL if the term was not found. Results are Chris@0: * statically cached. Chris@18: * Chris@18: * @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use Chris@18: * Drupal\taxonomy\Entity\Term::load(). Chris@18: * Chris@18: * @see https://www.drupal.org/node/2266845 Chris@0: */ Chris@0: function taxonomy_term_load($tid) { Chris@18: @trigger_error('taxonomy_term_load() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\taxonomy\Entity\Term::load(). See https://www.drupal.org/node/2266845', E_USER_DEPRECATED); Chris@0: if (!is_numeric($tid)) { Chris@0: return NULL; Chris@0: } Chris@0: return Term::load($tid); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implodes a list of tags of a certain vocabulary into a string. Chris@0: * Chris@0: * @see \Drupal\Component\Utility\Tags::explode() Chris@0: */ Chris@0: function taxonomy_implode_tags($tags, $vid = NULL) { Chris@0: $typed_tags = []; Chris@0: foreach ($tags as $tag) { Chris@0: // Extract terms belonging to the vocabulary in question. Chris@0: if (!isset($vid) || $tag->bundle() == $vid) { Chris@0: // Make sure we have a completed loaded taxonomy term. Chris@0: if ($tag instanceof EntityInterface && $label = $tag->label()) { Chris@0: // Commas and quotes in tag names are special cases, so encode 'em. Chris@0: $typed_tags[] = Tags::encode($label); Chris@0: } Chris@0: } Chris@0: } Chris@0: return implode(', ', $typed_tags); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Title callback for term pages. Chris@0: * Chris@0: * @param \Drupal\taxonomy\Entity\Term $term Chris@0: * A taxonomy term entity. Chris@0: * Chris@0: * @return Chris@0: * The term name to be used as the page title. Chris@0: */ Chris@0: function taxonomy_term_title(Term $term) { Chris@0: return $term->getName(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @defgroup taxonomy_index Taxonomy indexing Chris@0: * @{ Chris@0: * Functions to maintain taxonomy indexing. Chris@0: * Chris@0: * Taxonomy uses default field storage to store canonical relationships Chris@0: * between terms and fieldable entities. However its most common use case Chris@0: * requires listing all content associated with a term or group of terms Chris@0: * sorted by creation date. To avoid slow queries due to joining across Chris@0: * multiple node and field tables with various conditions and order by criteria, Chris@0: * we maintain a denormalized table with all relationships between terms, Chris@0: * published nodes and common sort criteria such as status, sticky and created. Chris@0: * When using other field storage engines or alternative methods of Chris@0: * denormalizing this data you should set the Chris@0: * taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes in Chris@0: * SQL. Chris@0: */ Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_insert() for node entities. Chris@0: */ Chris@0: function taxonomy_node_insert(EntityInterface $node) { Chris@0: // Add taxonomy index entries for the node. Chris@0: taxonomy_build_node_index($node); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds and inserts taxonomy index entries for a given node. Chris@0: * Chris@0: * The index lists all terms that are related to a given node entity, and is Chris@0: * therefore maintained at the entity level. Chris@0: * Chris@0: * @param \Drupal\node\Entity\Node $node Chris@0: * The node entity. Chris@0: */ Chris@0: function taxonomy_build_node_index($node) { Chris@0: // We maintain a denormalized table of term/node relationships, containing Chris@0: // only data for current, published nodes. Chris@0: if (!\Drupal::config('taxonomy.settings')->get('maintain_index_table') || !(\Drupal::entityManager()->getStorage('node') instanceof SqlContentEntityStorage)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $status = $node->isPublished(); Chris@0: $sticky = (int) $node->isSticky(); Chris@0: // We only maintain the taxonomy index for published nodes. Chris@0: if ($status && $node->isDefaultRevision()) { Chris@0: // Collect a unique list of all the term IDs from all node fields. Chris@0: $tid_all = []; Chris@0: $entity_reference_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem'; Chris@0: foreach ($node->getFieldDefinitions() as $field) { Chris@0: $field_name = $field->getName(); Chris@0: $class = $field->getItemDefinition()->getClass(); Chris@0: $is_entity_reference_class = ($class === $entity_reference_class) || is_subclass_of($class, $entity_reference_class); Chris@0: if ($is_entity_reference_class && $field->getSetting('target_type') == 'taxonomy_term') { Chris@0: foreach ($node->getTranslationLanguages() as $language) { Chris@0: foreach ($node->getTranslation($language->getId())->$field_name as $item) { Chris@0: if (!$item->isEmpty()) { Chris@0: $tid_all[$item->target_id] = $item->target_id; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: // Insert index entries for all the node's terms. Chris@0: if (!empty($tid_all)) { Chris@18: $connection = \Drupal::database(); Chris@0: foreach ($tid_all as $tid) { Chris@18: $connection->merge('taxonomy_index') Chris@0: ->key(['nid' => $node->id(), 'tid' => $tid, 'status' => $node->isPublished()]) Chris@0: ->fields(['sticky' => $sticky, 'created' => $node->getCreatedTime()]) Chris@0: ->execute(); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_update() for node entities. Chris@0: */ Chris@0: function taxonomy_node_update(EntityInterface $node) { Chris@0: // If we're not dealing with the default revision of the node, do not make any Chris@0: // change to the taxonomy index. Chris@0: if (!$node->isDefaultRevision()) { Chris@0: return; Chris@0: } Chris@0: taxonomy_delete_node_index($node); Chris@0: taxonomy_build_node_index($node); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_predelete() for node entities. Chris@0: */ Chris@0: function taxonomy_node_predelete(EntityInterface $node) { Chris@0: // Clean up the {taxonomy_index} table when nodes are deleted. Chris@0: taxonomy_delete_node_index($node); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Deletes taxonomy index entries for a given node. Chris@0: * Chris@0: * @param \Drupal\Core\Entity\EntityInterface $node Chris@0: * The node entity. Chris@0: */ Chris@0: function taxonomy_delete_node_index(EntityInterface $node) { Chris@0: if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) { Chris@18: \Drupal::database()->delete('taxonomy_index')->condition('nid', $node->id())->execute(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities. Chris@0: */ Chris@0: function taxonomy_taxonomy_term_delete(Term $term) { Chris@0: if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) { Chris@0: // Clean up the {taxonomy_index} table when terms are deleted. Chris@18: \Drupal::database()->delete('taxonomy_index')->condition('tid', $term->id())->execute(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @} End of "defgroup taxonomy_index". Chris@0: */