1<?php
2
3namespace Drupal\taxonomy;
4
5use Drupal\Core\Entity\EntityInterface;
6use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
7use Drupal\Core\Entity\Sql\TableMappingInterface;
8
9/**
10 * Defines a Controller class for taxonomy terms.
11 */
12class TermStorage extends SqlContentEntityStorage implements TermStorageInterface {
13
14  /**
15   * Array of term parents keyed by vocabulary ID and child term ID.
16   *
17   * @var array
18   */
19  protected $treeParents = [];
20
21  /**
22   * Array of term ancestors keyed by vocabulary ID and parent term ID.
23   *
24   * @var array
25   */
26  protected $treeChildren = [];
27
28  /**
29   * Array of terms in a tree keyed by vocabulary ID and term ID.
30   *
31   * @var array
32   */
33  protected $treeTerms = [];
34
35  /**
36   * Array of loaded trees keyed by a cache id matching tree arguments.
37   *
38   * @var array
39   */
40  protected $trees = [];
41
42  /**
43   * Array of all loaded term ancestry keyed by ancestor term ID, keyed by term
44   * ID.
45   *
46   * @var \Drupal\taxonomy\TermInterface[][]
47   */
48  protected $ancestors;
49
50  /**
51   * The type of hierarchy allowed within a vocabulary.
52   *
53   * Possible values:
54   * - VocabularyInterface::HIERARCHY_DISABLED: No parents.
55   * - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
56   * - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
57   *
58   * @var int[]
59   *   An array of one the possible values above, keyed by vocabulary ID.
60   */
61  protected $vocabularyHierarchyType;
62
63  /**
64   * {@inheritdoc}
65   *
66   * @param array $values
67   *   An array of values to set, keyed by property name. A value for the
68   *   vocabulary ID ('vid') is required.
69   */
70  public function create(array $values = []) {
71    // Save new terms with no parents by default.
72    if (empty($values['parent'])) {
73      $values['parent'] = [0];
74    }
75    $entity = parent::create($values);
76    return $entity;
77  }
78
79  /**
80   * {@inheritdoc}
81   */
82  public function resetCache(array $ids = NULL) {
83    drupal_static_reset('taxonomy_term_count_nodes');
84    $this->ancestors = [];
85    $this->treeChildren = [];
86    $this->treeParents = [];
87    $this->treeTerms = [];
88    $this->trees = [];
89    $this->vocabularyHierarchyType = [];
90    parent::resetCache($ids);
91  }
92
93  /**
94   * {@inheritdoc}
95   */
96  public function deleteTermHierarchy($tids) {}
97
98  /**
99   * {@inheritdoc}
100   */
101  public function updateTermHierarchy(EntityInterface $term) {}
102
103  /**
104   * {@inheritdoc}
105   */
106  public function loadParents($tid) {
107    $terms = [];
108    /** @var \Drupal\taxonomy\TermInterface $term */
109    if ($tid && $term = $this->load($tid)) {
110      foreach ($this->getParents($term) as $id => $parent) {
111        // This method currently doesn't return the <root> parent.
112        // @see https://www.drupal.org/node/2019905
113        if (!empty($id)) {
114          $terms[$id] = $parent;
115        }
116      }
117    }
118
119    return $terms;
120  }
121
122  /**
123   * Returns a list of parents of this term.
124   *
125   * @return \Drupal\taxonomy\TermInterface[]
126   *   The parent taxonomy term entities keyed by term ID. If this term has a
127   *   <root> parent, that item is keyed with 0 and will have NULL as value.
128   *
129   * @internal
130   * @todo Refactor away when TreeInterface is introduced.
131   */
132  protected function getParents(TermInterface $term) {
133    $parents = $ids = [];
134    // Cannot use $this->get('parent')->referencedEntities() here because that
135    // strips out the '0' reference.
136    foreach ($term->get('parent') as $item) {
137      if ($item->target_id == 0) {
138        // The <root> parent.
139        $parents[0] = NULL;
140        continue;
141      }
142      $ids[] = $item->target_id;
143    }
144
145    // @todo Better way to do this? AND handle the NULL/0 parent?
146    // Querying the terms again so that the same access checks are run when
147    // getParents() is called as in Drupal version prior to 8.3.
148    $loaded_parents = [];
149
150    if ($ids) {
151      $query = \Drupal::entityQuery('taxonomy_term')
152        ->accessCheck(TRUE)
153        ->condition('tid', $ids, 'IN');
154
155      $loaded_parents = static::loadMultiple($query->execute());
156    }
157
158    return $parents + $loaded_parents;
159  }
160
161  /**
162   * {@inheritdoc}
163   */
164  public function loadAllParents($tid) {
165    /** @var \Drupal\taxonomy\TermInterface $term */
166    return (!empty($tid) && $term = $this->load($tid)) ? $this->getAncestors($term) : [];
167  }
168
169  /**
170   * Returns all ancestors of this term.
171   *
172   * @return \Drupal\taxonomy\TermInterface[]
173   *   A list of ancestor taxonomy term entities keyed by term ID.
174   *
175   * @internal
176   * @todo Refactor away when TreeInterface is introduced.
177   */
178  protected function getAncestors(TermInterface $term) {
179    if (!isset($this->ancestors[$term->id()])) {
180      $this->ancestors[$term->id()] = [$term->id() => $term];
181      $search[] = $term->id();
182
183      while ($tid = array_shift($search)) {
184        foreach ($this->getParents(static::load($tid)) as $id => $parent) {
185          if ($parent && !isset($this->ancestors[$term->id()][$id])) {
186            $this->ancestors[$term->id()][$id] = $parent;
187            $search[] = $id;
188          }
189        }
190      }
191    }
192    return $this->ancestors[$term->id()];
193  }
194
195  /**
196   * {@inheritdoc}
197   */
198  public function loadChildren($tid, $vid = NULL) {
199    /** @var \Drupal\taxonomy\TermInterface $term */
200    return (!empty($tid) && $term = $this->load($tid)) ? $this->getChildren($term) : [];
201  }
202
203  /**
204   * Returns all children terms of this term.
205   *
206   * @return \Drupal\taxonomy\TermInterface[]
207   *   A list of children taxonomy term entities keyed by term ID.
208   *
209   * @internal
210   * @todo Refactor away when TreeInterface is introduced.
211   */
212  public function getChildren(TermInterface $term) {
213    $query = \Drupal::entityQuery('taxonomy_term')
214      ->accessCheck(TRUE)
215      ->condition('parent', $term->id());
216    return static::loadMultiple($query->execute());
217  }
218
219  /**
220   * {@inheritdoc}
221   */
222  public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {
223    $cache_key = implode(':', func_get_args());
224    if (!isset($this->trees[$cache_key])) {
225      // We cache trees, so it's not CPU-intensive to call on a term and its
226      // children, too.
227      if (!isset($this->treeChildren[$vid])) {
228        $this->treeChildren[$vid] = [];
229        $this->treeParents[$vid] = [];
230        $this->treeTerms[$vid] = [];
231        $query = $this->database->select($this->getDataTable(), 't');
232        $query->join('taxonomy_term__parent', 'p', '[t].[tid] = [p].[entity_id]');
233        $query->addExpression('[parent_target_id]', 'parent');
234        $result = $query
235          ->addTag('taxonomy_term_access')
236          ->fields('t')
237          ->condition('t.vid', $vid)
238          ->condition('t.default_langcode', 1)
239          ->orderBy('t.weight')
240          ->orderBy('t.name')
241          ->execute();
242        foreach ($result as $term) {
243          $this->treeChildren[$vid][$term->parent][] = $term->tid;
244          $this->treeParents[$vid][$term->tid][] = $term->parent;
245          $this->treeTerms[$vid][$term->tid] = $term;
246        }
247      }
248
249      // Load full entities, if necessary. The entity controller statically
250      // caches the results.
251      $term_entities = [];
252      if ($load_entities) {
253        $term_entities = $this->loadMultiple(array_keys($this->treeTerms[$vid]));
254      }
255
256      $max_depth = (!isset($max_depth)) ? count($this->treeChildren[$vid]) : $max_depth;
257      $tree = [];
258
259      // Keeps track of the parents we have to process, the last entry is used
260      // for the next processing step.
261      $process_parents = [];
262      $process_parents[] = $parent;
263
264      // Loops over the parent terms and adds its children to the tree array.
265      // Uses a loop instead of a recursion, because it's more efficient.
266      while (count($process_parents)) {
267        $parent = array_pop($process_parents);
268        // The number of parents determines the current depth.
269        $depth = count($process_parents);
270        if ($max_depth > $depth && !empty($this->treeChildren[$vid][$parent])) {
271          $has_children = FALSE;
272          $child = current($this->treeChildren[$vid][$parent]);
273          do {
274            if (empty($child)) {
275              break;
276            }
277            $term = $load_entities ? $term_entities[$child] : $this->treeTerms[$vid][$child];
278            if (isset($this->treeParents[$vid][$load_entities ? $term->id() : $term->tid])) {
279              // Clone the term so that the depth attribute remains correct
280              // in the event of multiple parents.
281              $term = clone $term;
282            }
283            $term->depth = $depth;
284            if (!$load_entities) {
285              unset($term->parent);
286            }
287            $tid = $load_entities ? $term->id() : $term->tid;
288            $term->parents = $this->treeParents[$vid][$tid];
289            $tree[] = $term;
290            if (!empty($this->treeChildren[$vid][$tid])) {
291              $has_children = TRUE;
292
293              // We have to continue with this parent later.
294              $process_parents[] = $parent;
295              // Use the current term as parent for the next iteration.
296              $process_parents[] = $tid;
297
298              // Reset pointers for child lists because we step in there more
299              // often with multi parents.
300              reset($this->treeChildren[$vid][$tid]);
301              // Move pointer so that we get the correct term the next time.
302              next($this->treeChildren[$vid][$parent]);
303              break;
304            }
305          } while ($child = next($this->treeChildren[$vid][$parent]));
306
307          if (!$has_children) {
308            // We processed all terms in this hierarchy-level, reset pointer
309            // so that this function works the next time it gets called.
310            reset($this->treeChildren[$vid][$parent]);
311          }
312        }
313      }
314      $this->trees[$cache_key] = $tree;
315    }
316    return $this->trees[$cache_key];
317  }
318
319  /**
320   * {@inheritdoc}
321   */
322  public function nodeCount($vid) {
323    $query = $this->database->select('taxonomy_index', 'ti');
324    $query->addExpression('COUNT(DISTINCT [ti].[nid])');
325    $query->leftJoin($this->getBaseTable(), 'td', '[ti].[tid] = [td].[tid]');
326    $query->condition('td.vid', $vid);
327    $query->addTag('vocabulary_node_count');
328    return $query->execute()->fetchField();
329  }
330
331  /**
332   * {@inheritdoc}
333   */
334  public function resetWeights($vid) {
335    $this->database->update($this->getDataTable())
336      ->fields(['weight' => 0])
337      ->condition('vid', $vid)
338      ->execute();
339  }
340
341  /**
342   * {@inheritdoc}
343   */
344  public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL) {
345    $query = $this->database->select($this->getDataTable(), 'td');
346    $query->innerJoin('taxonomy_index', 'tn', '[td].[tid] = [tn].[tid]');
347    $query->fields('td', ['tid']);
348    $query->addField('tn', 'nid', 'node_nid');
349    $query->orderby('td.weight');
350    $query->orderby('td.name');
351    $query->condition('tn.nid', $nids, 'IN');
352    $query->addTag('taxonomy_term_access');
353    if (!empty($vocabs)) {
354      $query->condition('td.vid', $vocabs, 'IN');
355    }
356    if (!empty($langcode)) {
357      $query->condition('td.langcode', $langcode);
358    }
359
360    $results = [];
361    $all_tids = [];
362    foreach ($query->execute() as $term_record) {
363      $results[$term_record->node_nid][] = $term_record->tid;
364      $all_tids[] = $term_record->tid;
365    }
366
367    $all_terms = $this->loadMultiple($all_tids);
368    $terms = [];
369    foreach ($results as $nid => $tids) {
370      foreach ($tids as $tid) {
371        $terms[$nid][$tid] = $all_terms[$tid];
372      }
373    }
374    return $terms;
375  }
376
377  /**
378   * {@inheritdoc}
379   */
380  public function getTermIdsWithPendingRevisions() {
381    $table_mapping = $this->getTableMapping();
382    $id_field = $table_mapping->getColumnNames($this->entityType->getKey('id'))['value'];
383    $revision_field = $table_mapping->getColumnNames($this->entityType->getKey('revision'))['value'];
384    $rta_field = $table_mapping->getColumnNames($this->entityType->getKey('revision_translation_affected'))['value'];
385    $langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value'];
386    $revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value'];
387
388    $query = $this->database->select($this->getRevisionDataTable(), 'tfr');
389    $query->fields('tfr', [$id_field]);
390    $query->addExpression("MAX([tfr].[$revision_field])", $revision_field);
391
392    $query->join($this->getRevisionTable(), 'tr', "[tfr].[$revision_field] = [tr].[$revision_field] AND [tr].[$revision_default_field] = 0");
393
394    $inner_select = $this->database->select($this->getRevisionDataTable(), 't');
395    $inner_select->condition("t.$rta_field", '1');
396    $inner_select->fields('t', [$id_field, $langcode_field]);
397    $inner_select->addExpression("MAX([t].[$revision_field])", $revision_field);
398    $inner_select
399      ->groupBy("t.$id_field")
400      ->groupBy("t.$langcode_field");
401
402    $query->join($inner_select, 'mr', "[tfr].[$revision_field] = [mr].[$revision_field] AND [tfr].[$langcode_field] = [mr].[$langcode_field]");
403
404    $query->groupBy("tfr.$id_field");
405
406    return $query->execute()->fetchAllKeyed(1, 0);
407  }
408
409  /**
410   * {@inheritdoc}
411   */
412  public function getVocabularyHierarchyType($vid) {
413    // Return early if we already computed this value.
414    if (isset($this->vocabularyHierarchyType[$vid])) {
415      return $this->vocabularyHierarchyType[$vid];
416    }
417
418    $parent_field_storage = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId)['parent'];
419    $table_mapping = $this->getTableMapping();
420
421    $target_id_column = $table_mapping->getFieldColumnName($parent_field_storage, 'target_id');
422    $delta_column = $table_mapping->getFieldColumnName($parent_field_storage, TableMappingInterface::DELTA);
423
424    $query = $this->database->select($table_mapping->getFieldTableName('parent'), 'p');
425    $query->addExpression("MAX([$target_id_column])", 'max_parent_id');
426    $query->addExpression("MAX([$delta_column])", 'max_delta');
427    $query->condition('bundle', $vid);
428
429    $result = $query->execute()->fetchAll();
430
431    // If all the terms have the same parent, the parent can only be root (0).
432    if ((int) $result[0]->max_parent_id === 0) {
433      $this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_DISABLED;
434    }
435    // If no term has a delta higher than 0, no term has multiple parents.
436    elseif ((int) $result[0]->max_delta === 0) {
437      $this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_SINGLE;
438    }
439    else {
440      $this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_MULTIPLE;
441    }
442
443    return $this->vocabularyHierarchyType[$vid];
444  }
445
446  /**
447   * {@inheritdoc}
448   */
449  public function __sleep() {
450    $vars = parent::__sleep();
451    // Do not serialize static cache.
452    unset($vars['ancestors'], $vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees'], $vars['vocabularyHierarchyType']);
453    return $vars;
454  }
455
456  /**
457   * {@inheritdoc}
458   */
459  public function __wakeup() {
460    parent::__wakeup();
461    // Initialize static caches.
462    $this->ancestors = [];
463    $this->treeChildren = [];
464    $this->treeParents = [];
465    $this->treeTerms = [];
466    $this->trees = [];
467    $this->vocabularyHierarchyType = [];
468  }
469
470}
471