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

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

parent 1a51606b
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']
<?php
/**
* @file
* Contains \Drupal\menu_link\Tests\MenuTreeTest.
*/
namespace Drupal\menu_link\Tests;
use Drupal\menu_link\MenuTree;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Tests the menu tree.
*
* @group Drupal
* @group menu_link
*
* @coversDefaultClass \Drupal\menu_link\MenuTree
*/
class MenuTreeTest extends UnitTestCase {
/**
* The tested menu tree.
*
* @var \Drupal\menu_link\MenuTree|\Drupal\menu_link\Tests\TestMenuTree
*/
protected $menuTree;
/**
* The mocked database connection.
*
* @var \Drupal\Core\DatabaseConnection|\PHPUnit_Framework_MockObject_MockObject
*/
protected $connection;
/**
* The mocked cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cacheBackend;
/**
* The mocked language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
/**
* The test request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack.
*/
protected $requestStack;
/**
* The mocked entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityManager;
/**
* The mocked entity query factor.y
*
* @var \Drupal\Core\Entity\Query\QueryFactory|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityQueryFactory;
/**
* The mocked state.
*
* @var \Drupal\Core\KeyValueStore\StateInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $state;
/**
* Stores some default values for a menu link.
*
* @var array
*/
protected $defaultMenuLink = array(
'menu_name' => 'main-menu',
'mlid' => 1,
'title' => 'Example 1',
'route_name' => 'example1',
'link_path' => 'example1',
'access' => 1,
'hidden' => FALSE,
'has_children' => FALSE,
'in_active_trail' => TRUE,
'localized_options' => array('attributes' => array('title' => '')),
'weight' => 0,
);
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Tests \Drupal\menu_link\MenuTree',
'description' => '',
'group' => 'Menu',
);
}
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
$this->requestStack = new RequestStack();
$this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$this->entityQueryFactory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory')
->disableOriginalConstructor()
->getMock();
$this->state = $this->getMock('Drupal\Core\KeyValueStore\StateInterface');
$this->menuTree = new TestMenuTree($this->connection, $this->cacheBackend, $this->languageManager, $this->requestStack, $this->entityManager, $this->entityQueryFactory, $this->state);
}
/**
* Tests active paths.
*
* @covers ::setPath
* @covers ::getPath
*/
public function testActivePaths() {
$this->assertNull($this->menuTree->getPath('test_menu1'));
$this->menuTree->setPath('test_menu1', 'example_path1');
$this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1'));
$this->assertNull($this->menuTree->getPath('test_menu2'));
$this->menuTree->setPath('test_menu2', 'example_path2');
$this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1'));
$this->assertEquals('example_path2', $this->menuTree->getPath('test_menu2'));
}
/**
* Tests buildTreeData with a single level.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeDataWithSingleLevel() {
$items = array();
$items[] = array(
'mlid' => 1,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example1',
'access' => TRUE,
);
$items[] = array(
'mlid' => 2,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example2',
'access' => TRUE,
);
$result = $this->menuTree->buildTreeData($items, array(), 1);
$this->assertCount(2, $result);
$result1 = array_shift($result);
$this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']);
$result2 = array_shift($result);
$this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
}
/**
* Tests buildTreeData with a single level and one item being active.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeDataWithSingleLevelAndActiveItem() {
$items = array();
$items[] = array(
'mlid' => 1,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example1',
'access' => TRUE,
);
$items[] = array(
'mlid' => 2,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example2',
'access' => TRUE,
);
$result = $this->menuTree->buildTreeData($items, array(1), 1);
$this->assertCount(2, $result);
$result1 = array_shift($result);
$this->assertEquals($items[0] + array('in_active_trail' => TRUE), $result1['link']);
$result2 = array_shift($result);
$this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
}
/**
* Tests buildTreeData with a single level and none item being active.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeDataWithSingleLevelAndNoActiveItem() {
$items = array();
$items[] = array(
'mlid' => 1,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example1',
'access' => TRUE,
);
$items[] = array(
'mlid' => 2,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example2',
'access' => TRUE,
);
$result = $this->menuTree->buildTreeData($items, array(3), 1);
$this->assertCount(2, $result);
$result1 = array_shift($result);
$this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']);
$result2 = array_shift($result);
$this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
}
/**
* Tests buildTreeData with a more complex example.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeWithComplexData() {
$items = array(
1 => array('mlid' => 1, 'depth' => 1, 'route_name' => 'example1', 'access' => TRUE, 'weight' => 0, 'title' => ''),
2 => array('mlid' => 2, 'depth' => 1, 'route_name' => 'example2', 'access' => TRUE, 'weight' => 0, 'title' => ''),
3 => array('mlid' => 3, 'depth' => 2, 'route_name' => 'example3', 'access' => TRUE, 'weight' => 0, 'title' => ''),
4 => array('mlid' => 4, 'depth' => 3, 'route_name' => 'example4', 'access' => TRUE, 'weight' => 0, 'title' => ''),
5 => array('mlid' => 5, 'depth' => 1, 'route_name' => 'example5', 'access' => TRUE, 'weight' => 0, 'title' => ''),
);
$tree = $this->menuTree->buildTreeData($items);
// Validate that parent items #1, #2, and #5 exist on the root level.
$this->assertEquals($items[1]['mlid'], $tree['50000 1']['link']['mlid']);
$this->assertEquals($items[2]['mlid'], $tree['50000 2']['link']['mlid']);
$this->assertEquals($items[5]['mlid'], $tree['50000 5']['link']['mlid']);
// Validate that child item #4 exists at the correct location in the hierarchy.
$this->assertEquals($items[4]['mlid'], $tree['50000 2']['below']['50000 3']['below']['50000 4']['link']['mlid']);
}
/**
* Tests the output with a single level.
*
* @covers ::output
*/
public function testOutputWithSingleLevel() {
$tree = array(
'1' => array(
'link' => array('mlid' => 1) + $this->defaultMenuLink,
'below' => array(),
),
'2' => array(
'link' => array('mlid' => 2) + $this->defaultMenuLink,
'below' => array(),
),
);
$output = $this->menuTree->renderTree($tree);
// Validate that the - in main-menu is changed into an underscore
$this->assertEquals($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link');
$this->assertEquals($output['2']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link');
$this->assertEquals($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper');
}
/**
* Tests the output method with a complex example.
*
* @covers ::output
*/
public function testOutputWithComplexData() {
$tree = array(
'1'=> array(
'link' => array('mlid' => 1, 'has_children' => 1, 'title' => 'Item 1', 'link_path' => 'a') + $this->defaultMenuLink,
'below' => array(
'2' => array('link' => array('mlid' => 2, 'title' => 'Item 2', 'link_path' => 'a/b') + $this->defaultMenuLink,
'below' => array(
'3' => array('link' => array('mlid' => 3, 'title' => 'Item 3', 'in_active_trail' => 0, 'link_path' => 'a/b/c') + $this->defaultMenuLink,
'below' => array()),
'4' => array('link' => array('mlid' => 4, 'title' => 'Item 4', 'in_active_trail' => 0, 'link_path' => 'a/b/d') + $this->defaultMenuLink,
'below' => array())
)
)
)
),
'5' => array('link' => array('mlid' => 5, 'hidden' => 1, 'title' => 'Item 5', 'link_path' => 'e') + $this->defaultMenuLink, 'below' => array()),
'6' => array('link' => array('mlid' => 6, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'link_path' => 'f') + $this->defaultMenuLink, 'below' => array()),
'7' => array('link' => array('mlid' => 7, 'title' => 'Item 7', 'in_active_trail' => 0, 'link_path' => 'g') + $this->defaultMenuLink, 'below' => array())
);
$output = $this->menuTree->renderTree($tree);
// Looking for child items in the data
$this->assertEquals( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item');
$this->assertTrue(in_array('active-trail', $output['1']['#below']['2']['#attributes']['class']), 'Checking the active trail class');
// Validate that the hidden and no access items are missing
$this->assertFalse(isset($output['5']), 'Hidden item should be missing');