Commit 4a57034d authored by catch's avatar catch

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

parent 1a51606b
This diff is collapsed.
......@@ -12,6 +12,7 @@
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;
/**
......@@ -33,6 +34,13 @@ class MenuFormController extends EntityFormController {
*/
protected $menuLinkStorage;
/**
* The menu tree service.
*
* @var \Drupal\menu_link\MenuTreeInterface
*/
protected $menuTree;
/**
* The overview tree form.
*
......@@ -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 @@ public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageC
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 @@ protected function buildOverviewForm(array &$form, array &$form_state) {
}
$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));
......
......@@ -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);
}
......
This diff is collapsed.
<?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());
}
services:
menu_link.tree:
class: Drupal\menu_link\MenuTree
arguments: ['@database', '@cache.data', '@language_manager', '@request_stack', '@entity.manager', '@entity.query', '@state']
......@@ -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();
......
......@@ -9,6 +9,10 @@
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 @@
* 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);
}
/**
......
<?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');
}
}
<?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');
}
}
<?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');
}
}
......@@ -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().';
}
......
......@@ -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 {
......
......@@ -67,7 +67,9 @@ function testSecondaryMenu() {
$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'];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment