Commit 4a57034d authored by catch's avatar catch
Browse files

Issue #2207893 by dawehner, pwolanin, jessebeach, Boobaa: Convert menu tree building to a service.

parent 1a51606b
Loading
Loading
Loading
Loading
+7 −590
Changes for core/includes/menu.inc: 7 added lines, 590 removed lines.
Original line number Diff line number Diff line
@@ -321,594 +321,6 @@ function _menu_link_translate(&$item) {
  }
}

/**
 * Renders a menu tree based on the current path.
 *
 * @param $menu_name
 *   The name of the menu.
 *
 * @return
 *   A structured array representing the specified menu on the current page, to
 *   be rendered by drupal_render().
 */
function menu_tree($menu_name) {
  $menu_output = &drupal_static(__FUNCTION__, array());

  if (!isset($menu_output[$menu_name])) {
    $tree = menu_tree_page_data($menu_name);
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

/**
 * Returns an output structure for rendering a menu tree.
 *
 * The menu item's LI element is given one of the following classes:
 * - expanded: The menu item is showing its submenu.
 * - collapsed: The menu item has a submenu which is not shown.
 * - leaf: The menu item has no submenu.
 *
 * @param $tree
 *   A data structure representing the tree as returned from menu_tree_data.
 *
 * @return
 *   A structured array to be rendered by drupal_render().
 */
function menu_tree_output($tree) {
  $build = array();
  $items = array();

  // Pull out just the menu links we are going to render so that we
  // get an accurate count for the first/last classes.
  foreach ($tree as $data) {
    if ($data['link']['access'] && !$data['link']['hidden']) {
      $items[] = $data;
    }
  }

  foreach ($items as $data) {
    $class = array();
    // Set a class for the <li>-tag. Since $data['below'] may contain local
    // tasks, only set 'expanded' class if the link also has children within
    // the current menu.
    if ($data['link']['has_children'] && $data['below']) {
      $class[] = 'expanded';
    }
    elseif ($data['link']['has_children']) {
      $class[] = 'collapsed';
    }
    else {
      $class[] = 'leaf';
    }
    // Set a class if the link is in the active trail.
    if ($data['link']['in_active_trail']) {
      $class[] = 'active-trail';
      $data['link']['localized_options']['attributes']['class'][] = 'active-trail';
    }

    // Allow menu-specific theme overrides.
    $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
    $element['#attributes']['class'] = $class;
    $element['#title'] = $data['link']['title'];
    // @todo Use route name and parameters to generate the link path, unless
    //    it is external.
    $element['#href'] = $data['link']['link_path'];
    $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
    $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below'];
    $element['#original_link'] = $data['link'];
    // Index using the link's unique mlid.
    $build[$data['link']['mlid']] = $element;
  }
  if ($build) {
    // Make sure drupal_render() does not re-order the links.
    $build['#sorted'] = TRUE;
    // Add the theme wrapper for outer markup.
    // Allow menu-specific theme overrides.
    $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_');
    // Set cache tag.
    $menu_name = $data['link']['menu_name'];
    $build['#cache']['tags']['menu'][$menu_name] = $menu_name;
  }

  return $build;
}

/**
 * Gets the data structure representing a named menu tree.
 *
 * Since this can be the full tree including hidden items, the data returned
 * may be used for generating an an admin interface or a select.
 *
 * @param $menu_name
 *   The named menu links to return
 * @param $link
 *   A fully loaded menu link, or NULL. If a link is supplied, only the
 *   path to root will be included in the returned tree - as if this link
 *   represented the current page in a visible menu.
 * @param $max_depth
 *   Optional maximum depth of links to retrieve. Typically useful if only one
 *   or two levels of a sub tree are needed in conjunction with a non-NULL
 *   $link, in which case $max_depth should be greater than $link['depth'].
 *
 * @return
 *   An tree of menu links in an array, in the order they should be rendered.
 */
function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) {
  $tree = &drupal_static(__FUNCTION__, array());
  $language_interface = \Drupal::languageManager()->getCurrentLanguage();

  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
  $mlid = isset($link['mlid']) ? $link['mlid'] : 0;
  // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth.
  $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $language_interface->id . ':' . (int) $max_depth;

  if (!isset($tree[$cid])) {
    // If the static variable doesn't have the data, check {cache_data}.
    $cache = \Drupal::cache('data')->get($cid);
    if ($cache && isset($cache->data)) {
      // If the cache entry exists, it contains the parameters for
      // menu_build_tree().
      $tree_parameters = $cache->data;
    }
    // If the tree data was not in the cache, build $tree_parameters.
    if (!isset($tree_parameters)) {
      $tree_parameters = array(
        'min_depth' => 1,
        'max_depth' => $max_depth,
      );
      if ($mlid) {
        // The tree is for a single item, so we need to match the values in its
        // p columns and 0 (the top level) with the plid values of other links.
        $parents = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          if (!empty($link["p$i"])) {
            $parents[] = $link["p$i"];
          }
        }
        $tree_parameters['expanded'] = $parents;
        $tree_parameters['active_trail'] = $parents;
        $tree_parameters['active_trail'][] = $mlid;
      }

      // Cache the tree building parameters using the page-specific cid.
      \Drupal::cache('data')->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
    }

    // Build the tree using the parameters; the resulting tree will be cached
    // by _menu_build_tree()).
    $tree[$cid] = menu_build_tree($menu_name, $tree_parameters);
  }

  return $tree[$cid];
}

