summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcatch2014-03-26 19:56:01 (GMT)
committercatch2014-03-26 19:56:01 (GMT)
commit4a57034dd94fca90883cfbe1f02196ef5bac01a5 (patch)
treea314d9bc90ccac5aff30cb91670907db83aff5a8
parent1a51606bee25b523610c325df1975eff2076e07d (diff)
Issue #2207893 by dawehner, pwolanin, jessebeach, Boobaa: Convert menu tree building to a service.
-rw-r--r--core/includes/menu.inc597
-rw-r--r--core/modules/menu/lib/Drupal/menu/MenuFormController.php22
-rw-r--r--core/modules/menu/menu.module5
-rw-r--r--core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php606
-rw-r--r--core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php174
-rw-r--r--core/modules/menu_link/menu_link.services.yml4
-rw-r--r--core/modules/menu_link/tests/Drupal/menu_link/Tests/MenuTreeTest.php387
-rw-r--r--core/modules/shortcut/shortcut.module2
-rw-r--r--core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php44
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php116
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php68
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php77
-rw-r--r--core/modules/system/tests/modules/menu_test/menu_test.module4
-rw-r--r--core/modules/toolbar/toolbar.module15
-rw-r--r--core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php4
15 files changed, 1256 insertions, 869 deletions
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index b00965f..987d9d0 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -322,594 +322,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().
*/
function template_preprocess_menu_tree(&$variables) {
@@ -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.
diff --git a/core/modules/menu/lib/Drupal/menu/MenuFormController.php b/core/modules/menu/lib/Drupal/menu/MenuFormController.php
index d6720a7..fd3f123 100644
--- a/core/modules/menu/lib/Drupal/menu/MenuFormController.php
+++ b/core/modules/menu/lib/Drupal/menu/MenuFormController.php
@@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityFormController;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Language\Language;
use Drupal\menu_link\MenuLinkStorageControllerInterface;
+use Drupal\menu_link\MenuTreeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -34,6 +35,13 @@ class MenuFormController extends EntityFormController {
protected $menuLinkStorage;
/**
+ * The menu tree service.
+ *
+ * @var \Drupal\menu_link\MenuTreeInterface
+ */
+ protected $menuTree;
+
+ /**
* The overview tree form.
*
* @var array
@@ -47,10 +55,13 @@ class MenuFormController extends EntityFormController {
* The factory for entity queries.
* @param \Drupal\menu_link\MenuLinkStorageControllerInterface $menu_link_storage
* The menu link storage controller.
+ * @param \Drupal\menu_link\MenuTreeInterface $menu_tree
+ * The menu tree service.
*/
- public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageControllerInterface $menu_link_storage) {
+ public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageControllerInterface $menu_link_storage, MenuTreeInterface $menu_tree) {
$this->entityQueryFactory = $entity_query_factory;
$this->menuLinkStorage = $menu_link_storage;
+ $this->menuTree = $menu_tree;
}
/**
@@ -59,7 +70,8 @@ class MenuFormController extends EntityFormController {
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.query'),
- $container->get('entity.manager')->getStorageController('menu_link')
+ $container->get('entity.manager')->getStorageController('menu_link'),
+ $container->get('menu_link.tree')
);
}
@@ -256,13 +268,9 @@ class MenuFormController extends EntityFormController {
}
$delta = max(count($links), 50);
- $tree = menu_tree_data($links);
- $node_links = array();
- menu_tree_collect_node_links($tree, $node_links);
-
// We indicate that a menu administrator is running the menu access check.
$this->getRequest()->attributes->set('_menu_admin', TRUE);
- menu_tree_check_access($tree, $node_links);
+ $tree = $this->menuTree->buildTreeData($links);
$this->getRequest()->attributes->set('_menu_admin', FALSE);
$form = array_merge($form, $this->buildOverviewTreeForm($tree, $delta));
diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module
index bf90079..245e888 100644
--- a/core/modules/menu/menu.module
+++ b/core/modules/menu/menu.module
@@ -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);
}
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
new file mode 100644
index 0000000..b0a69f6
--- /dev/null
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
@@ -0,0 +1,606 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\MenuTree.
+ */
+
+namespace Drupal\menu_link;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\Core\KeyValueStore\StateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Provides the default implementation of a menu tree.
+ */
+class MenuTree implements MenuTreeInterface {
+
+ /**
+ * The database connection.
+ *
+ * @var \Drupal\Core\Database\Connection
+ * The database connection.
+ */
+ protected $database;
+
+ /**
+ * The cache backend.
+ *
+ * @var \Drupal\Core\Cache\CacheBackendInterface
+ */
+ protected $cache;
+
+ /**
+ * The language manager.
+ *
+ * @var \Drupal\Core\Language\LanguageManagerInterface
+ */
+ protected $languageManager;
+
+ /**
+ * The request stack.
+ *
+ * @var \Symfony\Component\HttpFoundation\RequestStack
+ */
+ protected $requestStack;
+
+ /**
+ * The menu link storage controller.
+ *
+ * @var \Drupal\menu_link\MenuLinkStorageControllerInterface
+ */
+ protected $menuLinkStorage;
+
+ /**
+ * The entity query factory.
+ *
+ * @var \Drupal\Core\Entity\Query\QueryFactory
+ */
+ protected $queryFactory;
+
+ /**
+ * The state.
+ *
+ * @var \Drupal\Core\KeyValueStore\StateInterface
+ */
+ protected $state;
+
+ /**
+ * A list of active trail paths keyed by $menu_name.
+ *
+ * @var array
+ */
+ protected $trailPaths;
+
+ /**
+ * Stores the rendered menu output keyed by $menu_name.
+ *
+ * @var array
+ */
+ protected $menuOutput;
+
+ /**
+ * Stores the menu tree used by the doBuildTree method, keyed by a cache ID.
+ *
+ * This cache ID is built using the $menu_name, the current language and
+ * some parameters passed into an entity query.
+ */
+ protected $menuTree;
+
+ /**
+ * Stores the full menu tree data keyed by a cache ID.
+ *
+ * This variable distinct from static::$menuTree by having also items without
+ * access by the current user.
+ *
+ * This cache ID is built with the menu name, a passed in root link ID, the
+ * current language as well as the maximum depth.
+ *
+ * @var array
+ */
+ protected $menuFullTrees;
+
+ /**
+ * Stores the menu tree data on the current page keyed by a cache ID.
+ *
+ * This contains less information than a tree built with buildAllData.
+ *
+ * @var array
+ */
+ protected $menuPageTrees;
+
+ /**
+ * Constructs a new MenuTree.
+ *
+ * @param \Drupal\Core\Database\Connection $database
+ * The database connection.
+ * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+ * The cache backend.
+ * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * The language manager.
+ * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+ * The request stack.
+ * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+ * The entity manager.
+ * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query_factory
+ * The entity query factory.
+ * @param \Drupal\Core\KeyValueStore\StateInterface $state
+ * The state.
+ */
+ public function __construct(Connection $database, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager, RequestStack $request_stack, EntityManagerInterface $entity_manager, QueryFactory $entity_query_factory, StateInterface $state) {
+ $this->database = $database;
+ $this->cache = $cache_backend;
+ $this->languageManager = $language_manager;
+ $this->requestStack = $request_stack;
+ $this->menuLinkStorage = $entity_manager->getStorageController('menu_link');
+ $this->queryFactory = $entity_query_factory;
+ $this->state = $state;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildAllData($menu_name, $link = NULL, $max_depth = NULL) {
+ $language_interface = $this->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($this->menuFullTrees[$cid])) {
+ // If the static variable doesn't have the data, check {cache_menu}.
+ $cache = $this->cache->get($cid);
+ if ($cache && $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.
+ $this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
+ }
+
+ // Build the tree using the parameters; the resulting tree will be cached
+ // by $this->doBuildTree()).
+ $this->menuFullTrees[$cid] = $this->buildTree($menu_name, $tree_parameters);
+ }
+
+ return $this->menuFullTrees[$cid];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
+ $language_interface = $this->languageManager->getCurrentLanguage();
+
+ // Check if the active trail has been overridden for this menu tree.
+ $active_path = $this->getPath($menu_name);
+ // Load the request corresponding to the current page.
+ $request = $this->requestStack->getCurrentRequest();
+ $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($this->menuPageTrees[$cid]) && $only_active_trail) {
+ $cid .= ':trail';
+ }
+
+ if (!isset($this->menuPageTrees[$cid])) {
+ // If the static variable doesn't have the data, check {cache_menu}.
+ $cache = $this->cache->get($cid);
+ if ($cache && $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 = $this->menuLinkGetPreferred($menu_name, $active_path)) {
+ // 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 = $this->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 = $this->queryFactory->get('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.
+ $this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
+ }
+
+ // Build the tree using the parameters; the resulting tree will be
+ // cached by $tihs->buildTree().
+ $this->menuPageTrees[$cid] = $this->buildTree($menu_name, $tree_parameters);
+ }
+ return $this->menuPageTrees[$cid];
+ }
+
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setPath($menu_name, $path = NULL) {
+ if (isset($path)) {
+ $this->trailPaths[$menu_name] = $path;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPath($menu_name) {
+ return isset($this->trailPaths[$menu_name]) ? $this->trailPaths[$menu_name] : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderMenu($menu_name) {
+
+ if (!isset($this->menuOutput[$menu_name])) {
+ $tree = $this->buildPageData($menu_name);
+ $this->menuOutput[$menu_name] = $this->renderTree($tree);
+ }
+ return $this->menuOutput[$menu_name];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderTree($tree) {
+ $build = array();
+ $items = array();
+ $menu_name = $tree ? end($tree)['link']['menu_name'] : '';
+
+ // 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'] ? $this->renderTree($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($menu_name, '-', '_');
+ // Set cache tag.
+ $menu_name = $data['link']['menu_name'];
+ $build['#cache']['tags']['menu'][$menu_name] = $menu_name;
+ }
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildTree($menu_name, array $parameters = array()) {
+ // Build the menu tree.
+ $tree = $this->doBuildTree($menu_name, $parameters);
+ // Check access for the current user to each item in the tree.
+ $this->checkAccess($tree);
+ return $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.
+ *
+ * @param string $menu_name
+ * The name of the menu.
+ * @param array $parameters
+ * The parameters passed into static::buildTree()
+ *
+ * @see static::buildTree()
+ */
+ protected function doBuildTree($menu_name, array $parameters = array()) {
+ $language_interface = $this->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_menu}.
+ if (!isset($this->menuTree[$tree_cid])) {
+ $cache = $this->cache->get($tree_cid);
+ if ($cache && $cache->data) {
+ $this->menuFullTrees[$tree_cid] = $cache->data;
+ }
+ }
+
+ if (!isset($this->menuTree[$tree_cid])) {
+ $query = $this->queryFactory->get('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 = $this->menuLinkStorage->loadMultiple($result);
+ }
+ $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
+ $tree = $this->doBuildTreeData($links, $active_trail, $min_depth);
+
+ // Cache the data, if it is not already in the cache.
+ $this->cache->set($tree_cid, $tree, Cache::PERMANENT, array('menu' => $menu_name));
+ $this->menuTree[$tree_cid] = $tree;
+ }
+
+ return $this->menuTree[$tree_cid];
+ }
+
+ /**
+ * Sorts the menu tree and recursively checks access for each item.
+ *
+ * @param array $tree
+ * The menu tree you wish to operate on.
+ */
+ protected function checkAccess(&$tree) {
+ $new_tree = array();
+ foreach ($tree as $key => $v) {
+ $item = &$tree[$key]['link'];
+ $this->menuLinkTranslate($item);
+ if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) {
+ if ($tree[$key]['below']) {
+ $this->checkAccess($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;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildTreeData(array $links, array $parents = array(), $depth = 1) {
+ $tree = $this->doBuildTreeData($links, $parents, $depth);
+ $this->checkAccess($tree);
+ return $tree;
+ }
+
+ /**
+ * Prepares the data for calling $this->treeDataRecursive().
+ */
+ protected function doBuildTreeData(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 $this->treeDataRecursive($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.
+ *
+ * @param array $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 array $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 int $depth
+ * The minimum depth to include in the returned menu tree.
+ *
+ * @return array
+ */
+ protected function treeDataRecursive(&$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 doBuildTreeData to build the sub-tree.
+ $tree[$item['mlid']]['below'] = $this->treeDataRecursive($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;
+ }
+
+ /**
+ * Wraps menu_link_get_preferred().
+ */
+ protected function menuLinkGetPreferred($menu_name, $active_path) {
+ return menu_link_get_preferred($active_path, $menu_name);
+ }
+
+ /**
+ * Wraps _menu_link_translate().
+ */
+ protected function menuLinkTranslate(&$item) {
+ _menu_link_translate($item);
+ }
+
+}
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
new file mode 100644
index 0000000..e4cd83d
--- /dev/null
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
@@ -0,0 +1,174 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\MenuTreeInterface.
+ */
+
+namespace Drupal\menu_link;
+
+/**
+ * Defines an interface for trees out of menu links.
+ */
+interface MenuTreeInterface {
+
+ /**
+ * Returns a rendered 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 array $tree
+ * A data structure representing the tree as returned from menu_tree_data.
+ *
+ * @return array
+ * A structured array to be rendered by drupal_render().
+ */
+ public function renderTree($tree);
+
+ /**
+ * 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 static::setPath(), the preferred link will be
+ * overridden by the corresponding path returned by static::getPath().
+ *
+ * Setting this path does not affect the main content; for that use
+ * menu_set_active_item() instead.
+ *
+ * @param string $menu_name
+ * The name of the affected menu tree.
+ * @param string $path
+ * The path to use when finding the active trail.
+ */
+ public function setPath($menu_name, $path = NULL);
+
+ /**
+ * Gets the path for determining the active trail of the specified menu tree.
+ *
+ * @param string $menu_name
+ * The menu name of the requested tree.
+ *
+ * @return string
+ * A string containing the path. If no path has been specified with
+ * static::setPath(), NULL is returned.
+ */
+ public function getPath($menu_name);
+
+ /**
+ * Sorts and returns the built data representing a menu tree.
+ *
+ * @param array $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 array $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 int $depth
+ * The minimum depth to include in the returned menu tree.
+ *
+ * @return array
+ * 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.
+ */
+ public function buildTreeData(array $links, array $parents = array(), $depth = 1);
+
+ /**
+ * 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 string $menu_name
+ * The named menu links to return.
+ * @param int $max_depth
+ * (optional) The maximum depth of links to retrieve.
+ * @param bool $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 array
+ * 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.
+ */
+ public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE);
+
+ /**
+ * 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 string $menu_name
+ * The named menu links to return
+ * @param array $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 int $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 array
+ * An tree of menu links in an array, in the order they should be rendered.
+ */
+ public function buildAllData($menu_name, $link = NULL, $max_depth = NULL);
+
+ /**
+ * Renders a menu tree based on the current path.
+ *
+ * @param string $menu_name
+ * The name of the menu.
+ *
+ * @return array
+ * A structured array representing the specified menu on the current page,
+ * to be rendered by drupal_render().
+ */
+ public function renderMenu($menu_name);
+
+ /**
+ * Builds a menu tree, translates links, and checks access.
+ *
+ * @param string $menu_name
+ * The name of the menu.
+ * @param array $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 array
+ * A fully built menu tree.
+ */
+ public function buildTree($menu_name, array $parameters = array());
+
+}
diff --git a/core/modules/menu_link/menu_link.services.yml b/core/modules/menu_link/menu_link.services.yml
new file mode 100644
index 0000000..5793fa0
--- /dev/null
+++ b/core/modules/menu_link/menu_link.services.yml
@@ -0,0 +1,4 @@
+services:
+ menu_link.tree:
+ class: Drupal\menu_link\MenuTree
+ arguments: ['@database', '@cache.data', '@language_manager', '@request_stack', '@entity.manager', '@entity.query', '@state']
diff --git a/core/modules/menu_link/tests/Drupal/menu_link/Tests/MenuTreeTest.php b/core/modules/menu_link/tests/Drupal/menu_link/Tests/MenuTreeTest.php
new file mode 100644
index 0000000..7e5b2ed
--- /dev/null
+++ b/core/modules/menu_link/tests/Drupal/menu_link/Tests/MenuTreeTest.php
@@ -0,0 +1,387 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\Tests\MenuTreeTest.
+ */
+
+namespace Drupal\menu_link\Tests;
+
+use Drupal\menu_link\MenuTree;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Tests the menu tree.
+ *
+ * @group Drupal
+ * @group menu_link
+ *
+ * @coversDefaultClass \Drupal\menu_link\MenuTree
+ */
+class MenuTreeTest extends UnitTestCase {
+
+ /**
+ * The tested menu tree.
+ *
+ * @var \Drupal\menu_link\MenuTree|\Drupal\menu_link\Tests\TestMenuTree
+ */
+ protected $menuTree;
+
+ /**
+ * The mocked database connection.
+ *
+ * @var \Drupal\Core\DatabaseConnection|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $connection;
+
+ /**
+ * The mocked cache backend.
+ *
+ * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $cacheBackend;
+
+ /**
+ * The mocked language manager.
+ *
+ * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $languageManager;
+
+ /**
+ * The test request stack.
+ *
+ * @var \Symfony\Component\HttpFoundation\RequestStack.
+ */
+ protected $requestStack;
+
+ /**
+ * The mocked entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $entityManager;
+
+ /**
+ * The mocked entity query factor.y
+ *
+ * @var \Drupal\Core\Entity\Query\QueryFactory|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $entityQueryFactory;
+
+ /**
+ * The mocked state.
+ *
+ * @var \Drupal\Core\KeyValueStore\StateInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $state;
+
+ /**
+ * Stores some default values for a menu link.
+ *
+ * @var array
+ */
+ protected $defaultMenuLink = array(
+ 'menu_name' => 'main-menu',
+ 'mlid' => 1,
+ 'title' => 'Example 1',
+ 'route_name' => 'example1',
+ 'link_path' => 'example1',
+ 'access' => 1,
+ 'hidden' => FALSE,
+ 'has_children' => FALSE,
+ 'in_active_trail' => TRUE,
+ 'localized_options' => array('attributes' => array('title' => '')),
+ 'weight' => 0,
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Tests \Drupal\menu_link\MenuTree',
+ 'description' => '',
+ 'group' => 'Menu',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
+ $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
+ $this->requestStack = new RequestStack();
+ $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+ $this->entityQueryFactory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->state = $this->getMock('Drupal\Core\KeyValueStore\StateInterface');
+
+ $this->menuTree = new TestMenuTree($this->connection, $this->cacheBackend, $this->languageManager, $this->requestStack, $this->entityManager, $this->entityQueryFactory, $this->state);
+ }
+
+ /**
+ * Tests active paths.
+ *
+ * @covers ::setPath
+ * @covers ::getPath
+ */
+ public function testActivePaths() {
+ $this->assertNull($this->menuTree->getPath('test_menu1'));
+
+ $this->menuTree->setPath('test_menu1', 'example_path1');
+ $this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1'));
+ $this->assertNull($this->menuTree->getPath('test_menu2'));
+
+ $this->menuTree->setPath('test_menu2', 'example_path2');
+ $this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1'));
+ $this->assertEquals('example_path2', $this->menuTree->getPath('test_menu2'));
+ }
+
+ /**
+ * Tests buildTreeData with a single level.
+ *
+ * @covers ::buildTreeData
+ * @covers ::doBuildTreeData
+ */
+ public function testBuildTreeDataWithSingleLevel() {
+ $items = array();
+ $items[] = array(
+ 'mlid' => 1,
+ 'depth' => 1,
+ 'weight' => 0,
+ 'title' => '',
+ 'route_name' => 'example1',
+ 'access' => TRUE,
+ );
+ $items[] = array(
+ 'mlid' => 2,
+ 'depth' => 1,
+ 'weight' => 0,
+ 'title' => '',
+ 'route_name' => 'example2',
+ 'access' => TRUE,
+ );
+
+ $result = $this->menuTree->buildTreeData($items, array(), 1);
+
+ $this->assertCount(2, $result);
+ $result1 = array_shift($result);
+ $this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']);
+ $result2 = array_shift($result);
+ $this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
+ }
+
+ /**
+ * Tests buildTreeData with a single level and one item being active.
+ *
+ * @covers ::buildTreeData
+ * @covers ::doBuildTreeData
+ */
+ public function testBuildTreeDataWithSingleLevelAndActiveItem() {
+ $items = array();
+ $items[] = array(
+ 'mlid' => 1,
+ 'depth' => 1,
+ 'weight' => 0,
+ 'title' => '',
+ 'route_name' => 'example1',
+ 'access' => TRUE,
+ );
+ $items[] = array(
+ 'mlid' => 2,
+ 'depth' => 1,
+ 'weight' => 0,
+ 'title' => '',
+ 'route_name' => 'example2',
+ 'access' => TRUE,
+ );
+
+ $result = $this->menuTree->buildTreeData($items, array(1), 1);
+
+ $this->assertCount(2, $result);
+ $result1 = array_shift($result);
+ $this->assertEquals($items[0] + array('in_active_trail' => TRUE), $result1['link']);
+ $result2 = array_shift($result);
+ $this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
+ }
+
+ /**
+ * Tests buildTreeData with a single level and none item being active.
+ *
+ * @covers ::buildTreeData
+ * @covers ::doBuildTreeData
+ */
+ public function testBuildTreeDataWithSingleLevelAndNoActiveItem() {
+ $items = array();
+ $items[] = array(
+ 'mlid' => 1,
+ 'depth' => 1,
+ 'weight' => 0,
+ 'title' => '',
+ 'route_name' => 'example1',
+ 'access' => TRUE,
+ );
+ $items[] = array(
+ 'mlid' => 2,
+ 'depth' => 1,
+ 'weight' => 0,
+ 'title' => '',
+ 'route_name' => 'example2',
+ 'access' => TRUE,
+ );
+
+ $result = $this->menuTree->buildTreeData($items, array(3), 1);
+
+ $this->assertCount(2, $result);
+ $result1 = array_shift($result);
+ $this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']);
+ $result2 = array_shift($result);
+ $this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
+ }
+
+ /**
+ * Tests buildTreeData with a more complex example.
+ *
+ * @covers ::buildTreeData
+ * @covers ::doBuildTreeData
+ */
+ public function testBuildTreeWithComplexData() {
+ $items = array(
+ 1 => array('mlid' => 1, 'depth' => 1, 'route_name' => 'example1', 'access' => TRUE, 'weight' => 0, 'title' => ''),
+ 2 => array('mlid' => 2, 'depth' => 1, 'route_name' => 'example2', 'access' => TRUE, 'weight' => 0, 'title' => ''),
+ 3 => array('mlid' => 3, 'depth' => 2, 'route_name' => 'example3', 'access' => TRUE, 'weight' => 0, 'title' => ''),
+ 4 => array('mlid' => 4, 'depth' => 3, 'route_name' => 'example4', 'access' => TRUE, 'weight' => 0, 'title' => ''),
+ 5 => array('mlid' => 5, 'depth' => 1, 'route_name' => 'example5', 'access' => TRUE, 'weight' => 0, 'title' => ''),
+ );
+
+ $tree = $this->menuTree->buildTreeData($items);
+
+ // Validate that parent items #1, #2, and #5 exist on the root level.
+ $this->assertEquals($items[1]['mlid'], $tree['50000 1']['link']['mlid']);
+ $this->assertEquals($items[2]['mlid'], $tree['50000 2']['link']['mlid']);
+ $this->assertEquals($items[5]['mlid'], $tree['50000 5']['link']['mlid']);
+
+ // Validate that child item #4 exists at the correct location in the hierarchy.
+ $this->assertEquals($items[4]['mlid'], $tree['50000 2']['below']['50000 3']['below']['50000 4']['link']['mlid']);
+ }
+
+ /**
+ * Tests the output with a single level.
+ *
+ * @covers ::output
+ */
+ public function testOutputWithSingleLevel() {
+ $tree = array(
+ '1' => array(
+ 'link' => array('mlid' => 1) + $this->defaultMenuLink,
+ 'below' => array(),
+ ),
+ '2' => array(
+ 'link' => array('mlid' => 2) + $this->defaultMenuLink,
+ 'below' => array(),
+ ),
+ );
+
+ $output = $this->menuTree->renderTree($tree);
+
+ // Validate that the - in main-menu is changed into an underscore
+ $this->assertEquals($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link');
+ $this->assertEquals($output['2']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link');
+ $this->assertEquals($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper');
+ }
+
+ /**
+ * Tests the output method with a complex example.
+ *
+ * @covers ::output
+ */
+ public function testOutputWithComplexData() {
+ $tree = array(
+ '1'=> array(
+ 'link' => array('mlid' => 1, 'has_children' => 1, 'title' => 'Item 1', 'link_path' => 'a') + $this->defaultMenuLink,
+ 'below' => array(
+ '2' => array('link' => array('mlid' => 2, 'title' => 'Item 2', 'link_path' => 'a/b') + $this->defaultMenuLink,
+ 'below' => array(
+ '3' => array('link' => array('mlid' => 3, 'title' => 'Item 3', 'in_active_trail' => 0, 'link_path' => 'a/b/c') + $this->defaultMenuLink,
+ 'below' => array()),
+ '4' => array('link' => array('mlid' => 4, 'title' => 'Item 4', 'in_active_trail' => 0, 'link_path' => 'a/b/d') + $this->defaultMenuLink,
+ 'below' => array())
+ )
+ )
+ )
+ ),
+ '5' => array('link' => array('mlid' => 5, 'hidden' => 1, 'title' => 'Item 5', 'link_path' => 'e') + $this->defaultMenuLink, 'below' => array()),
+ '6' => array('link' => array('mlid' => 6, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'link_path' => 'f') + $this->defaultMenuLink, 'below' => array()),
+ '7' => array('link' => array('mlid' => 7, 'title' => 'Item 7', 'in_active_trail' => 0, 'link_path' => 'g') + $this->defaultMenuLink, 'below' => array())
+ );
+
+ $output = $this->menuTree->renderTree($tree);
+
+ // Looking for child items in the data
+ $this->assertEquals( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item');
+ $this->assertTrue(in_array('active-trail', $output['1']['#below']['2']['#attributes']['class']), 'Checking the active trail class');
+ // Validate that the hidden and no access items are missing
+ $this->assertFalse(isset($output['5']), 'Hidden item should be missing');
+ $this->assertFalse(isset($output['6']), 'False access should be missing');
+ // Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are
+ // skipped and 7 still included.
+ $this->assertTrue(isset($output['7']), 'Item after hidden items is present');
+ }
+
+ /**
+ * Tests menu tree access check with a single level.
+ *
+ * @covers ::checkAccess
+ */
+ public function testCheckAccessWithSingleLevel() {
+ $items = array(
+ array('mlid' => 1, 'route_name' => 'menu_test_1', 'depth' => 1, 'link_path' => 'menu_test/test_1', 'in_active_trail' => FALSE) + $this->defaultMenuLink,
+ array('mlid' => 2, 'route_name' => 'menu_test_2', 'depth' => 1, 'link_path' => 'menu_test/test_2', 'in_active_trail' => FALSE) + $this->defaultMenuLink,
+ );
+
+ // Register a menuLinkTranslate to mock the access.
+ $this->menuTree->menuLinkTranslateCallable = function(&$item) {
+ $item['access'] = $item['mlid'] == 1;
+ };
+
+ // Build the menu tree and check access for all of the items.
+ $tree = $this->menuTree->buildTreeData($items);
+
+ $this->assertCount(1, $tree);
+ $item = reset($tree);
+ $this->assertEquals($items[0], $item['link']);
+ }
+
+}
+
+class TestMenuTree extends MenuTree {
+
+ /**
+ * An alternative callable used for menuLinkTranslate.
+ * @var callable
+ */
+ public $menuLinkTranslateCallable;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function menuLinkTranslate(&$item) {
+ if (isset($this->menuLinkTranslateCallable)) {
+ call_user_func_array($this->menuLinkTranslateCallable, array(&$item));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function menuLinkGetPreferred($menu_name, $active_path) {
+ }
+
+}
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 8cd6333..c3d4197 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -304,8 +304,6 @@ function shortcut_valid_link($path) {
*
* @return \Drupal\shortcut\ShortcutInterface[]
* An array of shortcut links, in the format returned by the menu system.
- *
- * @see menu_tree()
*/
function shortcut_renderable_links($shortcut_set = NULL) {
$shortcut_links = array();
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
index 15cb1d8..32a84d7 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
@@ -9,6 +9,10 @@ namespace Drupal\system\Plugin\Block;
use Drupal\Component\Utility\NestedArray;
use Drupal\block\BlockBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\menu_link\MenuTreeInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
/**
* Provides a generic Menu block.
@@ -20,14 +24,50 @@ use Drupal\block\BlockBase;
* derivative = "Drupal\system\Plugin\Derivative\SystemMenuBlock"
* )
*/
-class SystemMenuBlock extends BlockBase {
+class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * The menu tree.
+ *
+ * @var \Drupal\menu_link\MenuTreeInterface
+ */
+ protected $menuTree;
+
+ /**
+ * Constructs a new SystemMenuBlock.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin_id for the plugin instance.
+ * @param array $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\menu_link\MenuTreeInterface $menu_tree
+ * The menu tree.
+ */
+ public function __construct(array $configuration, $plugin_id, array $plugin_definition, MenuTreeInterface $menu_tree) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->menuTree = $menu_tree;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('menu_link.tree')
+ );
+ }
/**
* {@inheritdoc}
*/
public function build() {
$menu = $this->getDerivativeId();
- return menu_tree($menu);
+ return $this->menuTree->renderMenu($menu);
}
/**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php
deleted file mode 100644
index 974208f..0000000
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\system\Tests\Menu\TreeAccessTest.
- */
-
-namespace Drupal\system\Tests\Menu;
-
-use Drupal\menu_link\Entity\MenuLink;
-use Drupal\simpletest\DrupalUnitTestBase;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-
-/**
- * Tests the access check for menu tree using both menu links and route items.
- */
-
-class TreeAccessTest extends DrupalUnitTestBase {
-
- /**
- * A list of menu links used for this test.
- *
- * @var array
- */
- protected $links;
-
- /**
- * The route collection used for this test.
- *
- * @var\ \Symfony\Component\Routing\RouteCollection
- */
- protected $routeCollection;
-
- /**
- * Modules to enable.
- *
- * @var array
- */
- public static $modules = array('menu_link');
-
- public static function getInfo() {
- return array(
- 'name' => 'Menu tree access',
- 'description' => 'Tests the access check for menu tree using both menu links and route items.',
- 'group' => 'Menu',
- );
- }
-
- /**
- * Overrides \Drupal\simpletest\DrupalUnitTestBase::containerBuild().
- */
- public function containerBuild(ContainerBuilder $container) {
- parent::containerBuild($container);
-
- $route_collection = $this->getTestRouteCollection();
-
- $container->register('router.route_provider', 'Drupal\system\Tests\Routing\MockRouteProvider')
- ->addArgument($route_collection);
- }
-
- /**
- * Generates the test route collection.
- *
- * @return \Symfony\Component\Routing\RouteCollection
- * Returns the test route collection.
- */
- protected function getTestRouteCollection() {
- if (!isset($this->routeCollection)) {
- $route_collection = new RouteCollection();
- $route_collection->add('menu_test_1', new Route('/menu_test/test_1',
- array(
- '_controller' => '\Drupal\menu_test\TestController::test'
- ),
- array(
- '_access' => 'TRUE'
- )
- ));
- $route_collection->add('menu_test_2', new Route('/menu_test/test_2',
- array(
- '_controller' => '\Drupal\menu_test\TestController::test'
- ),
- array(
- '_access' => 'FALSE'
- )
- ));
- $this->routeCollection = $route_collection;
- }
-
- return $this->routeCollection;
- }
-
- /**
- * Tests access check for menu links with a route item.
- */
- public function testRouteItemMenuLinksAccess() {
- // Add the access checkers to the route items.
- $this->container->get('access_manager')->setChecks($this->getTestRouteCollection());
-
- // Setup the links with the route items.
- $this->links = array(
- new MenuLink(array('mlid' => 1, 'route_name' => 'menu_test_1', 'depth' => 1, 'link_path' => 'menu_test/test_1'), 'menu_link'),
- new MenuLink(array('mlid' => 2, 'route_name' => 'menu_test_2', 'depth' => 1, 'link_path' => 'menu_test/test_2'), 'menu_link'),
- );
-
- // Build the menu tree and check access for all of the items.
- $tree = menu_tree_data($this->links);
- menu_tree_check_access($tree);
-
- $this->assertEqual(count($tree), 1, 'Ensure that just one menu link got access.');
- $item = reset($tree);
- $this->assertEqual($this->links[0], $item['link'], 'Ensure that the right link got access');
- }
-
-}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php
deleted file mode 100644
index 8b6c4a1..0000000
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\system\Tests\Menu\TreeDataUnitTest.
- */
-
-namespace Drupal\system\Tests\Menu;
-
-use Drupal\menu_link\Entity\MenuLink;
-use Drupal\simpletest\UnitTestBase;
-
-/**
- * Menu tree data related tests.
- */
-class TreeDataUnitTest extends UnitTestBase {
- /**
- * Dummy link structure acceptable for menu_tree_data().
- */
- protected $links = array();
-
- public static function getInfo() {
- return array(
- 'name' => 'Menu tree generation',
- 'description' => 'Tests recursive menu tree generation functions.',
- 'group' => 'Menu',
- );
- }
-
- /**
- * Validate the generation of a proper menu tree hierarchy.
- */
- public function testMenuTreeData() {
- $this->links = array(
- 1 => new MenuLink(array('mlid' => 1, 'depth' => 1), 'menu_link'),
- 2 => new MenuLink(array('mlid' => 2, 'depth' => 1), 'menu_link'),
- 3 => new MenuLink(array('mlid' => 3, 'depth' => 2), 'menu_link'),
- 4 => new MenuLink(array('mlid' => 4, 'depth' => 3), 'menu_link'),
- 5 => new MenuLink(array('mlid' => 5, 'depth' => 1), 'menu_link'),
- );
-
- $tree = menu_tree_data($this->links);
-
- // Validate that parent items #1, #2, and #5 exist on the root level.
- $this->assertSameLink($this->links[1], $tree[1]['link'], 'Parent item #1 exists.');
- $this->assertSameLink($this->links[2], $tree[2]['link'], 'Parent item #2 exists.');
- $this->assertSameLink($this->links[5], $tree[5]['link'], 'Parent item #5 exists.');
-
- // Validate that child item #4 exists at the correct location in the hierarchy.
- $this->assertSameLink($this->links[4], $tree[2]['below'][3]['below'][4]['link'], 'Child item #4 exists in the hierarchy.');
- }
-
- /**
- * Check that two menu links are the same by comparing the mlid.
- *
- * @param $link1
- * A menu link item.
- * @param $link2
- * A menu link item.
- * @param $message
- * The message to display along with the assertion.
- * @return
- * TRUE if the assertion succeeded, FALSE otherwise.
- */
- protected function assertSameLink($link1, $link2, $message = '') {
- return $this->assert($link1['mlid'] == $link2['mlid'], $message ?: 'First link is identical to second link');
- }
-}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php
deleted file mode 100644
index bc80cb5..0000000
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\system\Tests\Menu\TreeOutputTest.
- */
-
-namespace Drupal\system\Tests\Menu;
-
-use Drupal\simpletest\DrupalUnitTestBase;
-
-/**
- * Menu tree output related tests.
- */
-class TreeOutputTest extends DrupalUnitTestBase {
-
- public static $modules = array('system', 'menu_link', 'field');
-
- /**
- * Dummy link structure acceptable for menu_tree_output().
- */
- protected $tree_data = array();
-
- public static function getInfo() {
- return array(
- 'name' => 'Menu tree output',
- 'description' => 'Tests menu tree output functions.',
- 'group' => 'Menu',
- );
- }
-
- function setUp() {
- parent::setUp();
-
- $this->installSchema('system', array('router'));
- }
-
- /**
- * Validate the generation of a proper menu tree output.
- */
- function testMenuTreeData() {
- $storage_controller = $this->container->get('entity.manager')->getStorageController('menu_link');
- // @todo Prettify this tree buildup code, it's very hard to read.
- $this->tree_data = array(
- '1'=> array(
- 'link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 1, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access' => 1, 'link_path' => 'a', 'localized_options' => array('attributes' => array('title' =>'')))),
- 'below' => array(
- '2' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 2, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access' => 1, 'link_path' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')))),
- 'below' => array(
- '3' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 3, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')))),
- 'below' => array() ),
- '4' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 4, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')))),
- 'below' => array() )
- )
- )
- )
- ),
- '5' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 5, 'hidden' => 1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'e', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array()),
- '6' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 6, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'link_path' => 'f', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array()),
- '7' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 7, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'g', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array())
- );
-
- $output = menu_tree_output($this->tree_data);
-
- // Validate that the - in main-menu is changed into an underscore
- $this->assertEqual($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link');
- $this->assertEqual($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper');
- // Looking for child items in the data
- $this->assertEqual( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item');
- $this->assertTrue( in_array('active-trail',$output['1']['#below']['2']['#attributes']['class']) , 'Checking the active trail class');
- // Validate that the hidden and no access items are missing
- $this->assertFalse( isset($output['5']), 'Hidden item should be missing');
- $this->assertFalse( isset($output['6']), 'False access should be missing');
- // Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are skipped and 7 still included
- $this->assertTrue( isset($output['7']), 'Item after hidden items is present');
- }
-}
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index 4d31718..3f34a1e 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -223,8 +223,10 @@ function menu_test_callback() {
*/
function menu_test_menu_trail_callback() {
$menu_path = \Drupal::state()->get('menu_test.menu_tree_set_path') ?: array();
+ /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+ $menu_tree = \Drupal::service('menu_link.tree');
if (!empty($menu_path)) {
- menu_tree_set_path($menu_path['menu_name'], $menu_path['path']);
+ $menu_tree->setPath($menu_path['menu_name'], $menu_path['path']);
}
return 'This is menu_test_menu_trail_callback().';
}
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index feadc08c..29773f1 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -358,6 +358,9 @@ function toolbar_toolbar() {
// Add attributes to the links before rendering.
toolbar_menu_navigation_links($tree);
+ /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+ $menu_tree = \Drupal::service('menu_link.tree');
+
$menu = array(
'#heading' => t('Administration menu'),
'toolbar_administration' => array(
@@ -365,7 +368,7 @@ function toolbar_toolbar() {
'#attributes' => array(
'class' => array('toolbar-menu-administration'),
),
- 'administration_menu' => menu_tree_output($tree),
+ 'administration_menu' => $menu_tree->renderTree($tree),
),
);
@@ -415,6 +418,8 @@ function toolbar_toolbar() {
*/
function toolbar_get_menu_tree() {
$tree = array();
+ /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+ $menu_tree = \Drupal::service('menu_link.tree');
$query = \Drupal::entityQuery('menu_link')
->condition('menu_name', 'admin')
->condition('module', 'system')
@@ -422,7 +427,7 @@ function toolbar_get_menu_tree() {
$result = $query->execute();
if (!empty($result)) {
$admin_link = menu_link_load(reset($result));
- $tree = menu_build_tree('admin', array(
+ $tree = $menu_tree->buildTree('admin', array(
'expanded' => array($admin_link['mlid']),
'min_depth' => $admin_link['depth'] + 1,
'max_depth' => $admin_link['depth'] + 1,
@@ -465,6 +470,8 @@ function toolbar_menu_navigation_links(&$tree) {
*/
function toolbar_get_rendered_subtrees() {
$subtrees = array();
+ /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+ $menu_tree = \Drupal::service('menu_link.tree');
$tree = toolbar_get_menu_tree();
foreach ($tree as $tree_item) {
$item = $tree_item['link'];
@@ -476,9 +483,9 @@ function toolbar_get_rendered_subtrees() {
$query->condition('p' . $i, $item['p' . $i]);
}
$parents = $query->execute();
- $subtree = menu_build_tree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1));
+ $subtree = $menu_tree->buildTree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1));
toolbar_menu_navigation_links($subtree);
- $subtree = menu_tree_output($subtree);
+ $subtree = $menu_tree->renderTree($subtree);
$subtree = drupal_render($subtree);
}
else {
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php b/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php
index bbe5a71..c9dcd30 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php
@@ -67,7 +67,9 @@ class UserAccountLinksTests extends WebTestBase {
$this->drupalGet('<front>');
// For a logged-out user, expect no secondary links.
- $tree = menu_build_tree('account');
+ /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+ $menu_tree = \Drupal::service('menu_link.tree');
+ $tree = $menu_tree->buildTree('account');
$this->assertEqual(count($tree), 1, 'The secondary links menu contains only one menu link.');
$link = reset($tree);
$link = $link['link'];