Commit fd2db9cd authored by catch's avatar catch

Issue #2301317 by pwolanin, effulgentsia, Wim Leers, dawehner, alexpott:...

Issue #2301317 by pwolanin, effulgentsia, Wim Leers, dawehner, alexpott: MenuLinkNG part4: Conversion.
parent 16caf92d
......@@ -277,7 +277,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', '@menu.active_trail', '@controller_resolver']
arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver', '@cache.menu', '@current_route_match']
menu.default_tree_manipulators:
class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
arguments: ['@access_manager', '@current_user']
......@@ -443,7 +443,12 @@ services:
arguments: ['@router.dumper', '@lock', '@event_dispatcher', '@module_handler', '@controller_resolver', '@state']
router.rebuild_subscriber:
class: Drupal\Core\EventSubscriber\RouterRebuildSubscriber
arguments: ['@router.builder', '@lock']
arguments: ['@router.builder']
tags:
- { name: event_subscriber }
menu.rebuild_subscriber:
class: Drupal\Core\EventSubscriber\MenuRouterRebuildSubscriber
arguments: ['@lock', '@plugin.manager.menu.link']
tags:
- { name: event_subscriber }
path.alias_storage:
......
......@@ -153,8 +153,8 @@
* - weight: Lower (negative) numbers come before higher (positive) numbers,
* for menu items with the same parent.
*
* Menu items from other modules can be altered using
* hook_menu_link_defaults_alter().
* Discovered menu links from other modules can be altered using
* hook_menu_links_discovered_alter().
*
* @todo Derivatives will probably be defined for these; when they are, add
* documentation here.
......@@ -264,18 +264,56 @@
const MENU_MAX_DEPTH = 9;
/**
* Reserved key to identify the most specific menu link for a given path.
* @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.
*
* The value of this constant is a hash of the constant name. We use the hash
* so that the reserved key is over 32 characters in length and will not
* collide with allowed menu names:
* 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::getCurrentRouteMenuTreeParameters() 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\Core\Menu\MenuLinkTree::build(), this will
* build a renderable array.
*
* Combined, that would look like this:
* @code
* sha1('MENU_PREFERRED_LINK') = 1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91
* @endcode
* $menu_tree = \Drupal::menuTree();
* $menu_name = 'my_menu';
*
* // Build the typical default set of menu tree parameters.
* $parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);
*
* // Load the tree based on this set of parameters.
* $tree = $menu_tree->load($menu_name, $parameters);
*
* // Transform the tree using the manipulators you want.
* $manipulators = array(
* // Only show links that are accessible for the current user.
* array('callable' => 'menu.default_tree_manipulators:checkAccess'),
* // Use the default sorting of menu links.
* array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
* );
* $tree = $menu_tree->transform($tree, $manipulators);
*
* // Finally, build a renderable array from the transformed tree.
* $menu = $menu_tree->build($tree);
*
* @see menu_link_get_preferred()
* $menu_html = drupal_render($menu);
* @endcode
*/
const MENU_PREFERRED_LINK = '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91';
/**
* Localizes a menu link title using t() if possible.
......@@ -367,21 +405,40 @@ function _menu_link_translate(&$item) {
* Implements template_preprocess_HOOK() for theme_menu_tree().
*/
function template_preprocess_menu_tree(&$variables) {
$variables['tree'] = $variables['tree']['#children'];
}
if (isset($variables['tree']['#heading'])) {
$variables['heading'] = $variables['tree']['#heading'];
$heading = &$variables['heading'];
// Convert a string heading into an array, using a H2 tag by default.
if (is_string($heading)) {
$heading = array('text' => $heading);
}
// Merge in default array properties into $heading.
$heading += array(
'level' => 'h2',
'attributes' => array(),
);
// @todo Remove backwards compatibility for $heading['class'].
// https://www.drupal.org/node/2310341
if (isset($heading['class'])) {
$heading['attributes']['class'] = $heading['class'];
}
// Convert the attributes array into an Attribute object.
$heading['attributes'] = new Attribute($heading['attributes']);
$heading['text'] = String::checkPlain($heading['text']);
}
/**
* Returns HTML for a wrapper for a menu sub-tree.
*
* @param $variables
* An associative array containing:
* - tree: An HTML string containing the tree's items.
*
* @see template_preprocess_menu_tree()
* @ingroup themeable
*/
function theme_menu_tree($variables) {
return '<ul class="menu">' . $variables['tree'] . '</ul>';
if (isset($variables['tree']['#attributes'])) {
$variables['attributes'] = new Attribute($variables['tree']['#attributes']);
}
else {
$variables['attributes'] = new Attribute();
}
if (!isset($variables['attributes']['class'])) {
$variables['attributes']['class'] = array();
}
$variables['attributes']['class'][] = 'menu';
$variables['tree'] = $variables['tree']['#children'];
}
/**
......@@ -400,8 +457,10 @@ function theme_menu_link(array $variables) {
if ($element['#below']) {
$sub_menu = drupal_render($element['#below']);
}
$element['#localized_options']['set_active_class'] = TRUE;
$output = l($element['#title'], $element['#href'], $element['#localized_options']);
/** @var \Drupal\Core\Url $url */
$url = $element['#url'];
$url->setOption('set_active_class', TRUE);
$output = \Drupal::linkGenerator()->generateFromUrl($element['#title'], $url);
return '<li' . new Attribute($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
}
......@@ -541,56 +600,28 @@ function _menu_get_links_source($name, $default) {
}
/**
* Returns an array of links for a navigation menu.
* Builds a renderable array for a navigation menu.
*
* @param $menu_name
* @param string $menu_name
* The name of the menu.
* @param $level
* @param int $level
* Optional, the depth of the menu to be returned.
*
* @return
* An array of links of the specified menu and level.
* @return array
* A renderable array.
*/
function menu_navigation_links($menu_name, $level = 0) {
// Don't even bother querying the menu table if no menu is specified.
if (empty($menu_name)) {
return array();
}
// Get the menu hierarchy for the current page.
/** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
$menu_tree = \Drupal::service('menu_link.tree');
$tree = $menu_tree->buildPageData($menu_name, $level + 1);
// Go down the active trail until the right level is reached.
while ($level-- > 0 && $tree) {
// Loop through the current level's items until we find one that is in trail.
while ($item = array_shift($tree)) {
if ($item['link']['in_active_trail']) {
// If the item is in the active trail, we continue in the subtree.
$tree = empty($item['below']) ? array() : $item['below'];
break;
}
}
}
// Create a single level of links.
$links = array();
foreach ($tree as $item) {
if (!$item['link']['hidden']) {
$class = '';
$l = $item['link']['localized_options'];
$l['href'] = $item['link']['link_path'];
$l['title'] = $item['link']['title'];
if ($item['link']['in_active_trail']) {
$class = ' active-trail';
$l['attributes']['class'][] = 'active-trail';
}
// Keyed with the unique mlid to generate classes in links.html.twig.
$links['menu-' . $item['link']['mlid'] . $class] = $l;
}
}
return $links;
$menu_tree = \Drupal::menuTree();
$parameters = $menu_tree->getCurrentRouteMenuTreeParameters($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->build($tree);
}
/**
......@@ -853,17 +884,7 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
* might have been made to the router items or menu links.
*/
function menu_cache_clear_all() {
\Drupal::cache('data')->deleteAll();
menu_reset_static_cache();
}
/**
* Resets the menu system static cache.
*/
function menu_reset_static_cache() {
\Drupal::entityManager()
->getStorage('menu_link')->resetCache();
drupal_static_reset('menu_link_get_preferred');
\Drupal::cache('menu')->invalidateAll();
}
/**
......
......@@ -2145,27 +2145,17 @@ function template_preprocess_page(&$variables) {
// Pass the main menu and secondary menu to the template as render arrays.
if (!empty($variables['main_menu'])) {
$variables['main_menu'] = array(
'#theme' =>'links__system_main_menu',
'#links' => $variables['main_menu'],
'#heading' => array(
'text' => t('Main menu'),
'class' => array('visually-hidden'),
'attributes' => array('id' => 'links__system_main_menu'),
),
'#set_active_class' => TRUE,
$variables['main_menu']['#heading'] = array(
'text' => t('Main menu'),
'class' => array('visually-hidden'),
'attributes' => array('id' => 'links__system_main_menu'),
);
}
if (!empty($variables['secondary_menu'])) {
$variables['secondary_menu'] = array(
'#theme' =>'links__system_secondary_menu',
'#links' => $variables['secondary_menu'],
'#heading' => array(
'text' => t('Secondary menu'),
'class' => array('visually-hidden'),
'attributes' => array('id' => 'links__system_secondary_menu'),
),
'#set_active_class' => TRUE,
$variables['secondary_menu']['#heading'] = array(
'text' => t('Secondary menu'),
'class' => array('visually-hidden'),
'attributes' => array('id' => 'links__system_secondary_menu'),
);
}
......@@ -2651,6 +2641,7 @@ function drupal_common_theme() {
),
'menu_tree' => array(
'render element' => 'tree',
'template' => 'menu-tree',
),
'menu_local_task' => array(
'render element' => 'element',
......
......@@ -647,4 +647,14 @@ public static function logger($channel) {
return static::$container->get('logger.factory')->get($channel);
}
/**
* Returns the menu tree.
*
* @return \Drupal\Core\Menu\MenuLinkTreeInterface
* The menu tree.
*/
public static function menuTree() {
return static::$container->get('menu.link_tree');
}
}
......@@ -255,10 +255,7 @@ protected function getTableMapping($table, $entity_type_id) {
$mapping = $storage->getTableMapping()->getAllColumns($table);
}
else {
// @todo Stop calling drupal_get_schema() once menu links are converted
// to the Entity Field API. See https://drupal.org/node/1842858.
$schema = drupal_get_schema($table);
$mapping = array_keys($schema['fields']);
return FALSE;
}
return array_flip($mapping);
}
......
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\MenuRouterRebuildSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Rebuilds the default menu links and runs menu-specific code if necessary.
*/
class MenuRouterRebuildSubscriber implements EventSubscriberInterface {
/**
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routeBuilder;
/**
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* The menu link plugin manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
*/
protected $menuLinkManager;
/**
* Constructs the MenuRouterRebuildSubscriber object.
*
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link plugin manager.
*/
public function __construct(LockBackendInterface $lock, MenuLinkManagerInterface $menu_link_manager) {
$this->lock = $lock;
$this->menuLinkManager = $menu_link_manager;
}
/**
* Rebuilds the menu links and deletes the local_task cache tag.
*
* @param \Symfony\Component\EventDispatcher\Event $event
* The event object.
*/
public function onRouterRebuild(Event $event) {
$this->menuLinksRebuild();
Cache::deleteTags(array('local_task' => 1));
}
/**
* Perform menu-specific rebuilding.
*/
protected function menuLinksRebuild() {
if ($this->lock->acquire(__FUNCTION__)) {
$transaction = db_transaction();
try {
// Ensure the menu links are up to date.
$this->menuLinkManager->rebuild();
// Ignore any database replicas temporarily.
db_ignore_replica();
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception('menu', $e);
}
$this->lock->release(__FUNCTION__);
}
else {
// Wait for another request that is already doing this work.
// We choose to block here since otherwise the router item may not
// be available during routing resulting in a 404.
$this->lock->wait(__FUNCTION__);
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[RoutingEvents::FINISHED][] = array('onRouterRebuild', 200);
return $events;
}
}
......@@ -17,7 +17,7 @@
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Rebuilds the default menu links and runs menu-specific code if necessary.
* Rebuilds the router if needed at the end of the request.
*/
class RouterRebuildSubscriber implements EventSubscriberInterface {
......@@ -26,22 +26,14 @@ class RouterRebuildSubscriber implements EventSubscriberInterface {
*/
protected $routeBuilder;
/**
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* Constructs the RouterRebuildSubscriber object.
*
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The route builder.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
*/
public function __construct(RouteBuilderInterface $route_builder, LockBackendInterface $lock) {
public function __construct(RouteBuilderInterface $route_builder) {
$this->routeBuilder = $route_builder;
$this->lock = $lock;
}
/**
......@@ -55,54 +47,10 @@ public function onKernelTerminate(PostResponseEvent $event) {
}
/**
* Rebuilds the menu links and deletes the local_task cache tag.
*
* @param \Symfony\Component\EventDispatcher\Event $event
* The event object.
*/
public function onRouterRebuild(Event $event) {
$this->menuLinksRebuild();
Cache::deleteTags(array('local_task' => 1));
}
/**
* Perform menu-specific rebuilding.
*/
protected function menuLinksRebuild() {
if ($this->lock->acquire(__FUNCTION__)) {
$transaction = db_transaction();
try {
// Ensure the menu links are up to date.
menu_link_rebuild_defaults();
// Clear the menu cache.
menu_cache_clear_all();
// Track which menu items are expanded.
_menu_update_expanded_menus();
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception('menu', $e);
}
$this->lock->release(__FUNCTION__);
}
else {
// Wait for another request that is already doing this work.
// We choose to block here since otherwise the router item may not
// be available during routing resulting in a 404.
$this->lock->wait(__FUNCTION__);
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 200);
$events[RoutingEvents::FINISHED][] = array('onRouterRebuild', 200);
return $events;
}
......
......@@ -37,22 +37,6 @@ public function getWeight() {
return $this->pluginDefinition['weight'];
}
/**
* {@inheritdoc}
*/
public function getTitle() {
// Subclasses may pull in the request or specific attributes as parameters.
$options = array();
if (!empty($this->pluginDefinition['title_context'])) {
$options['context'] = $this->pluginDefinition['title_context'];
}
$args = array();
if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) {
$args = (array) $title_arguments;
}
return $this->t($this->pluginDefinition['title'], $args, $options);
}
/**
* {@inheritdoc}
*/
......@@ -109,16 +93,6 @@ public function isDeletable() {
return (bool) $this->getDeleteRoute();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
if ($this->pluginDefinition['description']) {
return $this->t($this->pluginDefinition['description']);
}
return '';
}
/**
* {@inheritdoc}
*/
......
......@@ -63,6 +63,32 @@ public static function create(ContainerInterface $container, array $configuratio
);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
// Subclasses may pull in the request or specific attributes as parameters.
$options = array();
if (!empty($this->pluginDefinition['title_context'])) {
$options['context'] = $this->pluginDefinition['title_context'];
}
$args = array();
if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) {
$args = (array) $title_arguments;
}
return $this->t($this->pluginDefinition['title'], $args, $options);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
if ($this->pluginDefinition['description']) {
return $this->t($this->pluginDefinition['description']);
}
return '';
}
/**
* {@inheritdoc}
*/
......@@ -77,11 +103,13 @@ public function isResettable() {
public function updateLink(array $new_definition_values, $persist) {
// Filter the list of updates to only those that are allowed.
$overrides = array_intersect_key($new_definition_values, $this->overrideAllowed);
// Update the definition.
$this->pluginDefinition = $overrides + $this->getPluginDefinition();
if ($persist) {
// Always save the menu name as an override to avoid defaulting to tools.
$overrides['menu_name'] = $this->pluginDefinition['menu_name'];
$this->staticOverride->saveOverride($this->getPluginId(), $overrides);
}
// Update the definition.
$this->pluginDefinition = $overrides + $this->getPluginDefinition();
return $this->pluginDefinition;
}
......
......@@ -8,7 +8,9 @@
namespace Drupal\Core\Menu;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
/**
......@@ -44,6 +46,30 @@ class MenuLinkTree implements MenuLinkTreeInterface {
*/
protected $controllerResolver;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Stores the cached current route parameters by menu and current route match.
*
* @todo Remove this non-static caching in
* https://www.drupal.org/node/1805054.
*
* @var \Drupal\Core\Menu\MenuTreeParameters[]
*/
protected $cachedCurrentRouteParameters;
/**
* Constructs a \Drupal\Core\Menu\MenuLinkTree object.
*
......@@ -57,31 +83,53 @@ class MenuLinkTree implements MenuLinkTreeInterface {
* The active menu trail service.
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, 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, CacheBackendInterface $cache, RouteMatchInterface $route_match) {
$this->treeStorage = $tree_storage;
$this->menuLinkManager = $menu_link_manager;
$this->routeProvider = $route_provider;
$this->menuActiveTrail = $menu_active_trail;
$this->controllerResolver = $controller_resolver;
// @todo Remove these two in https://www.drupal.org/node/1805054.
$this->cache = $cache;
$this->routeMatch = $route_match;
}
/**
* {@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;
$route_parameters = $this->routeMatch->getRawParameters()->all();
ksort($route_parameters);
$cid = 'current-route-parameters:' . $menu_name . ':route:' . $this->routeMatch->getRouteName() . ':route_parameters:' . serialize($route_parameters);