/**
 * Sets the path for determining the active trail of the specified menu tree.
 *
 * This path will also affect the breadcrumbs under some circumstances.
 * Breadcrumbs are built using the preferred link returned by
 * menu_link_get_preferred(). If the preferred link is inside one of the menus
 * specified in calls to menu_tree_set_path(), the preferred link will be
 * overridden by the corresponding path returned by menu_tree_get_path().
 *
 * Setting this path does not affect the main content; for that use
 * menu_set_active_item() instead.
 *
 * @param $menu_name
 *   The name of the affected menu tree.
 * @param $path
 *   The path to use when finding the active trail.
 */
function menu_tree_set_path($menu_name, $path = NULL) {
  $paths = &drupal_static(__FUNCTION__);
  if (isset($path)) {
    $paths[$menu_name] = $path;
  }
  return isset($paths[$menu_name]) ? $paths[$menu_name] : NULL;
}

/**
 * Gets the path for determining the active trail of the specified menu tree.
 *
 * @param $menu_name
 *   The menu name of the requested tree.
 *
 * @return
 *   A string containing the path. If no path has been specified with
 *   menu_tree_set_path(), NULL is returned.
 */
function menu_tree_get_path($menu_name) {
  return menu_tree_set_path($menu_name);
}

/**
 * Gets the data structure for a named menu tree, based on the current page.
 *
 * The tree order is maintained by storing each parent in an individual
 * field, see http://drupal.org/node/141866 for more.
 *
 * @param $menu_name
 *   The named menu links to return.
 * @param $max_depth
 *   (optional) The maximum depth of links to retrieve.
 * @param $only_active_trail
 *   (optional) Whether to only return the links in the active trail (TRUE)
 *   instead of all links on every level of the menu link tree (FALSE). Defaults
 *   to FALSE.
 *
 * @return
 *   An array of menu links, in the order they should be rendered. The array
 *   is a list of associative arrays -- these have two keys, link and below.
 *   link is a menu item, ready for theming as a link. Below represents the
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
 */
