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