Commit 50ac4700 authored by alexpott's avatar alexpott

Issue #2301273 by pwolanin, dawehner, Wim Leers, effulgentsia, joelpittet,...

Issue #2301273 by pwolanin, dawehner, Wim Leers, effulgentsia, joelpittet, larowlan, YesCT, kgoel: MenuLinkNG part2 (no UI or conversions): MenuLinkTree API and unit tests.
parent 0e50b732
......@@ -275,6 +275,18 @@ services:
plugin.manager.menu.link:
class: Drupal\Core\Menu\MenuLinkManager
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', '@menu.active_trail', '@controller_resolver']
menu.default_tree_manipulators:
class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
arguments: ['@access_manager', '@current_user']
menu.active_trail:
class: Drupal\Core\Menu\MenuActiveTrail
arguments: ['@plugin.manager.menu.link', '@current_route_match']
menu.parent_form_selector:
class: Drupal\Core\Menu\MenuParentFormSelector
arguments: ['@menu.link_tree', '@entity.manager', '@string_translation']
plugin.manager.menu.local_action:
class: Drupal\Core\Menu\LocalActionManager
arguments: ['@controller_resolver', '@request_stack', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user']
......
<?php
/**
* @file
* Contains \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a couple of menu link tree manipulators.
*
* This class provides menu link tree manipulators to:
* - perform access checking
* - generate a unique index for the elements in a tree and sorting by it
* - flatten a tree (i.e. a 1-dimensional tree)
* - extract a subtree of the given tree according to the active trail
*/
class DefaultMenuLinkTreeManipulators {
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManager
*/
protected $accessManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators object.
*
* @param \Drupal\Core\Access\AccessManager $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(AccessManager $access_manager, AccountInterface $account) {
$this->accessManager = $access_manager;
$this->account = $account;
}
/**
* Performs access checks of a menu tree.
*
* Removes menu links from the given menu tree whose links are inaccessible
* for the current user, sets the 'access' property to TRUE on tree elements
* that are accessible for the current user.
*
* Makes the resulting menu tree impossible to render cache, unless render
* caching per user is acceptable.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function checkAccess(array $tree) {
foreach ($tree as $key => $element) {
// Other menu tree manipulators may already have calculated access, do not
// overwrite the existing value in that case.
if (!isset($element->access)) {
$tree[$key]->access = $this->menuLinkCheckAccess($element->link);
}
if ($tree[$key]->access) {
if ($tree[$key]->subtree) {
$tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
}
}
else {
unset($tree[$key]);
}
}
return $tree;
}
/**
* Checks access for one menu link instance.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $instance
* The menu link instance.
*
* @return bool
* TRUE if the current user can access the link, FALSE otherwise.
*/
protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
// Use the definition here since that's a lot faster than creating a Url
// object that we don't need.
$definition = $instance->getPluginDefinition();
// 'url' should only be populated for external links.
if (!empty($definition['url']) && empty($definition['route_name'])) {
$access = TRUE;
}
else {
$access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account);
}
return $access;
}
/**
* Generates a unique index and sorts by it.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function generateIndexAndSort(array $tree) {
$new_tree = array();
foreach ($tree as $key => $v) {
if ($tree[$key]->subtree) {
$tree[$key]->subtree = $this->generateIndexAndSort($tree[$key]->subtree);
}
$instance = $tree[$key]->link;
// The weights are made a uniform 5 digits by adding 50000 as an offset.
// After $this->menuLinkCheckAccess(), $instance->getTitle() has the
// localized or translated title. Adding the plugin id to the end of the
// index insures that it is unique.
$new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key];
}
ksort($new_tree);
return $new_tree;
}
/**
* Flattens the tree to a single level.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function flatten(array $tree) {
foreach ($tree as $key => $element) {
if ($tree[$key]->subtree) {
$tree += $this->flatten($tree[$key]->subtree);
}
$tree[$key]->subtree = array();
}
return $tree;
}
/**
* Extracts a subtree of the active trail.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
* @param int $level
* The level in the active trail to extract.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function extractSubtreeOfActiveTrail(array $tree, $level) {
// Go down the active trail until the right level is reached.
while ($level-- > 0 && $tree) {
// Loop through the current level's elements until we find one that is in
// the active trail.
while ($element = array_shift($tree)) {
if ($element->inActiveTrail) {
// If the element is in the active trail, we continue in the subtree.
$tree = $element->subtree;
break;
}
}
}
return $tree;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuActiveTrail.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Provides the default implementation of the active menu trail service.
*
* It uses the current route name and route parameters to compare with the ones
* of the menu links.
*/
class MenuActiveTrail implements MenuActiveTrailInterface {
/**
* The menu link plugin manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The route match object for the current page.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a \Drupal\Core\Menu\MenuActiveTrail object.
*
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link plugin manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A route match object for finding the active link.
*/
public function __construct(MenuLinkManagerInterface $menu_link_manager, RouteMatchInterface $route_match) {
$this->menuLinkManager = $menu_link_manager;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function getActiveTrailIds($menu_name) {
// Parent ids; used both as key and value to ensure uniqueness.
// We always want all the top-level links with parent == ''.
$active_trail = array('' => '');
// If a link in the given menu indeed matches the route, then use it to
// complete the active trail.
if ($active_link = $this->getActiveLink($menu_name)) {
if ($parents = $this->menuLinkManager->getParentIds($active_link->getPluginId())) {
$active_trail = $parents + $active_trail;
}
}
return $active_trail;
}
/**
* {@inheritdoc}
*/
public function getActiveTrailCacheKey($menu_name) {
return 'menu_trail.' . implode('|', $this->getActiveTrailIds($menu_name));
}
/**
* {@inheritdoc}
*/
public function getActiveLink($menu_name = NULL) {
// Note: this is a very simple implementation. If you need more control
// over the return value, such as matching a prioritized list of menu names,
// you should substitute your own implementation for the 'menu.active_trail'
// service in the container.
// The menu links coming from the storage are already sorted by depth,
// weight and ID.
$found = NULL;
$route_name = $this->routeMatch->getRouteName();
// On a default (not custom) 403 page the route name is NULL. On a custom
// 403 page we will get the route name for that page, so we can consider
// it a feature that a relevant menu tree may be displayed.
if ($route_name) {
$route_parameters = $this->routeMatch->getRawParameters()->all();
// Load links matching this route.
$links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name);
// Select the first matching link.
if ($links) {
$found = reset($links);
}
}
return $found;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuActiveTrailInterface.
*/
namespace Drupal\Core\Menu;
/**
* Defines an interface for the active menu trail service.
*
* The active trail of a given menu is the trail from the current page to the
* root of that menu's tree.
*/
interface MenuActiveTrailInterface {
/**
* Gets the active trail IDs of the specified menu tree.
*
* @param string $menu_name
* The menu name of the requested tree.
*
* @return array
* An array containing the active trail: a list of plugin IDs.
*/
public function getActiveTrailIds($menu_name);
/**
* Gets the active trail cache key of the specified menu tree.
*
* @param string $menu_name
* The menu name of the requested tree.
*
* @return string
* The cache key that uniquely identifies the active trail of the menu tree.
*/
public function getActiveTrailCacheKey($menu_name);
/**
* Fetches a menu link which matches the route name, parameters and menu name.
*
* @param string|NULL $menu_name
* (optional) The menu within which to find the active link. If omitted, all
* menus will be searched.
*
* @return \Drupal\Core\Menu\MenuLinkInterface|NULL
* The menu link for the given route name, parameters and menu, or NULL if
* there is no matching menu link or the current user cannot access the
* current page (i.e. we have a 403 response).
*/
public function getActiveLink($menu_name = NULL);
}
......@@ -91,7 +91,7 @@ public function isExpanded() {
/**
* {@inheritdoc}
*/
public function isResetable() {
public function isResettable() {
return FALSE;
}
......
......@@ -66,7 +66,7 @@ public static function create(ContainerInterface $container, array $configuratio
/**
* {@inheritdoc}
*/
public function isResetable() {
public function isResettable() {
// The link can be reset if it has an override.
return (bool) $this->staticOverride->loadOverride($this->getPluginId());
}
......
......@@ -88,7 +88,7 @@ public function isExpanded();
* @return bool
* TRUE if it can be reset, FALSE otherwise.
*/
public function isResetable();
public function isResettable();
/**
* Returns whether this link can be translated.
......
......@@ -255,7 +255,7 @@ public function deleteLinksInMenu($menu_name) {
if ($instance->isDeletable()) {
$this->deleteInstance($instance, TRUE);
}
elseif ($instance->isResetable()) {
elseif ($instance->isResettable()) {
$new_instance = $this->resetInstance($instance);
$affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
}
......@@ -395,7 +395,7 @@ public function resetLink($id) {
protected function resetInstance(MenuLinkInterface $instance) {
$id = $instance->getPluginId();
if (!$instance->isResetable()) {
if (!$instance->isResettable()) {
throw new PluginException(String::format('Menu link %id is not resettable', array('%id' => $id)));
}
// Get the original data from disk, reset the override and re-save the menu
......
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkTree.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Routing\RouteProviderInterface;
/**
* Implements the loading, transforming and rendering of menu link trees.
*/
class MenuLinkTree implements MenuLinkTreeInterface {
/**
* The menu link tree storage.
*
* @var \Drupal\Core\Menu\MenuTreeStorageInterface
*/
protected $treeStorage;
/**
* The route provider to load routes by name.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The active menu trail service.
*
* @var \Drupal\Core\Menu\MenuActiveTrailInterface
*/
protected $menuActiveTrail;
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* Constructs a \Drupal\Core\Menu\MenuLinkTree object.
*
* @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
* The menu link tree storage.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link plugin manager.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider to load routes by name.
* @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, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) {
$this->treeStorage = $tree_storage;
$this->menuLinkManager = $menu_link_manager;
$this->routeProvider = $route_provider;
$this->menuActiveTrail = $menu_active_trail;
$this->controllerResolver = $controller_resolver;
}
/**
* {@inheritdoc}
*/
public function getCurrentRouteMenuTreeParameters($menu_name) {
$active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name);
$parameters = new MenuTreeParameters();
$parameters->setActiveTrail($active_trail)
// We want links in the active trail to be expanded.
->addExpandedParents($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.
->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail));
return $parameters;
}
/**
* {@inheritdoc}
*/
public function load($menu_name, MenuTreeParameters $parameters) {
$data = $this->treeStorage->loadTreeData($menu_name, $parameters);
// Pre-load all the route objects in the tree for access checks.
if ($data['route_names']) {
$this->routeProvider->getRoutesByNames($data['route_names']);
}
return $this->createInstances($data['tree']);
}
/**
* Returns a tree containing of MenuLinkTreeElement based upon tree data.
*
* This method converts the tree representation as array coming from the tree
* storage to a tree containing a list of MenuLinkTreeElement[].
*
* @param array $data_tree
* The tree data coming from the menu tree storage.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* An array containing the elements of a menu tree.
*/
protected function createInstances(array $data_tree) {
$tree = array();
foreach ($data_tree as $key => $element) {
$subtree = $this->createInstances($element['subtree']);
// Build a MenuLinkTreeElement out of the menu tree link definition:
// transform the tree link definition into a link definition and store
// tree metadata.
$tree[$key] = new MenuLinkTreeElement(
$this->menuLinkManager->createInstance($element['definition']['id']),
(bool) $element['has_children'],
(int) $element['depth'],
(bool) $element['in_active_trail'],
$subtree
);
}
return $tree;
}
/**
* {@inheritdoc}
*/
public function transform(array $tree, array $manipulators) {
foreach ($manipulators as $manipulator) {
$callable = $manipulator['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) {
$class = array();
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $data->link;
// Generally we only deal with visible links, but just in case.
if ($link->isHidden()) {
continue;
}
// Set a class for the <li>-tag. Only set 'expanded' class if the link
// also has visible children within the current tree.
if ($data->hasChildren && !empty($data->subtree)) {
$class[] = 'expanded';
}
elseif ($data->hasChildren) {
$class[] = 'collapsed';
}
else {
$class[] = 'leaf';
}
// Set a class if the link is in the active trail.
if ($data->inActiveTrail) {
$class[] = 'active-trail';
}
// Allow menu-specific theme overrides.
$element['#theme'] = 'menu_link__' . strtr($link->getMenuName(), '-', '_');
$element['#attributes']['class'] = $class;
$element['#title'] = $link->getTitle();
$element['#url'] = $link->getUrlObject();
$element['#below'] = $data->subtree ? $this->build($data->subtree) : array();
if (isset($data->options)) {
$element['#url']->setOptions(NestedArray::mergeDeep($element['#url']->getOptions(), $data->options));
}
$element['#original_link'] = $link;
// Index using the link's unique ID.
$build[$link->getPluginId()] = $element;
}
if ($build) {
// Make sure drupal_render() does not re-order the links.
$build['#sorted'] = TRUE;
// Get the menu name from the last link.
$menu_name = $link->getMenuName();
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme_wrappers'][] = 'menu_tree__' . strtr($menu_name, '-', '_');
// Set cache tag.
$build['#cache']['tags']['menu'][$menu_name] = $menu_name;
}
return $build;
}
/**
* {@inheritdoc}
*/
public function maxDepth() {
return $this->treeStorage->maxDepth();
}
/**
* {@inheritdoc}
*/
public function getSubtreeHeight($id) {
return $this->treeStorage->getSubtreeHeight($id);
}
/**
* {@inheritdoc}
*/
public function getExpanded($menu_name, array $parents) {
return $this->treeStorage->getExpanded($menu_name, $parents);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkTreeElement.
*/
namespace Drupal\Core\Menu;
/**
* Provides a value object to model an element in a menu link tree.
*
* \Drupal\Core\Menu\MenuLinkTreeElement objects represent a menu link's data.
* Objects of this class provide complimentary data: the placement in a tree.
* Therefore, we can summarize this split as follows:
* - Menu link objects contain all information about an individual menu link,
* plus what their parent is. But they don't know where exactly in a menu link
* tree they live.
* - Instances of this class are complimentary to those objects, they know:
* 1. all additional metadata from {menu_tree}, which contains "materialized"
* metadata about a menu link tree, such as whether a link in the tree has
* visible children and the depth relative to the root;
* 2. plus all additional metadata that's adjusted for the current tree query,
* such as whether the link is in the active trail, whether the link is
* accessible for the current user, and the link's children (which are only
* loaded if the link was marked as "expanded" by the query).
*
* @see \Drupal\Core\Menu\MenuTreeStorage::loadTreeData()
*/
class MenuLinkTreeElement {
/**
* The menu link for this element in a menu link tree.
*
* @var \Drupal\Core\Menu\MenuLinkInterface
*/
public $link;
/**
* The subtree of this element in the menu link tree (this link's children).
*
* (Children of a link are only loaded if a link is marked as "expanded" by
* the query.)
*
* @var \Drupal\Core\Menu\MenuLinkTreeElement[]
*/
public $subtree;
/**
* The depth of this link relative to the root of the tree.
*
* @var int
*/
public $depth;
/**
* Whether this link has any children at all.
*
* @var bool
*/
public $hasChildren;
/**
* Whether this link is in the active trail.
*
* @var bool
*/
public $inActiveTrail;
/**
* Whether this link is accessible by the current user.
*
* If the value is NULL the access was not determined yet, if Boolean it was
* determined already.
*
* @var bool|NULL
*/
public $access;
/**
* Additional options for this link.
*
* This is merged (\Drupal\Component\Utility\NestedArray::mergeDeep()) with
* \Drupal\Core\Menu\MenuLinkInterface::getOptions(), to allow menu link tree
* manipulators to add or override link options.