function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
  $tree = &drupal_static(__FUNCTION__, array());

  $language_interface = \Drupal::languageManager()->getCurrentLanguage();

  // Check if the active trail has been overridden for this menu tree.
  $active_path = menu_tree_get_path($menu_name);
  // Load the request corresponding to the current page.
  $request = \Drupal::request();
  $system_path = NULL;
  if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) {
    // @todo https://drupal.org/node/2068471 is adding support so we can tell
    // if this is called on a 404/403 page.
    $system_path = $request->attributes->get('_system_path');
    $page_not_403 = 1;
  }
  if (isset($system_path)) {
    if (isset($max_depth)) {
      $max_depth = min($max_depth, MENU_MAX_DEPTH);
    }
    // Generate a cache ID (cid) specific for this page.
    $cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_not_403 . ':' . (int) $max_depth;
    // If we are asked for the active trail only, and $menu_name has not been
    // built and cached for this page yet, then this likely means that it
    // won't be built anymore, as this function is invoked from
    // template_preprocess_page(). So in order to not build a giant menu tree
    // that needs to be checked for access on all levels, we simply check
    // whether we have the menu already in cache, or otherwise, build a minimum
    // tree containing the active trail only.
    // @see menu_set_active_trail()
    if (!isset($tree[$cid]) && $only_active_trail) {
      $cid .= ':trail';
    }

    if (!isset($tree[$cid])) {
      // If the static variable doesn't have the data, check {cache_data}.
      $cache = \Drupal::cache('data')->get($cid);
      if ($cache && isset($cache->data)) {
        // If the cache entry exists, it contains the parameters for
        // menu_build_tree().
        $tree_parameters = $cache->data;
      }
      // If the tree data was not in the cache, build $tree_parameters.
      if (!isset($tree_parameters)) {
        $tree_parameters = array(
          'min_depth' => 1,
          'max_depth' => $max_depth,
        );
        // Parent mlids; used both as key and value to ensure uniqueness.
        // We always want all the top-level links with plid == 0.
        $active_trail = array(0 => 0);

        // If this page is accessible to the current user, build the tree
        // parameters accordingly.
        if ($page_not_403) {
          // Find a menu link corresponding to the current path. If $active_path
          // is NULL, let menu_link_get_preferred() determine the path.
          if ($active_link = menu_link_get_preferred($active_path, $menu_name)) {
            // The active link may only be taken into account to build the
            // active trail, if it resides in the requested menu. Otherwise,
            // we'd needlessly re-run _menu_build_tree() queries for every menu
            // on every page.
            if ($active_link['menu_name'] == $menu_name) {
              // Use all the coordinates, except the last one because there
              // can be no child beyond the last column.
              for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
                if ($active_link['p' . $i]) {
                  $active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
                }
              }
              // If we are asked to build links for the active trail only, skip
              // the entire 'expanded' handling.
              if ($only_active_trail) {
                $tree_parameters['only_active_trail'] = TRUE;
              }
            }
          }
          $parents = $active_trail;

          $expanded = \Drupal::state()->get('menu_expanded');
          // Check whether the current menu has any links set to be expanded.
          if (!$only_active_trail && $expanded && in_array($menu_name, $expanded)) {
            // Collect all the links set to be expanded, and then add all of
            // their children to the list as well.
            do {
              $query = \Drupal::entityQuery('menu_link')
                ->condition('menu_name', $menu_name)
                ->condition('expanded', 1)
                ->condition('has_children', 1)
                ->condition('plid', $parents, 'IN')
                ->condition('mlid', $parents, 'NOT IN');
              $result = $query->execute();
              $parents += $result;
            } while (!empty($result));
          }
          $tree_parameters['expanded'] = $parents;
          $tree_parameters['active_trail'] = $active_trail;
        }
        // If access is denied, we only show top-level links in menus.
        else {
          $tree_parameters['expanded'] = $active_trail;
          $tree_parameters['active_trail'] = $active_trail;
        }
        // Cache the tree building parameters using the page-specific cid.
        \Drupal::cache('data')->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
      }

      // Build the tree using the parameters; the resulting tree will be cached
      // by _menu_build_tree().
      $tree[$cid] = menu_build_tree($menu_name, $tree_parameters);
    }
    return $tree[$cid];
  }

  return array();
}

/**
 * Builds a menu tree, translates links, and checks access.
 *
 * @param $menu_name
 *   The name of the menu.
 * @param $parameters
 *   (optional) An associative array of build parameters. Possible keys:
 *   - expanded: An array of parent link ids to return only menu links that are
 *     children of one of the plids in this list. If empty, the whole menu tree
 *     is built, unless 'only_active_trail' is TRUE.
 *   - active_trail: An array of mlids, representing the coordinates of the
 *     currently active menu link.
 *   - only_active_trail: Whether to only return links that are in the active
 *     trail. This option is ignored, if 'expanded' is non-empty.
 *   - min_depth: The minimum depth of menu links in the resulting tree.
 *     Defaults to 1, which is the default to build a whole tree for a menu
 *     (excluding menu container itself).
 *   - max_depth: The maximum depth of menu links in the resulting tree.
 *   - conditions: An associative array of custom database select query
 *     condition key/value pairs; see _menu_build_tree() for the actual query.
 *
 * @return
 *   A fully built menu tree.
 */
function menu_build_tree($menu_name, array $parameters = array()) {
  // Build the menu tree.
  $data = _menu_build_tree($menu_name, $parameters);
  // Check access for the current user to each item in the tree.
  menu_tree_check_access($data['tree'], $data['node_links']);
  return $data['tree'];
}

/**
 * Builds a menu tree.
 *
 * This function may be used build the data for a menu tree only, for example
 * to further massage the data manually before further processing happens.
 * menu_tree_check_access() needs to be invoked afterwards.
 *
 * @see menu_build_tree()
 */
