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        ->condition('tid', $ids, 'IN');
153
154      $loaded_parents = static::loadMultiple($query->execute());
155    }
156
157    return $parents + $loaded_parents;
158  }
159
160  /**
161   * {@inheritdoc}
162   */
163  public function loadAllParents($tid) {
164    /** @var \Drupal\taxonomy\TermInterface $term */
165    return (!empty($tid) && $term = $this->load($tid)) ? $this->getAncestors($term) : [];
166  }
167
168  /**
169   * Returns all ancestors of this term.
170   *
171   * @return \Drupal\taxonomy\TermInterface[]
172   *   A list of ancestor taxonomy term entities keyed by term ID.
173   *
174   * @internal
175   * @todo Refactor away when TreeInterface is introduced.
176   */
177  protected function getAncestors(TermInterface $term) {
178    if (!isset($this->ancestors[$term->id()])) {
179      $this->ancestors[$term->id()] = [$term->id() => $term];
180      $search[] = $term->id();
181
182      while ($tid = array_shift($search)) {
183        foreach ($this->getParents(static::load($tid)) as $id => $parent) {
184          if ($parent && !isset($this->ancestors[$term->id()][$id])) {
185            $this->ancestors[$term->id()][$id] = $parent;
186            $search[] = $id;
187          }
188        }
189      }
190    }
191    return $this->ancestors[$term->id()];
192  }
193
194  /**
195   * {@inheritdoc}
196   */
197  public function loadChildren($tid, $vid = NULL) {
198    /** @var \Drupal\taxonomy\TermInterface $term */
199    return (!empty($tid) && $term = $this->load($tid)) ? $this->getChildren($term) : [];
200  }
201
202  /**
203   * Returns all children terms of this term.
204   *
205   * @return \Drupal\taxonomy\TermInterface[]
206   *   A list of children taxonomy term entities keyed by term ID.
207   *
208   * @internal
209   * @todo Refactor away when TreeInterface is introduced.
210   */
211  public function getChildren(TermInterface $term) {
212    $query = \Drupal::entityQuery('taxonomy_term')
213      ->condition('parent', $term->id());
214    return static::loadMultiple($query->execute());
215  }
216
217  /**
218   * {@inheritdoc}
219   */
220  public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {
221    $cache_key = implode(':', func_get_args());
222    if (!isset($this->trees[$cache_key])) {
223      // We cache trees, so it's not CPU-intensive to call on a term and its
224      // children, too.
225      if (!isset($this->treeChildren[$vid])) {
226        $this->treeChildren[$vid] = [];
227        $this->treeParents[$vid] = [];
228        $this->treeTerms[$vid] = [];
229        $query = $this->database->select($this->getDataTable(), 't');
230        $query->join('taxonomy_term__parent', 'p', 't.tid = p.entity_id');
231        $query->addExpression('parent_target_id', 'parent');
232        $result = $query
233          ->addTag('taxonomy_term_access')
234          ->fields('t')
235          ->condition('t.vid', $vid)
236          ->condition('t.default_langcode', 1)
237          ->orderBy('t.weight')
238          ->orderBy('t.name')
239          ->execute();
240        foreach ($result as $term) {
241          $this->treeChildren[$vid][$term->parent][] = $term->tid;
242          $this->treeParents[$vid][$term->tid][] = $term->parent;
243          $this->treeTerms[$vid][$term->tid] = $term;
244        }
245      }
246
247      // Load full entities, if necessary. The entity controller statically
248      // caches the results.
249      $term_entities = [];
250      if ($load_entities) {
251        $term_entities = $this->loadMultiple(array_keys($this->treeTerms[$vid]));
252      }
253
254      $max_depth = (!isset($max_depth)) ? count($this->treeChildren[$vid]) : $max_depth;
255      $tree = [];
256
257      // Keeps track of the parents we have to process, the last entry is used
258      // for the next processing step.
259      $process_parents = [];
260      $process_parents[] = $parent;
261
262      // Loops over the parent terms and adds its children to the tree array.
263      // Uses a loop instead of a recursion, because it's more efficient.
264      while (count($process_parents)) {
265        $parent = array_pop($process_parents);
266        // The number of parents determines the current depth.
267        $depth = count($process_parents);
268        if ($max_depth > $depth && !empty($this->treeChildren[$vid][$parent])) {
269          $has_children = FALSE;
270          $child = current($this->treeChildren[$vid][$parent]);
271          do {
272            if (empty($child)) {
273              break;
274            }
275            $term = $load_entities ? $term_entities[$child] : $this->treeTerms[$vid][$child];
276            if (isset($this->treeParents[$vid][$load_entities ? $term->id() : $term->tid])) {
277              // Clone the term so that the depth attribute remains correct
278              // in the event of multiple parents.
279              $term = clone $term;
280            }
281            $term->depth = $depth;
282            if (!$load_entities) {
283              unset($term->parent);
284            }
285            $tid = $load_entities ? $term->id() : $term->tid;
286            $term->parents = $this->treeParents[$vid][$tid];
287            $tree[] = $term;
288            if (!empty($this->treeChildren[$vid][$tid])) {
289              $has_children = TRUE;
290
291              // We have to continue with this parent later.
292              $process_parents[] = $parent;
293              // Use the current term as parent for the next iteration.
294              $process_parents[] = $tid;
295
296              // Reset pointers for child lists because we step in there more
297              // often with multi parents.
298              reset($this->treeChildren[$vid][$tid]);
299              // Move pointer so that we get the correct term the next time.
300              next($this->treeChildren[$vid][$parent]);
301              break;
302            }
303          } while ($child = next($this->treeChildren[$vid][$parent]));
304
305          if (!$has_children) {
306            // We processed all terms in this hierarchy-level, reset pointer
307            // so that this function works the next time it gets called.
308            reset($this->treeChildren[$vid][$parent]);
309          }
310        }
311      }
312      $this->trees[$cache_key] = $tree;
313    }
314    return $this->trees[$cache_key];
315  }
316
317  /**
318   * {@inheritdoc}
319   */
320  public function nodeCount($vid) {
321    $query = $this->database->select('taxonomy_index', 'ti');
322    $query->addExpression('COUNT(DISTINCT ti.nid)');
323    $query->leftJoin($this->getBaseTable(), 'td', 'ti.tid = td.tid');
324    $query->condition('td.vid', $vid);
325    $query->addTag('vocabulary_node_count');
326    return $query->execute()->fetchField();
327  }
328
329  /**
330   * {@inheritdoc}
331   */
332  public function resetWeights($vid) {
333    $this->database->update($this->getDataTable())
334      ->fields(['weight' => 0])
335      ->condition('vid', $vid)
336      ->execute();
337  }
338
339  /**
340   * {@inheritdoc}
341   */
342  public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL) {
343    $query = $this->database->select($this->getDataTable(), 'td');
344    $query->innerJoin('taxonomy_index', 'tn', 'td.tid = tn.tid');
345    $query->fields('td', ['tid']);
346    $query->addField('tn', 'nid', 'node_nid');
347    $query->orderby('td.weight');
348    $query->orderby('td.name');
349    $query->condition('tn.nid', $nids, 'IN');
350    $query->addTag('taxonomy_term_access');
351    if (!empty($vocabs)) {
352      $query->condition('td.vid', $vocabs, 'IN');
353    }
354    if (!empty($langcode)) {
355      $query->condition('td.langcode', $langcode);
356    }
357
358    $results = [];
359    $all_tids = [];
360    foreach ($query->execute() as $term_record) {
361      $results[$term_record->node_nid][] = $term_record->tid;
362      $all_tids[] = $term_record->tid;
363    }
364
365    $all_terms = $this->loadMultiple($all_tids);
366    $terms = [];
367    foreach ($results as $nid => $tids) {
368      foreach ($tids as $tid) {
369        $terms[$nid][$tid] = $all_terms[$tid];
370      }
371    }
372    return $terms;
373  }
374
375  /**
376   * {@inheritdoc}
377   */
378  public function getTermIdsWithPendingRevisions() {
379    $table_mapping = $this->getTableMapping();
380    $id_field = $table_mapping->getColumnNames($this->entityType->getKey('id'))['value'];
381    $revision_field = $table_mapping->getColumnNames($this->entityType->getKey('revision'))['value'];
382    $rta_field = $table_mapping->getColumnNames($this->entityType->getKey('revision_translation_affected'))['value'];
383    $langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value'];
384    $revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value'];
385
386    $query = $this->database->select($this->getRevisionDataTable(), 'tfr');
387    $query->fields('tfr', [$id_field]);
388    $query->addExpression("MAX(tfr.$revision_field)", $revision_field);
389
390    $query->join($this->getRevisionTable(), 'tr', "tfr.$revision_field = tr.$revision_field AND tr.$revision_default_field = 0");
391
392    $inner_select = $this->database->select($this->getRevisionDataTable(), 't');
393    $inner_select->condition("t.$rta_field", '1');
394    $inner_select->fields('t', [$id_field, $langcode_field]);
395    $inner_select->addExpression("MAX(t.$revision_field)", $revision_field);
396    $inner_select
397      ->groupBy("t.$id_field")
398      ->groupBy("t.$langcode_field");
399
400    $query->join($inner_select, 'mr', "tfr.$revision_field = mr.$revision_field AND tfr.$langcode_field = mr.$langcode_field");
401
402    $query->groupBy("tfr.$id_field");
403
404    return $query->execute()->fetchAllKeyed(1, 0);
405  }
406
407  /**
408   * {@inheritdoc}
409   */
410  public function getVocabularyHierarchyType($vid) {
411    // Return early if we already computed this value.
412    if (isset($this->vocabularyHierarchyType[$vid])) {
413      return $this->vocabularyHierarchyType[$vid];
414    }
415
416    $parent_field_storage = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId)['parent'];
417    $table_mapping = $this->getTableMapping();
418
419    $target_id_column = $table_mapping->getFieldColumnName($parent_field_storage, 'target_id');
420    $delta_column = $table_mapping->getFieldColumnName($parent_field_storage, TableMappingInterface::DELTA);
421
422    $query = $this->database->select($table_mapping->getFieldTableName('parent'), 'p');
423    $query->addExpression("MAX($target_id_column)", 'max_parent_id');
424    $query->addExpression("MAX($delta_column)", 'max_delta');
425    $query->condition('bundle', $vid);
426
427    $result = $query->execute()->fetchAll();
428
429    // If all the terms have the same parent, the parent can only be root (0).
430    if ((int) $result[0]->max_parent_id === 0) {
431      $this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_DISABLED;
432    }
433    // If no term has a delta higher than 0, no term has multiple parents.
434    elseif ((int) $result[0]->max_delta === 0) {
435      $this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_SINGLE;
436    }
437    else {
438      $this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_MULTIPLE;
439    }
440
441    return $this->vocabularyHierarchyType[$vid];
442  }
443
444  /**
445   * {@inheritdoc}
446   */
447  public function __sleep() {
448    $vars = parent::__sleep();
449    // Do not serialize static cache.
450    unset($vars['ancestors'], $vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees'], $vars['vocabularyHierarchyType']);
451    return $vars;
452  }
453
454  /**
455   * {@inheritdoc}
456   */
457  public function __wakeup() {
458    parent::__wakeup();
459    // Initialize static caches.
460    $this->ancestors = [];
461    $this->treeChildren = [];
462    $this->treeParents = [];
463    $this->treeTerms = [];
464    $this->trees = [];
465    $this->vocabularyHierarchyType = [];
466  }
467
468}
469