access_policy-1.0.x-dev/src/TermHierarchy.php
src/TermHierarchy.php
<?php namespace Drupal\access_policy; use Drupal\Core\Database\Connection; /** * Defines a class for fetching taxonomy terms in a hierarchical way. * * This is primarily to address an issue in core where TermStorage::loadTree() * will always filter the results by taxonomy_term_access. When used with * access policy that method can cause an infinite loop. Once a fix for that * issue lands this service can be removed. * * @see https://www.drupal.org/project/drupal/issues/2938848 */ class TermHierarchy { /** * Array of term parents keyed by vocabulary ID and child term ID. * * @var array */ protected $treeParents = []; /** * Array of term ancestors keyed by vocabulary ID and parent term ID. * * @var array */ protected $treeChildren = []; /** * Array of terms in a tree keyed by vocabulary ID and term ID. * * @var array */ protected $treeTerms = []; /** * Array of loaded trees keyed by a cache id matching tree arguments. * * @var array */ protected $trees = []; /** * The database connection. * * @var \Drupal\Core\Database\Connection */ protected $database; /** * The taxonomy term data table. * * @var string */ protected $dataTable = 'taxonomy_term_field_data'; /** * Constructs a TermHierarchy object. * * @param \Drupal\Core\Database\Connection $connection * The database connection. */ public function __construct(Connection $connection) { $this->database = $connection; } /** * Finds all terms in a given vocabulary ID. * * This is copied from TermStorage::loadTree() and modified slightly to remove * 'taxonomy_term_access' tag. * * @param string $vid * Vocabulary ID to retrieve terms for. * @param int $parent * The term ID under which to generate the tree. If 0, generate the tree * for the entire vocabulary. * @param int $max_depth * The number of levels of the tree to return. Leave NULL to return all * levels. * * @return object[]|\Drupal\taxonomy\TermInterface[] * A numerically indexed array of term objects that are the children of the * vocabulary $vid. */ public function loadTree($vid, $parent = 0, $max_depth = NULL) { $cache_key = implode(':', func_get_args()); if (!isset($this->trees[$cache_key])) { // We cache trees, so it's not CPU-intensive to call on a term and its // children, too. if (!isset($this->treeChildren[$vid])) { $this->treeChildren[$vid] = []; $this->treeParents[$vid] = []; $this->treeTerms[$vid] = []; $query = $this->getQuery($vid); $result = $query->execute(); foreach ($result as $term) { $this->treeChildren[$vid][$term->parent][] = $term->tid; $this->treeParents[$vid][$term->tid][] = $term->parent; $this->treeTerms[$vid][$term->tid] = $term; } } $max_depth = (!isset($max_depth)) ? count($this->treeChildren[$vid]) : $max_depth; $tree = []; // Keeps track of the parents we have to process, the last entry is used // for the next processing step. $process_parents = []; $process_parents[] = $parent; // Loops over the parent terms and adds its children to the tree array. // Uses a loop instead of a recursion, because it's more efficient. while (count($process_parents)) { $parent = array_pop($process_parents); // The number of parents determines the current depth. $depth = count($process_parents); if ($max_depth > $depth && !empty($this->treeChildren[$vid][$parent])) { $has_children = FALSE; $child = current($this->treeChildren[$vid][$parent]); do { if (empty($child)) { break; } $term = $this->treeTerms[$vid][$child]; if (isset($this->treeParents[$vid][$term->tid])) { // Clone the term so that the depth attribute remains correct // in the event of multiple parents. $term = clone $term; } $term->depth = $depth; unset($term->parent); $tid = $term->tid; $term->parents = $this->treeParents[$vid][$tid]; $tree[] = $term; if (!empty($this->treeChildren[$vid][$tid])) { $has_children = TRUE; // We have to continue with this parent later. $process_parents[] = $parent; // Use the current term as parent for the next iteration. $process_parents[] = $tid; // Reset pointers for child lists because we step in there more // often with multi parents. reset($this->treeChildren[$vid][$tid]); // Move pointer so that we get the correct term the next time. next($this->treeChildren[$vid][$parent]); break; } } while ($child = next($this->treeChildren[$vid][$parent])); if (!$has_children) { // We processed all terms in this hierarchy-level, reset pointer // so that this function works the next time it gets called. reset($this->treeChildren[$vid][$parent]); } } } $this->trees[$cache_key] = $tree; } return $this->trees[$cache_key]; } /** * Get the hierarchical query. * * The primary change that this method makes is to remove the * 'taxonomy_term_access' tag from the query. * * @param string $vid * The vocabulary id. * * @return \Drupal\Core\Database\Query\SelectInterface * The database select query. */ protected function getQuery($vid) { $query = $this->database->select($this->dataTable, 't'); $query->join('taxonomy_term__parent', 'p', '[t].[tid] = [p].[entity_id]'); $query->addExpression('[parent_target_id]', 'parent'); $query ->fields('t') ->condition('t.vid', $vid) ->condition('t.default_langcode', 1) ->orderBy('t.weight') ->orderBy('t.name'); return $query; } /** * Reset the term hierarchy cache. */ public function resetCache() { $this->treeChildren = []; $this->treeParents = []; $this->treeTerms = []; $this->trees = []; } }