function _menu_build_tree($menu_name, array $parameters = array()) {
  // Static cache of already built menu trees.
  $trees = &drupal_static(__FUNCTION__, array());
  $language_interface = \Drupal::languageManager()->getCurrentLanguage();

  // Build the cache id; sort parents to prevent duplicate storage and remove
  // default parameter values.
  if (isset($parameters['expanded'])) {
    sort($parameters['expanded']);
  }
  $tree_cid = 'links:' . $menu_name . ':tree-data:' . $language_interface->id . ':' . hash('sha256', serialize($parameters));

  // If we do not have this tree in the static cache, check {cache_data}.
  if (!isset($trees[$tree_cid])) {
    $cache = \Drupal::cache('data')->get($tree_cid);
    if ($cache && isset($cache->data)) {
      $trees[$tree_cid] = $cache->data;
    }
  }

  if (!isset($trees[$tree_cid])) {
    $query = \Drupal::entityQuery('menu_link');
    for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
      $query->sort('p' . $i, 'ASC');
    }
    $query->condition('menu_name', $menu_name);
    if (!empty($parameters['expanded'])) {
      $query->condition('plid', $parameters['expanded'], 'IN');
    }
    elseif (!empty($parameters['only_active_trail'])) {
      $query->condition('mlid', $parameters['active_trail'], 'IN');
    }
    $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
    if ($min_depth != 1) {
      $query->condition('depth', $min_depth, '>=');
    }
    if (isset($parameters['max_depth'])) {
      $query->condition('depth', $parameters['max_depth'], '<=');
    }
    // Add custom query conditions, if any were passed.
    if (isset($parameters['conditions'])) {
      foreach ($parameters['conditions'] as $column => $value) {
        $query->condition($column, $value);
      }
    }

    // Build an ordered array of links using the query result object.
    $links = array();
    if ($result = $query->execute()) {
      $links = menu_link_load_multiple($result);
    }
    $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
    $data['tree'] = menu_tree_data($links, $active_trail, $min_depth);
    $data['node_links'] = array();
    menu_tree_collect_node_links($data['tree'], $data['node_links']);

    // Cache the data, if it is not already in the cache.
    \Drupal::cache('data')->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name));
    $trees[$tree_cid] = $data;
  }

  return $trees[$tree_cid];
}

/**
 * Collects node links from a given menu tree recursively.
 *
 * @param $tree
 *   The menu tree you wish to collect node links from.
 * @param $node_links
 *   An array in which to store the collected node links.
 */
function menu_tree_collect_node_links(&$tree, &$node_links) {
  foreach ($tree as $key => $v) {
    if ($tree[$key]['link']['router_path'] == 'node/%') {
      $nid = substr($tree[$key]['link']['link_path'], 5);
      if (is_numeric($nid)) {
        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
        $tree[$key]['link']['access'] = FALSE;
      }
    }
    if ($tree[$key]['below']) {
      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
    }
  }
}

/**
 * Checks access and performs dynamic operations for each link in the tree.
 *
 * @param $tree
 *   The menu tree you wish to operate on.
 * @param $node_links
 *   A collection of node link references generated from $tree by
 *   menu_tree_collect_node_links().
 */
function menu_tree_check_access(&$tree, $node_links = array()) {
  if ($node_links) {
    $nids = array_keys($node_links);
    $select = db_select('node_field_data', 'n');
    $select->addField('n', 'nid');
    // @todo This should be actually filtering on the desired node status field
    //   language and just fall back to the default language.
    $select->condition('n.status', 1);

    $select->condition('n.nid', $nids, 'IN');
    $select->addTag('node_access');
    $nids = $select->execute()->fetchCol();
    foreach ($nids as $nid) {
      foreach ($node_links[$nid] as $mlid => $link) {
        $node_links[$nid][$mlid]['access'] = TRUE;
      }
    }
  }
  _menu_tree_check_access($tree);
}

/**
 * Sorts the menu tree and recursively checks access for each item.
 */
function _menu_tree_check_access(&$tree) {
  $new_tree = array();
  foreach ($tree as $key => $v) {
    $item = &$tree[$key]['link'];
    _menu_link_translate($item);
    if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) {
      if ($tree[$key]['below']) {
        _menu_tree_check_access($tree[$key]['below']);
      }
      // The weights are made a uniform 5 digits by adding 50000 as an offset.
      // After _menu_link_translate(), $item['title'] has the localized link title.
      // Adding the mlid to the end of the index insures that it is unique.
      $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key];
    }
  }
  // Sort siblings in the tree based on the weights and localized titles.
  ksort($new_tree);
  $tree = $new_tree;
}

