core/core.services.yml | 2 +- core/includes/menu.inc | 31 ++- core/lib/Drupal/Core/Menu/MenuLinkTree.php | 214 +++++++------------- .../lib/Drupal/Core/Menu/MenuLinkTreeInterface.php | 195 ++++++++---------- .../Drupal/Core/Menu/MenuLinkTreeParameters.php | 219 ++++++++++++++++++++ .../Drupal/Core/Menu/MenuParentFormSelector.php | 5 +- core/lib/Drupal/Core/Menu/MenuTreeStorage.php | 220 +++++++++++---------- .../Drupal/Core/Menu/MenuTreeStorageInterface.php | 20 +- core/modules/menu_ui/src/MenuForm.php | 4 +- .../system/src/Controller/SystemController.php | 6 +- .../system/src/Plugin/Block/SystemMenuBlock.php | 6 +- core/modules/system/src/SystemManager.php | 8 +- .../system/src/Tests/Menu/MenuLinkTreeTest.php | 16 +- .../system/src/Tests/Menu/MenuTreeStorageTest.php | 19 +- core/modules/system/src/Tests/System/AdminTest.php | 7 +- core/modules/system/system.module | 5 +- core/modules/toolbar/toolbar.module | 20 +- .../user/src/Tests/UserAccountLinksTests.php | 3 +- .../Tests/Core/Menu/MenuLinkTreeParametersTest.php | 162 +++++++++++++++ 19 files changed, 730 insertions(+), 432 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index eb4c394..834d0f1 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -272,7 +272,7 @@ services: arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler'] menu.link_tree: class: Drupal\Core\Menu\MenuLinkTree - arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@access_manager', '@current_user', '@menu.active_trail', '@controller_resolver'] + arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver'] menu.default_tree_manipulators: class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators arguments: ['@access_manager', '@current_user'] diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 8de78f1..dfcf02c 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -97,6 +97,30 @@ * menu_links table holds the visible menu links. By default these are * derived from the same hook_menu definitions, however you are free to * add more with menu_link_save(). + * + * @section Rendering menus + * Once you have created menus (that contain menu links), you want to render + * them. Drupal provides a block (Drupal\system\Plugin\Block\SystemMenuBlock) to + * do so. + * + * However, perhaps you have more advanced needs and you're not satisfied with + * what the menu blocks offer you. If that's the case, you'll want to: + * - Instantiate \Drupal\Core\Menu\MenuTreeParameters, and set its values to + * match your needs. Alternatively, you can use + * MenuLinkTree::getDefaultRenderedMenuTreeLinkParameters() to get a typical + * default set of parameters, and then customize them to suit your needs. + * - Call \Drupal\Core\MenuLinkTree::load() with your menu link tree parameters, + * this will return a menu link tree. + * - Pass the menu tree to \Drupal\Core\Menu\MenuLinkTree::transform() to apply + * menu link tree manipulators that transform the tree. You will almost always + * want to apply access checking. The manipulators that you will typically + * need can be found in \Drupal\Core\Menu\DefaultMenuTreeManipulators. + * - Potentially write a custom menu tree manipulator, see + * \Drupal\Core\Menu\DefaultMenuTreeManipulators for examples. This is only + * necessary if you want to do things like adding extra metadata to rendered + * links to display icons next to them. + * - Pass the menu tree to \Drupal\menu_link\MenuTree::build(), this will build + * a renderable array. */ /** @@ -310,15 +334,16 @@ function _menu_get_links_source($name, $default) { function menu_navigation_links($menu_name, $level = 0) { /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = \Drupal::service('menu.link_tree'); - $parameters = $menu_tree->buildPageDataTreeParameters($menu_name, $level + 1); - $tree = $menu_tree->buildTree($menu_name, $parameters); + $parameters = $menu_tree->getDefaultRenderedMenuTreeLinkParameters($menu_name); + $parameters->setMaxDepth($level + 1); + $tree = $menu_tree->load($menu_name, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), array('callable' => 'menu.default_tree_manipulators:extractSubtreeOfActiveTrail', 'args' => array($level)), ); $tree = $menu_tree->transform($tree, $manipulators); - return $menu_tree->render($tree); + return $menu_tree->build($tree); } /** diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php index aae8c10..7ba6166 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkTree.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php @@ -8,16 +8,11 @@ namespace Drupal\Core\Menu; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Access\AccessManager; use Drupal\Core\Controller\ControllerResolverInterface; use Drupal\Core\Routing\RouteProviderInterface; -use Drupal\Core\Session\AccountInterface; -use Symfony\Component\DependencyInjection\Tests\Compiler\InlineServiceDefinitionsPassTest; /** - * Manages discovery, instantiation, and tree building of menu link plugins. - * - * This manager finds plugins that are rendered as menu links. + * Provides loading, transforming and rendering of menu link trees. */ class MenuLinkTree implements MenuLinkTreeInterface { @@ -43,20 +38,6 @@ class MenuLinkTree implements MenuLinkTreeInterface { protected $routeProvider; /** - * The access manager. - * - * @var \Drupal\Core\Access\AccessManager - */ - protected $accessManager; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $account; - - /** * The active menu trail service. * * @var \Drupal\Core\Menu\MenuActiveTrailInterface @@ -79,21 +60,15 @@ class MenuLinkTree implements MenuLinkTreeInterface { * The menu link plugin manager. * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider * The route provider to load routes by name. - * @param \Drupal\Core\Access\AccessManager $access_manager - * The access manager. - * @param \Drupal\Core\Session\AccountInterface $account - * The current user. * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail * The active menu trail service. * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver * The controller resolver. */ - public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, AccessManager $access_manager, AccountInterface $account, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) { + public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) { $this->treeStorage = $tree_storage; $this->menuLinkManager = $menu_link_manager; $this->routeProvider = $route_provider; - $this->accessManager = $access_manager; - $this->account = $account; $this->menuActiveTrail = $menu_active_trail; $this->controllerResolver = $controller_resolver; } @@ -101,21 +76,76 @@ public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkMana /** * {@inheritdoc} */ - public function maxDepth() { - return $this->treeStorage->maxDepth(); + public function getDefaultRenderedMenuTreeLinkParameters($menu_name) { + $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name); + + $parameters = new MenuLinkTreeParameters(); + $parameters->setActiveTrail($active_trail) + // We want links in the active trail to be expanded. + ->addExpanded($active_trail) + // We marked the links in the active trail to be expanded, but we also + // want their descendants that have the "expanded" flag enabled to be + // expanded. + ->addExpanded($this->treeStorage->getExpanded($menu_name, $active_trail)); + + return $parameters; } /** * {@inheritdoc} */ - public function getSubtreeHeight($id) { - return $this->treeStorage->getSubtreeHeight($id); + public function load($menu_name, MenuLinkTreeParameters $parameters) { + $data = $this->treeStorage->loadTreeData($menu_name, $parameters); + // Pre-load all the route objects in the subtree for access checks. + if ($data['route_names']) { + $this->routeProvider->getRoutesByNames($data['route_names']); + } + $tree = $data['tree']; + $this->createInstances($tree); + return $tree; + } + + /** + * Helper function that recursively instantiates the plugins. + */ + protected function createInstances(&$tree) { + foreach (array_keys($tree) as $id) { + // Upcast the MenuLinkTreeElement's "link" property from a definition to + // an instance. + $tree[$id]->link = $this->menuLinkManager->createInstance($tree[$id]->link['id']); + + if (!empty($tree[$id]->subtree)) { + $this->createInstances($tree[$id]->subtree); + } + } } /** * {@inheritdoc} */ - public function render($tree) { + public function transform(array $tree, array $manipulators) { + foreach ($manipulators as $manipulator) { + $callable = $manipulator['callable']; + if (!is_callable($callable)) { + $callable = $this->controllerResolver->getControllerFromDefinition($callable); + } + // Prepare the arguments for the menu tree manipulator callable; the first + // argument is always the menu link tree. + if (isset($manipulator['args'])) { + array_unshift($manipulator['args'], $tree); + $tree = call_user_func_array($callable, $manipulator['args']); + } + else { + $tree = call_user_func($callable, $tree); + } + } + return $tree; + } + + /** + * {@inheritdoc} + */ + public function build(array $tree) { $build = array(); foreach ($tree as $data) { @@ -147,7 +177,7 @@ public function render($tree) { $element['#attributes']['class'] = $class; $element['#title'] = $link->getTitle(); $element['#url'] = $link->getUrlObject(); - $element['#below'] = $data->subtree ? $this->render($data->subtree) : array(); + $element['#below'] = $data->subtree ? $this->build($data->subtree) : array(); if (isset($data->options)) { $element['#url']->setOptions(NestedArray::mergeDeep($element['#url']->getOptions(), $data->options)); } @@ -171,127 +201,17 @@ public function render($tree) { } /** - * Builds the required tree parameters used for the page menu tree. - * - * This method takes into account the active trail of the current page. - * - * @param string $menu_name - * The menu name. - * @param int $max_depth - * (optional) The maximum depth of links to retrieve. - * - * @return array - * An array of tree parameters. - */ - public function buildPageDataTreeParameters($menu_name, $max_depth = NULL) { - if (isset($max_depth)) { - $max_depth = min($max_depth, $this->treeStorage->maxDepth()); - } - - $tree_parameters = array( - 'min_depth' => 1, - 'max_depth' => $max_depth, - ); - - $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name); - // Collect all the links set to be expanded, and then add all of - // their children to the list as well. - $parents = $this->treeStorage->getExpanded($menu_name, $active_trail); - - $tree_parameters['expanded'] = $parents; - $tree_parameters['active_trail'] = $active_trail; - return $tree_parameters; - } - - /** * {@inheritdoc} - * - * @todo should this accept a menu link instance or just the ID? */ - public function buildAllDataTreeParameters($id = NULL, $max_depth = NULL) { - // Use ID as a flag for whether the data being loaded is for the whole - // tree. - $id = isset($id) ? $id : '%'; - - $tree_parameters = array( - 'min_depth' => 1, - 'max_depth' => $max_depth, - ); - if ($id != '%') { - // The tree is for a single item, so we need to match the values in - // of all the IDs on the path to root. - $tree_parameters['active_trail'] = $this->menuLinkManager->getParentIds($id); - $tree_parameters['expanded'] = $tree_parameters['active_trail']; - // Include top-level links. - $tree_parameters['expanded'][''] = ''; - } - return $tree_parameters; - } - - /** - * {@inheritdoc} - */ - public function buildSubtree($id, $max_relative_depth = NULL) { - $data = $this->treeStorage->loadSubtreeData($id, $max_relative_depth); - return $this->prepareTree($data); - } - - /** - * {@inheritdoc} - */ - public function buildTree($menu_name, array $parameters = array()) { - $data = $this->treeStorage->loadTreeData($menu_name, $parameters); - return $this->prepareTree($data); - } - - /** - * Helper function that pre-loads routes and creates plugin instances. - */ - protected function prepareTree(array $data) { - // Pre-load all the route objects in the subtree for access checks. - if ($data['route_names']) { - $this->routeProvider->getRoutesByNames($data['route_names']); - } - $tree = $data['tree']; - $this->createInstances($tree); - return $tree; + public function maxDepth() { + return $this->treeStorage->maxDepth(); } /** * {@inheritdoc} */ - public function transform(array $tree, array $manipulators) { - foreach ($manipulators as $manipulator) { - $callable = $manipulator['callable']; - if (!is_callable($callable)) { - $callable = $this->controllerResolver->getControllerFromDefinition($callable); - } - // Prepare the arguments for the menu tree manipulator callable; the first - // argument is always the menu link tree. - if (isset($manipulator['args'])) { - array_unshift($manipulator['args'], $tree); - $tree = call_user_func_array($callable, $manipulator['args']); - } - else { - $tree = call_user_func($callable, $tree); - } - } - return $tree; - } - - /** - * Helper function that recursively instantiates the plugins. - */ - protected function createInstances(&$tree) { - foreach (array_keys($tree) as $id) { - // Upcast the MenuLinkTreeElement's "link" property from a definition to - // an instance. - $tree[$id]->link = $this->menuLinkManager->createInstance($tree[$id]->link['id']); - - if (!empty($tree[$id]->subtree)) { - $this->createInstances($tree[$id]->subtree); - } - } + public function getSubtreeHeight($id) { + return $this->treeStorage->getSubtreeHeight($id); } } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php index 06fd26f..f33a059 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php @@ -10,157 +10,120 @@ /** * Defines an interface for retrieving menu link trees. */ +/** + * Defines an interface for loading, transforming and rendering menu link trees. + * + * The main goal of this service is to, given a menu name, load (::load()) the + * corresponding tree of menu links and turning this list of menu links into a + * tree (by looking at their tree metadata). Because menu links themselves are + * responsible for translation, this will already be translated for the current + * language. + * Which links are loaded can be specified in the menu link tree parameters that + * passed to ::load(). You can build your own set of parameter, but you can also + * start from a typical default (::getDefaultRenderedMenuTreeLinkParameters()). + * + * @see \Drupal\Core\Menu\MenuLinkTreeParameters + * + * If desired, one can transform (::transform()) that tree of menu links, for + * example performing access checking (to only show those links that can be + * accessed by the end user) or adding custom classes to links (to show icons + * next to the links). Very complex tasks can be performed as well (such as + * extracting a subtree from the menu link tree depending on the active trail). + * These transformations are performed by "menu link tree manipulators", and + * they can be used to perform any kind of transformation imaginable. + * + * @see \Drupal\menu_link\DefaultMenuTreeManipulators + * + * Finally, if desired, that tree of menu links can be built into a renderable + * array (::build()) for rendering as HTML. + */ interface MenuLinkTreeInterface { /** - * The maximum depth of tree that is supported. + * The default menu link tree parameters for rendering a menu. * - * @return int - * The maximum depth. - */ - public function maxDepth(); - - /** - * Finds the height of a subtree rooted by of the given ID. - * - * @param string $id - * The the ID of an item in the storage. + * Builds menu link tree parameters that: + * - expand all links in the active trail + * - also expands the descendants of the links in the active trail whose + * 'expanded' flag is enabled * - * @return int - * Returns the height of the subtree. This will be at least 1 if the ID - * exists, or 0 if the ID does not exist in the storage. - */ - public function getSubtreeHeight($id); - - /** - * Returns a menu tree ready to be rendered. + * This only sets the (relatively complex) parameters to achieve the two above + * goals, but you can still further customize these parameters. * - * 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. + * @see \Drupal\Core\Menu\MenuLinkTreeParameters * - * @param array $tree - * A data structure representing the tree as returned from menu_tree_data. + * @param string $menu_name + * The menu name, needed for retrieving the active trail and links with the + * 'expanded' flag enabled. * - * @return array - * A structured array to be rendered by drupal_render(). + * @return \Drupal\Core\Menu\MenuLinkTreeParameters $parameters + * The parameters to determine which menu links to be loaded into a tree. */ - public function render($tree); + public function getDefaultRenderedMenuTreeLinkParameters($menu_name); /** - * Builds the tree parameters to build a menu tree based on the current page. + * Loads a menu tree with a menu link plugin instance at each element. * * @param string $menu_name - * The named menu links to return. - * @param int $max_depth - * (optional) The maximum depth of links to retrieve. - * - * @return array - * An array of tree parameters. + * The name of the menu. + * @param \Drupal\Core\Menu\MenuLinkTreeParameters $parameters + * The parameters to determine which menu links to be loaded into a tree. * - * @todo This is now a misnomer; it's no longer based on the current page, but - * on the active trail. + * @return \Drupal\Core\Menu\MenuLinkTreeElement[] + * A menu link tree. */ - public function buildPageDataTreeParameters($menu_name, $max_depth = NULL); + public function load($menu_name, MenuLinkTreeParameters $parameters); /** - * Gets the parameters to pass to buildTree() to build a named menu tree. + * Applies menu link tree manipulators to transform the given 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 array $id - * A menu link ID, or NULL. If a link ID 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 - * $id, in which case $max_depth should be greater than $link['depth']. + * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree + * The menu tree to manipulate. + * @param array $manipulators + * The menu link tree manipulators to apply. Each is an array with keys: + * - callable: a callable or a string that can be resolved to a callable + * by ControllerResolverInterface::getControllerFromDefinition() + * - args: optional array of arguments to pass to the callable after $tree. * - * @return array - * An array of tree parameters. + * @return \Drupal\Core\Menu\MenuLinkTreeElement[] + * The manipulated menu link tree. */ - public function buildAllDataTreeParameters($id = NULL, $max_depth = NULL); + public function transform(array $tree, array $manipulators); /** - * Builds a menu tree with a menu link plugin instance at each element. + * Builds a renderable array of a menu tree. * - * Only visible links (hidden == 0) are returned in the data. + * 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 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 ids in this list. If empty, the whole menu - * tree is built, unless 'only_active_trail' is TRUE. - * - active_trail: An array of ids, 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. + * @param array $tree + * A data structure representing the tree as returned from ::load(). * * @return array - * An array of menu links, in the order they should be rendered. The array - * is a list of associative arrays -- these have several keys: - * - link: the menu link plugin instance - * - below: the subtree below the link, or empty array. It has the same - * structure as the top level array. - * - depth: int. the depth of this link below he root of the tree. - * - has_children: boolean. even if the below value may be empty the link - * may have children in the tree that are not shown. This - * is a hint for adding appropriate classes for theming. - * - in_active_trail: boolean - * - access: NULL. + * A renderable array. */ - public function buildTree($menu_name, array $parameters = array()); + public function build(array $tree); /** - * Builds a menu subtree starting with the passed in menu link plugin ID. - * - * Only visible links (hidden == 0) are returned in the data. - * - * @param string $id - * The menu link plugin ID. - * @param int $max_relative_depth - * The maximum depth of child menu links relative to the passed in. + * The maximum depth of tree that is supported. * - * @return array - * An array of menu links, in the order they should be rendered. The array - * is a list of associative arrays -- these have several keys: - * - link: the menu link plugin instance - * - below: the subtree below the link, or empty array. It has the same - * structure as the top level array. - * - depth: int. the depth of this link below he root of the tree. - * - has_children: boolean. even if the below value may be empty the link - * may have children in the tree that are not shown. This - * is a hint for adding appropriate classes for theming. - * - in_active_trail: boolean - * - access: NULL + * @return int + * The maximum depth. */ - public function buildSubtree($id, $max_relative_depth = NULL); + public function maxDepth(); /** - * Applies the given tree manipulators in order to a menu link tree. + * Finds the height of a subtree rooted by of the given ID. * - * @param array $tree - * The menu tree to manipulate. - * @param array $manipulators - * The menu tree manipulators to apply. Each is an array with keys: - * - callable: a callable or a string that can be resolved to a callable - * by ControllerResolverInterface::getControllerFromDefinition() - * - args: optional array of arguments to pass to the callable after $tree. + * @param string $id + * The the ID of an item in the storage. * - * @return array - * The manipulated menu tree. + * @return int + * Returns the height of the subtree. This will be at least 1 if the ID + * exists, or 0 if the ID does not exist in the storage. */ - public function transform(array $tree, array $manipulators); + public function getSubtreeHeight($id); } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeParameters.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeParameters.php new file mode 100644 index 0000000..50caa9f --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeParameters.php @@ -0,0 +1,219 @@ +root = $root; + return $this; + } + + /** + * Sets a minimum depth; loads a menu tree from the given level. + * + * @param int $min_depth + * The (root-relative) minimum depth to apply. + * + * @return $this + */ + public function setMinDepth($min_depth) { + $this->minDepth = max(1, $min_depth); + return $this; + } + + /** + * Sets a minimum depth; loads a menu tree up to the given level. + * + * @param int $max_depth + * The (root-relative) maximum depth to apply. + * + * @return $this + * + * @codeCoverageIgnore + */ + public function setMaxDepth($max_depth) { + $this->maxDepth = $max_depth; + return $this; + } + + /** + * Adds menu links to be expanded (whose children to show). + * + * @param string[] $expanded + * An array containing the links to be expanded: a list of menu link plugin + * IDs. + * + * @return $this + */ + public function addExpanded(array $expanded) { + $this->expanded = array_merge($this->expanded, $expanded); + $this->expanded = array_unique($this->expanded); + return $this; + } + + /** + * Sets the active trail. + * + * @param string[] $active_trail + * An array containing the active trail: a list of menu link plugin IDs. + * + * @return $this + * + * @see \Drupal\Core\Menu\MenuActiveTrail::getActiveTrailIds() + * + * @codeCoverageIgnore + */ + public function setActiveTrail(array $active_trail) { + $this->activeTrail = $active_trail; + return $this; + } + + /** + * Adds a custom query condition. + * + * @param string $definition_field + * Only conditions that are testing menu link definition fields are allowed. + * @param mixed $value + * The value to test the link definition field against. In most cases, this + * is a scalar. For more complex options, it is an array. The meaning of + * each element in the array is dependent on the $operator. + * @param string|NULL $operator + * The comparison operator, such as =, <, or >=. It also accepts more + * complex options such as IN, LIKE, or BETWEEN. + * + * @return $this + */ + public function addCondition($definition_field, $value, $operator = NULL) { + if (!isset($operator)) { + $this->conditions[$definition_field] = $value; + } + else { + $this->conditions[$definition_field] = array($value, $operator); + } + return $this; + } + + /** + * Excludes hidden links. + * + * @return $this + */ + public function excludeHiddenLinks() { + $this->addCondition('hidden', 0); + return $this; + } + + /** + * Ensures only the top level of the tree is loaded. + * + * @return $this + */ + public function topLevelOnly() { + $this->setMaxDepth(1); + return $this; + } + + /** + * Excludes the root menu link from the tree. + * + * Note that this is only necessary when you specified a custom root, because + * the "real" root (@code '' @encode) is mapped to a non-existing menu link. + * Hence when loading a menu link tree without specifying a custom root, you + * will never get a root; the tree will start at the children. + * + * @return $this + */ + public function excludeRoot() { + $this->setMinDepth(1); + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php index 406c6cd..823f3f9 100644 --- a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php +++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php @@ -45,8 +45,9 @@ public function getParentSelectOptions($id = '', array $menus = NULL) { foreach ($menus as $menu_name => $menu_title) { $options[$menu_name . ':'] = '<' . $menu_title . '>'; - $parameters = $this->menuLinkTree->buildAllDataTreeParameters(NULL, $depth_limit); - $tree = $this->menuLinkTree->buildTree($menu_name, $parameters); + $parameters = new MenuLinkTreeParameters(); + $parameters->setMaxDepth($depth_limit); + $tree = $this->menuLinkTree->load($menu_name, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php index 5f2767b..b9d7e27 100644 --- a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php @@ -749,13 +749,11 @@ protected function saveRecursive($id, &$children, &$links) { /** * {@inheritdoc} */ - public function loadTreeData($menu_name, array $parameters = array()) { - // Build the cache id; sort parents to prevent duplicate storage and remove - // default parameter values. - asort($parameters); - if (isset($parameters['expanded'])) { - sort($parameters['expanded']); - } + public function loadTreeData($menu_name, MenuLinkTreeParameters $parameters) { + // Build the cache id; sort 'expanded' and 'conditions' to prevent duplicate + // cache items. + sort($parameters->expanded); + sort($parameters->conditions); // @todo - may be able to skip hashing after https://drupal.org/node/2224847 $tree_cid = "tree-data:$menu_name:" . hash('sha256', serialize($parameters)); $cache = $this->treeCacheBackend->get($tree_cid); @@ -766,52 +764,117 @@ public function loadTreeData($menu_name, array $parameters = array()) { unset($data['definitions']); } else { - $query = $this->connection->select($this->table, $this->options); - $query->fields($this->table); - for ($i = 1; $i <= $this->maxDepth(); $i++) { - $query->orderBy('p' . $i, 'ASC'); + $links = $this->loadLinks($menu_name, $parameters); + $data['tree'] = $this->doBuildTreeData($links, $parameters->activeTrail, $parameters->minDepth); + $data['definitions'] = array(); + $data['route_names'] = $this->collectRoutesAndDefinitions($data['tree'], $data['definitions']); + $this->treeCacheBackend->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name)); + // The definitions were already added to $this->definitions in + // $this->doBuildTreeData() + unset($data['definitions']); + } + return $data; + } + + /** + * Loads links in the given menu, according to the given tree parameters. + * + * @param string $menu_name + * A menu name. + * @param MenuLinkTreeParameters &$parameters + * The parameters to determine which menu links to be loaded into a tree. + * Passed by reference, so that ::loadLinks() can set the absolute minimum + * depth, which is used by ::doBuildTreeData(). + * @return array + * 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_tree} table. This array must be ordered + * depth-first. + */ + protected function loadLinks($menu_name, MenuLinkTreeParameters &$parameters) { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table); + + // Allow a custom root to be specified for loading a menu link tree. If + // ommitted, the default root (i.e. the actual root, '') is used. + if ($parameters->root !== '') { + $root = $this->loadFull($parameters->root); + + // If the custom root does not exist, we cannot load the links below it. + if (!$root) { + return array(); } - $query->condition('menu_name', $menu_name); + // When specifying a custom root, we only want to find links whose + // parent IDs match that of the root; that's how ignore the rest of the + // tree. In other words: we exclude everything unreachable from the + // custom root. + for ($i = 1; $i <= $root['depth']; $i++) { + $query->condition("p$i", $root["p$i"]); + } - if (!empty($parameters['expanded'])) { - $query->condition('parent', $parameters['expanded'], 'IN'); + // When specifying a custom root, the menu is determined by that root. + $menu_name = $root['menu_name']; + + // If the custom root exists, then we must rewrite some of our + // parameters; parameters are relative to the root (default or custom), + // but the queries require absolute numbers, so adjust correspondingly. + if (isset($parameters->minDepth)) { + $parameters->minDepth += $root['depth']; } - elseif (!empty($parameters['only_active_trail'])) { - $query->condition('id', $parameters['active_trail'], 'IN'); + else { + $parameters->minDepth = $root['depth']; } - $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : NULL); - if ($min_depth) { - $query->condition('depth', $min_depth, '>='); + if (isset($parameters->maxDepth)) { + $parameters->maxDepth += $root['depth']; } - if (isset($parameters['max_depth'])) { - $query->condition('depth', $parameters['max_depth'], '<='); + } + + // If no minimum depth is specified, then set the actual minimum depth, + // depending on the root. + if (!isset($parameters->minDepth)) { + if ($parameters->root !== '' && $root) { + $parameters->minDepth = $root['depth']; + } + else { + $parameters->minDepth = 1; } - // Add custom query conditions, if any were passed. - if (!empty($parameters['conditions'])) { - // Only allow conditions that are testing definition fields. - $parameters['conditions'] = array_intersect_key($parameters['conditions'], array_flip($this->definitionFields())); - foreach ($parameters['conditions'] as $column => $value) { + } + + for ($i = 1; $i <= $this->maxDepth(); $i++) { + $query->orderBy('p' . $i, 'ASC'); + } + + $query->condition('menu_name', $menu_name); + + if (!empty($parameters->expanded)) { + $query->condition('parent', $parameters->expanded, 'IN'); + } + if (isset($parameters->minDepth) && $parameters->minDepth > 1) { + $query->condition('depth', $parameters->minDepth, '>='); + } + if (isset($parameters->maxDepth)) { + $query->condition('depth', $parameters->maxDepth, '<='); + } + // Add custom query conditions, if any were passed. + if (!empty($parameters->conditions)) { + // Only allow conditions that are testing definition fields. + $parameters->conditions = array_intersect_key($parameters->conditions, array_flip($this->definitionFields())); + foreach ($parameters->conditions as $column => $value) { + if (!is_array($value)) { $query->condition($column, $value); } - } - $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); - $links = $this->safeExecuteSelect($query)->fetchAll(\PDO::FETCH_ASSOC); - if (!isset($min_depth)) { - $first_link = reset($links); - if ($first_link) { - $min_depth = $first_link['depth']; + else { + $operator = $value[1]; + $value = $value[0]; + $query->condition($column, $value, $operator); } } - $data['tree'] = $this->doBuildTreeData($links, $active_trail, $min_depth); - $data['definitions'] = array(); - $data['route_names'] = $this->collectRoutesAndDefinitions($data['tree'], $data['definitions']); - $this->treeCacheBackend->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name)); - // The definitions were already added to $this->definitions in - // $this->doBuildTreeData() - unset($data['definitions']); } - return $data; + + $links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + + return $links; } /** @@ -859,48 +922,14 @@ protected function doCollectRoutesAndDefinitions(array $tree, array &$definition * {@inheritdoc} */ public function loadSubtreeData($id, $max_relative_depth = NULL) { - $tree_cid = "subtree-data:$id:$max_relative_depth"; - $cache = $this->treeCacheBackend->get($tree_cid); - if ($cache && isset($cache->data)) { - $data = $cache->data; - // Cache the definitions in memory so they don't need to be loaded again. - $this->definitions += $data['definitions']; - unset($data['definitions']); - } - else { - $tree = array(); - $root = $this->loadFull($id); - if (!$root) { - return $tree; - } - $query = $this->connection->select($this->table, $this->options); - $query->fields($this->table); - for ($i = 1; $i <= $this->maxDepth(); $i++) { - $query->orderBy('p' . $i, 'ASC'); - } - $query->condition('hidden', 0); - $query->condition('menu_name', $root['menu_name']); - for ($i = 1; $i <= $root['depth']; $i++) { - $query->condition("p$i", $root["p$i"]); - } - if (!empty($max_relative_depth)) { - $query->condition('depth', (int) $root['depth'] + $max_relative_depth, '<='); - } - $links = $this->safeExecuteSelect($query)->fetchAll(\PDO::FETCH_ASSOC); - $tree = $this->doBuildTreeData($links, array(), $root['depth']); - $data['definitions'] = array(); - $data['route_names'] = $this->collectRoutesAndDefinitions($tree, $data['definitions']); - $data['tree'] = $tree; - if ($data['tree']) { - $top = reset($tree); - $menu_name = $top->link['menu_name']; - $this->treeCacheBackend->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name)); - } - // The definitions were already added to $this->definitions in - // $this->doBuildTreeData() - unset($data['definitions']); + $tree = array(); + $root = $this->loadFull($id); + if (!$root) { + return $tree; } - return $data; + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot($id)->excludeHiddenLinks(); + return $this->loadTreeData($root['menu_name'], $parameters); } /** @@ -958,28 +987,13 @@ public function getAllChildIds($id) { * {@inheritdoc} */ public function loadAllChildLinks($id, $max_relative_depth = NULL) { - $tree = array(); - $root = $this->loadFull($id); - if (!$root || $root['depth'] == $this->maxDepth()) { - return $tree; - } - $query = $this->connection->select($this->table, $this->options); - $query->fields($this->table, $this->definitionFields()); - $query->condition('hidden', 0); - $query->condition('menu_name', $root['menu_name']); - for ($i = 1; $i <= $root['depth']; $i++) { - $query->condition("p$i", $root["p$i"]); - } - // The next p column should not be empty. This excludes the root link. - $query->condition("p$i", 0, '>'); - if (!empty($max_relative_depth)) { - $query->condition('depth', (int) $root['depth'] + $max_relative_depth, '<='); - } - $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); - foreach ($loaded as $id => $link) { - $loaded[$id] = $this->prepareLink($link); + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot($id)->excludeRoot()->setMaxDepth($max_relative_depth)->excludeHiddenLinks(); + $links = $this->loadLinks(NULL, $parameters); + foreach ($links as $id => $link) { + $links[$id] = $this->prepareLink($link); } - return $loaded; + return $links; } /** diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php index f386fc9..78042d5 100644 --- a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php @@ -121,29 +121,15 @@ public function delete($id); * * @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 plugin ids to return only menu links that - * are children of one of the ids in this list. If empty, the whole menu - * tree is built, unless 'only_active_trail' is TRUE. - * - active_trail: An array of ids, 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 condition key/value pairs - * to restrict the links loaded. Each key must be one of the keys - * in the plugin definition. + * @param \Drupal\Core\Menu\MenuLinkTreeParameters $parameters + * The parameters to determine which menu links to be loaded into a tree. * * @return array * An array with 2 elements: * - tree: A fully built menu tree. * - route_names: An array of all route names used in the tree. */ - public function loadTreeData($menu_name, array $parameters = array()); + public function loadTreeData($menu_name, MenuLinkTreeParameters $parameters); /** * Loads all the visible menu links that are below the given ID. diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php index ceeb501..18a5806b 100644 --- a/core/modules/menu_ui/src/MenuForm.php +++ b/core/modules/menu_ui/src/MenuForm.php @@ -14,6 +14,7 @@ use Drupal\Core\Menu\MenuLinkTreeElement; use Drupal\Core\Menu\MenuLinkTreeInterface; use Drupal\Core\Menu\MenuLinkManagerInterface; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\Core\Render\Element; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -203,8 +204,7 @@ protected function buildOverviewForm(array &$form, array &$form_state) { $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css'); - $parameters = $this->menuTree->buildAllDataTreeParameters(); - $tree = $this->menuTree->buildTree($this->entity->id(), $parameters); + $tree = $this->menuTree->load($this->entity->id(), new MenuLinkTreeParameters()); // We indicate that a menu administrator is running the menu access check. $manipulators = array( diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index 95725c4..e7db8a7 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -13,6 +13,7 @@ use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Menu\MenuLinkTreeInterface; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\Core\Theme\ThemeAccessCheck; use Drupal\system\SystemManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -117,8 +118,9 @@ public function overview($link_id) { if ($this->systemManager->checkRequirements() && $this->currentUser()->hasPermission('administer site configuration')) { drupal_set_message($this->t('One or more problems were detected with your Drupal installation. Check the status report for more information.', array('@status' => url('admin/reports/status'))), 'error'); } - $top_tree = $this->menuLinkTree->buildSubtree($link_id, 1); - $tree = $top_tree[$link_id]->subtree; + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot($link_id)->excludeRoot()->topLevelOnly()->excludeHiddenLinks(); + $tree = $this->menuLinkTree->load(NULL, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index d87fec0..a0e9a97 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -79,14 +79,14 @@ public static function create(ContainerInterface $container, array $configuratio */ public function build() { $menu_name = $this->getDerivativeId(); - $parameters = $this->menuTree->buildPageDataTreeParameters($menu_name); - $tree = $this->menuTree->buildTree($menu_name, $parameters); + $parameters = $this->menuTree->getDefaultRenderedMenuTreeLinkParameters($menu_name); + $tree = $this->menuTree->load($menu_name, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), ); $tree = $this->menuTree->transform($tree, $manipulators); - return $this->menuTree->render($tree); + return $this->menuTree->build($tree); } /** diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php index 2b9108a..af80b61 100644 --- a/core/modules/system/src/SystemManager.php +++ b/core/modules/system/src/SystemManager.php @@ -10,6 +10,7 @@ use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Menu\MenuActiveTrailInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -212,9 +213,10 @@ public function getBlockContents() { public function getAdminBlock(MenuLinkInterface $instance) { $content = array(); // Only find the children of this link. - $parameters['expanded'][] = $instance->getPluginId(); - $parameters['conditions']['hidden'] = 0; - $tree = $this->menuTree->buildTree($instance->getMenuName(), $parameters); + $link_id = $instance->getPluginId(); + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot($link_id)->excludeRoot()->topLevelOnly()->excludeHiddenLinks(); + $tree = $this->menuTree->load(NULL, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), diff --git a/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php index 92099ba..3913cda 100644 --- a/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php +++ b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Menu; use Drupal\Core\Menu\MenuLinkTreeElement; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\simpletest\KernelTestBase; use Drupal\Tests\Core\Menu\MenuLinkMock; @@ -75,17 +76,17 @@ public function testDeleteLinksInMenu() { \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'))->save(); \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu2', 'bundle' => 'menu_link_content'))->save(); - $output = $this->linkTree->buildTree('menu1'); + $output = $this->linkTree->load('menu1', new MenuLinkTreeParameters()); $this->assertEqual(count($output), 2); - $output = $this->linkTree->buildTree('menu2'); + $output = $this->linkTree->load('menu2', new MenuLinkTreeParameters()); $this->assertEqual(count($output), 1); $this->menuLinkManager->deleteLinksInMenu('menu1'); - $output = $this->linkTree->buildTree('menu1'); + $output = $this->linkTree->load('menu1', new MenuLinkTreeParameters()); $this->assertEqual(count($output), 0); - $output = $this->linkTree->buildTree('menu2'); + $output = $this->linkTree->load('menu2', new MenuLinkTreeParameters()); $this->assertEqual(count($output), 1); } @@ -116,7 +117,8 @@ public function testCreateLinksInMenu() { foreach ($links as $instance) { $this->menuLinkManager->createLink($instance->getPluginId(), $instance->getPluginDefinition()); } - $tree = $this->linkTree->buildTree('mock'); + $parameters = new MenuLinkTreeParameters(); + $tree = $this->linkTree->load('mock', $parameters); $count = function(array $tree) { $sum = function ($carry, MenuLinkTreeElement $item) { @@ -126,7 +128,9 @@ public function testCreateLinksInMenu() { }; $this->assertEqual($count($tree), 8); - $tree = $this->linkTree->buildSubtree('test.example2'); + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot('test.example2'); + $tree = $this->linkTree->load($instance->getMenuName(), $parameters); $top_link = reset($tree); $this->assertEqual(count($top_link->subtree), 1); $child = reset($top_link->subtree); diff --git a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php index 6d25768..f09e71e 100644 --- a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php +++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Menu; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\Core\Menu\MenuTreeStorage; use Drupal\simpletest\KernelTestBase; @@ -253,7 +254,7 @@ public function testLoadTree() { $this->addMenuLink('test4'); $this->addMenuLink('test5', 'test4'); - $data = $this->treeStorage->loadTreeData('tools'); + $data = $this->treeStorage->loadTreeData('tools', new MenuLinkTreeParameters()); $tree = $data['tree']; $this->assertEqual(count($tree['test1']->subtree), 1); $this->assertEqual(count($tree['test1']->subtree['test2']->subtree), 1); @@ -261,7 +262,9 @@ public function testLoadTree() { $this->assertEqual(count($tree['test4']->subtree), 1); $this->assertEqual(count($tree['test4']->subtree['test5']->subtree), 0); - $data = $this->treeStorage->loadTreeData('tools', array('active_trail' => array('test4', 'test5'))); + $parameters = new MenuLinkTreeParameters(); + $parameters->setActiveTrail(array('test4', 'test5')); + $data = $this->treeStorage->loadTreeData('tools', $parameters); $tree = $data['tree']; $this->assertEqual(count($tree['test1']->subtree), 1); $this->assertFalse($tree['test1']->inActiveTrail); @@ -273,14 +276,6 @@ public function testLoadTree() { $this->assertTrue($tree['test4']->inActiveTrail); $this->assertEqual(count($tree['test4']->subtree['test5']->subtree), 0); $this->assertTrue($tree['test4']->subtree['test5']->inActiveTrail); - - $data = $this->treeStorage->loadTreeData('tools', array('active_trail' => array('test4', 'test5'), 'only_active_trail' => TRUE)); - $tree = $data['tree']; - $this->assertTrue(empty($tree['test1'])); - $this->assertEqual(count($tree['test4']->subtree), 1); - $this->assertTrue($tree['test4']->inActiveTrail); - $this->assertEqual(count($tree['test4']->subtree['test5']->subtree), 0); - $this->assertTrue($tree['test4']->subtree['test5']->inActiveTrail); } /** @@ -396,8 +391,8 @@ protected function assertMenuLink($id, array $expected_properties, array $parent $found_children = array_keys($this->treeStorage->loadAllChildLinks($id)); // We need both these checks since the 2nd will pass if there are extra // IDs loaded in $found_children. - $this->assertEqual(count($children), count($found_children), "Found expected number of children for $id"); - $this->assertEqual(array_intersect($children, $found_children), $children, 'Child IDs match'); + $this->assertEqual(count($children), count($found_children)); //, "Found expected number of children for $id"); + $this->assertEqual(array_intersect($children, $found_children), $children); //, 'Child IDs match'); } } diff --git a/core/modules/system/src/Tests/System/AdminTest.php b/core/modules/system/src/Tests/System/AdminTest.php index 7b40c07..58a8889 100644 --- a/core/modules/system/src/Tests/System/AdminTest.php +++ b/core/modules/system/src/Tests/System/AdminTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\System; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\simpletest\WebTestBase; /** @@ -133,9 +134,9 @@ protected function getTopLevelMenuLinks() { $menu_tree = $this->container->get('menu.link_tree'); // The system.admin link is normally the parent of all top-level admin links. - $link_id = 'system.admin'; - $top_tree = $menu_tree->buildSubtree($link_id, 1); - $tree = $top_tree[$link_id]->subtree; + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->topLevelOnly()->excludeHiddenLinks(); + $tree = $menu_tree->load(NULL, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:flatten'), diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 230e596..7fb4be9 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -10,6 +10,7 @@ use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\block\BlockPluginInterface; use Drupal\user\UserInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -1460,7 +1461,9 @@ function system_get_module_admin_tasks($module, array $info) { $menu_tree = \Drupal::service('menu.link_tree'); if (!isset($tree)) { - $tree = $menu_tree->buildSubtree('system.admin'); + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->excludeHiddenLinks(); + $tree = $menu_tree->load('system.admin', $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index aa7eae9..83ec5a7 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -7,6 +7,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Cache\Cache; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\Core\Render\Element; use Drupal\Core\Template\Attribute; use Drupal\Component\Utility\Crypt; @@ -411,18 +412,17 @@ function toolbar_toolbar() { */ function toolbar_prerender_toolbar_administration_tray(array $element) { $menu_tree = \Drupal::menuTree(); - // Retrieve the administration menu from the menu tree manager. - $parameters = array( - 'expanded' => array('system.admin') - ); - $tree = $menu_tree->buildTree('admin', $parameters); + // Render the top-level administration menu links. + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->topLevelOnly()->excludeHiddenLinks(); + $tree = $menu_tree->load(NULL, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), array('callable' => 'toolbar_menu_navigation_links'), ); $tree = $menu_tree->transform($tree, $manipulators); - $element['administration_menu'] = $menu_tree->render($tree); + $element['administration_menu'] = $menu_tree->build($tree); return $element; } @@ -470,9 +470,9 @@ function toolbar_menu_navigation_links(array $tree) { function toolbar_get_rendered_subtrees() { /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = \Drupal::service('menu.link_tree'); - $link_id = 'system.admin'; - $top_tree = $menu_tree->buildSubtree($link_id, 3); - $tree = $top_tree[$link_id]->subtree; + $parameters = new MenuLinkTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(3)->excludeHiddenLinks(); + $tree = $menu_tree->load(NULL, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), @@ -484,7 +484,7 @@ function toolbar_get_rendered_subtrees() { /** @var \Drupal\Core\Menu\MenuLinkInterface $item */ $link = $element->link; if (!empty($element->subtree)) { - $subtree = $menu_tree->render($element->subtree); + $subtree = $menu_tree->build($element->subtree); $output = drupal_render($subtree); } else { diff --git a/core/modules/user/src/Tests/UserAccountLinksTests.php b/core/modules/user/src/Tests/UserAccountLinksTests.php index b65bc02..97033fe 100644 --- a/core/modules/user/src/Tests/UserAccountLinksTests.php +++ b/core/modules/user/src/Tests/UserAccountLinksTests.php @@ -7,6 +7,7 @@ namespace Drupal\user\Tests; +use Drupal\Core\Menu\MenuLinkTreeParameters; use Drupal\simpletest\WebTestBase; /** @@ -69,7 +70,7 @@ function testSecondaryMenu() { // For a logged-out user, expect no secondary links. /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = \Drupal::service('menu.link_tree'); - $tree = $menu_tree->buildTree('account'); + $tree = $menu_tree->load('account', new MenuLinkTreeParameters()); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), ); diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeParametersTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeParametersTest.php new file mode 100644 index 0000000..9110eec --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeParametersTest.php @@ -0,0 +1,162 @@ + 'Tests Drupal\Core\Menu\MenuLinkTreeParameters', + 'description' => '', + 'group' => 'Menu', + ); + } + + /** + * Provides test data for testSetMinDepth(). + */ + public function providerTestSetMinDepth() { + $data = array(); + + // Valid values at the extremes and in the middle. + $data[] = array(1, 1); + $data[] = array(2, 2); + $data[] = array(9, 9); + + // Invalid values are mapped to the closest valid value. + $data[] = array(-10000, 1); + $data[] = array(0, 1); + // … except for those invalid values that reach beyond the maximum depth, + // because MenuLinkTreeParameters is a value object and hence cannot depend + // on anything; to know the actual maximum depth, it'd have to depend on the + // MenuTreeStorage service. + $data[] = array(10, 10); + $data[] = array(100000, 100000); + + return $data; + } + + /** + * Tests setMinDepth(). + * + * @covers ::setMinDepth + * @dataProvider providerTestSetMinDepth + */ + public function testSetMinDepth($min_depth, $expected) { + $parameters = new MenuLinkTreeParameters(); + $parameters->setMinDepth($min_depth); + $this->assertEquals($expected, $parameters->minDepth); + } + + /** + * Tests addExpanded(). + * + * @covers ::addExpanded + */ + public function testAddExpanded() { + $parameters = new MenuLinkTreeParameters(); + + // Verify default value. + $this->assertEquals(array(), $parameters->expanded); + + // Add actual menu link plugin IDs to be expanded. + $parameters->addExpanded(array('foo', 'bar', 'baz')); + $this->assertEquals(array('foo', 'bar', 'baz'), $parameters->expanded); + + // Add additional menu link plugin IDs; they should be merged, not replacing + // the old ones. + $parameters->addExpanded(array('qux', 'quux')); + $this->assertEquals(array('foo', 'bar', 'baz', 'qux', 'quux'), $parameters->expanded); + + // Add pre-existing menu link plugin IDs; they should not be added again; + // this is a set. + $parameters->addExpanded(array('bar', 'quux')); + $this->assertEquals(array('foo', 'bar', 'baz', 'qux', 'quux'), $parameters->expanded); + } + + /** + * Tests addCondition(). + * + * @covers ::addCondition + */ + public function testAddCondition() { + $parameters = new MenuLinkTreeParameters(); + + // Verify default value. + $this->assertEquals(array(), $parameters->conditions); + + // Add a condition. + $parameters->addCondition('expanded', 1); + $this->assertEquals(array('expanded' => 1), $parameters->conditions); + + // Add another condition. + $parameters->addCondition('has_children', 0); + $this->assertEquals(array('expanded' => 1, 'has_children' => 0), $parameters->conditions); + + // Add a condition with an operator. + $parameters->addCondition('provider', array('module1', 'module2'), 'IN'); + $this->assertEquals(array('expanded' => 1, 'has_children' => 0, 'provider' => array(array('module1', 'module2'), 'IN')), $parameters->conditions); + + // Add another condition with an operator. + $parameters->addCondition('id', 1337, '<'); + $this->assertEquals(array('expanded' => 1, 'has_children' => 0, 'provider' => array(array('module1', 'module2'), 'IN'), 'id' => array(1337, '<')), $parameters->conditions); + + // It's impossible to add two conditions on the same field; in that case, + // the old condition will be overwritten. + $parameters->addCondition('provider', 'other_module'); + $this->assertEquals(array('expanded' => 1, 'has_children' => 0, 'provider' => 'other_module', 'id' => array(1337, '<')), $parameters->conditions); + } + + /** + * Tests excludeHiddenLinks(). + * + * @covers ::excludeHiddenLinks + */ + public function testExcludeHiddenLinks() { + $parameters = new MenuLinkTreeParameters(); + $parameters->excludeHiddenLinks(); + $this->assertEquals(0, $parameters->conditions['hidden']); + } + + /** + * Tests topLevelOnly(). + * + * @covers ::topLevelOnly + */ + public function testTopLevelOnly() { + $parameters = new MenuLinkTreeParameters(); + $parameters->topLevelOnly(); + $this->assertEquals(1, $parameters->maxDepth); + } + + /** + * Tests excludeRoot(). + * + * @covers ::excludeRoot + */ + public function testExcludeRoot() { + $parameters = new MenuLinkTreeParameters(); + $parameters->excludeRoot(); + $this->assertEquals(1, $parameters->minDepth); + } + +}