/**
 * Sorts and returns the built data representing a menu tree.
 *
 * @param $links
 *   A flat array of menu links that are part of the menu. Each array element
 *   is an associative array of information about the menu link, containing the
 *   fields from the {menu_links} table, and optionally additional information
 *   from the {menu_router} table, if the menu item appears in both tables.
 *   This array must be ordered depth-first. See _menu_build_tree() for a sample
 *   query.
 * @param $parents
 *   An array of the menu link ID values that are in the path from the current
 *   page to the root of the menu tree.
 * @param $depth
 *   The minimum depth to include in the returned menu tree.
 *
 * @return
 *   An array of menu links in the form of a tree. Each item in the tree is an
 *   associative array containing:
 *   - link: The menu link item from $links, with additional element
 *     'in_active_trail' (TRUE if the link ID was in $parents).
 *   - below: An array containing the sub-tree of this item, where each element
 *     is a tree item array with 'link' and 'below' elements. This array will be
 *     empty if the menu item has no items in its sub-tree having a depth
 *     greater than or equal to $depth.
 */
function menu_tree_data(array $links, array $parents = array(), $depth = 1) {
  // Reverse the array so we can use the more efficient array_pop() function.
  $links = array_reverse($links);
  return _menu_tree_data($links, $parents, $depth);
}

/**
 * Builds the data representing a menu tree.
 *
 * The function is a bit complex because the rendering of a link depends on
 * the next menu link.
 */
function _menu_tree_data(&$links, $parents, $depth) {
  $tree = array();
  while ($item = array_pop($links)) {
    // We need to determine if we're on the path to root so we can later build
    // the correct active trail.
    $item['in_active_trail'] = in_array($item['mlid'], $parents);
    // Add the current link to the tree.
    $tree[$item['mlid']] = array(
      'link' => $item,
      'below' => array(),
    );
    // Look ahead to the next link, but leave it on the array so it's available
    // to other recursive function calls if we return or build a sub-tree.
    $next = end($links);
    // Check whether the next link is the first in a new sub-tree.
    if ($next && $next['depth'] > $depth) {
      // Recursively call _menu_tree_data to build the sub-tree.
      $tree[$item['mlid']]['below'] = _menu_tree_data($links, $parents, $next['depth']);
      // Fetch next link after filling the sub-tree.
      $next = end($links);
    }
    // Determine if we should exit the loop and return.
    if (!$next || $next['depth'] < $depth) {
      break;
    }
  }
  return $tree;
}

/**
 * Implements template_preprocess_HOOK() for theme_menu_tree().
 */
@@ -1112,7 +524,9 @@ function menu_navigation_links($menu_name, $level = 0) {
  }

  // Get the menu hierarchy for the current page.
  $tree = menu_tree_page_data($menu_name, $level + 1);
  /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
  $menu_tree = \Drupal::service('menu_link.tree');
  $tree = $menu_tree->buildPageData($menu_name, $level + 1);

  // Go down the active trail until the right level is reached.
  while ($level-- > 0 && $tree) {
@@ -1361,6 +775,9 @@ function menu_set_active_item($path) {
function menu_set_active_trail($new_trail = NULL) {
  $trail = &drupal_static(__FUNCTION__);

  /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
  $menu_tree = \Drupal::service('menu_link.tree');

  if (isset($new_trail)) {
    $trail = $new_trail;
  }
@@ -1384,7 +801,7 @@ function menu_set_active_trail($new_trail = NULL) {
      // Pass TRUE for $only_active_trail to make menu_tree_page_data() build
      // a stripped down menu tree containing the active trail only, in case
      // the given menu has not been built in this request yet.
      $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE);
      $tree = $menu_tree->buildPageData($preferred_link['menu_name'], NULL, TRUE);
      list($key, $curr) = each($tree);
    }
    // There is no link for the current path.
+15 −7

File changed.

Preview size limit exceeded, changes collapsed.

+4 −1
Changes for core/modules/menu/menu.module: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -263,10 +263,13 @@ function _menu_get_options($menus, $available_menus, $item) {
    $limit = _menu_parent_depth_limit($item);
  }

  /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
  $menu_tree = \Drupal::service('menu_link.tree');

  $options = array();
  foreach ($menus as $menu_name => $title) {
    if (isset($available_menus[$menu_name])) {
      $tree = menu_tree_all_data($menu_name, NULL);
      $tree = $menu_tree->buildAllData($menu_name, NULL);
      $options[$menu_name . ':0'] = '<' . $title . '>';
      _menu_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit);
    }
+606 −0

File added.

Preview size limit exceeded, changes collapsed.

+174 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading