Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkDefault.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Plugin\PluginBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a common base implementation of a contextual link.
*/
class ContextualLinkDefault extends PluginBase implements ContextualLinkInterface {
/**
* {@inheritdoc}
*
* @todo: It might be helpful at some point to move this getTitle logic into
* a trait.
*/
public function getTitle(Request $request = NULL) {
$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 getRouteName() {
return $this->pluginDefinition['route_name'];
}
/**
* {@inheritdoc}
*/
public function getGroup() {
return $this->pluginDefinition['group'];
}
/**
* {@inheritdoc}
*/
public function getOptions() {
return $this->pluginDefinition['options'];
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->pluginDefinition['weight'];
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkInterface.
*/
namespace Drupal\Core\Menu;
/**
* Defines a contextual link plugin.
*/
interface ContextualLinkInterface {
/**
* Returns the localized title to be shown for this contextual link.
*
* Subclasses may add optional arguments like NodeInterface $node = NULL that
* will be supplied by the ControllerResolver.
*
* @return string
* The title to be shown for this action.
*
* @see \Drupal\Core\Menu\ContextualLinksManager::getTitle()
*/
public function getTitle();
/**
* Returns the route name of the contextual link.
*
* @return string
* The name of the route this contextual link links to.
*/
public function getRouteName();
/**
* Returns the group this contextual link should be rendered in.
*
* A contextual link group is a set of contextual links that are displayed
* together on a certain page. For example, the 'block' group displays all
* links related to the block, such as the block instance edit link as well as
* the views edit link, if it is a view block.
*
* @return string
* The contextual links group name.
*/
public function getGroup();
/**
* Returns the link options passed to the link generator.
*
* @return array
* An associative array of options.
*/
public function getOptions();
/**
* Returns the weight of the contextual link.
*
* The contextual links in one group are sorted by weight for display.
*
* @return int
* The weight as positive/negative integer.
*/
public function getWeight();
}

View file

@ -0,0 +1,198 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkManager.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Defines a contextual link plugin manager to deal with contextual links.
*
* @see \Drupal\Core\Menu\ContextualLinkInterface
*/
class ContextualLinkManager extends DefaultPluginManager implements ContextualLinkManagerInterface {
/**
* Provides default values for a contextual link definition.
*
* @var array
*/
protected $defaults = array(
// (required) The name of the route to link to.
'route_name' => '',
// (required) The contextual links group.
'group' => '',
// The static title text for the link.
'title' => '',
// The default link options.
'options' => array(),
// The weight of the link.
'weight' => NULL,
// Default class for contextual link implementations.
'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
// The plugin id. Set by the plugin system based on the top-level YAML key.
'id' => '',
);
/**
* A controller resolver object.
*
* @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* A static cache of all the contextual link plugins by group name.
*
* @var array
*/
protected $pluginsByGroup;
/**
* Constructs a new ContextualLinkManager instance.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(ControllerResolverInterface $controller_resolver, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager, AccessManagerInterface $access_manager, AccountInterface $account, RequestStack $request_stack) {
$this->factory = new ContainerFactory($this, '\Drupal\Core\Menu\ContextualLinkInterface');
$this->controllerResolver = $controller_resolver;
$this->accessManager = $access_manager;
$this->account = $account;
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
$this->alterInfo('contextual_links_plugins');
$this->setCacheBackend($cache_backend, 'contextual_links_plugins:' . $language_manager->getCurrentLanguage()->getId(), array('contextual_links_plugins'));
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new YamlDiscovery('links.contextual', $this->moduleHandler->getModuleDirectories());
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
// If there is no route name, this is a broken definition.
if (empty($definition['route_name'])) {
throw new PluginException(sprintf('Contextual link plugin (%s) definition must include "route_name".', $plugin_id));
}
// If there is no group name, this is a broken definition.
if (empty($definition['group'])) {
throw new PluginException(sprintf('Contextual link plugin (%s) definition must include "group".', $plugin_id));
}
}
/**
* {@inheritdoc}
*/
public function getContextualLinkPluginsByGroup($group_name) {
if (isset($this->pluginsByGroup[$group_name])) {
$contextual_links = $this->pluginsByGroup[$group_name];
}
elseif ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $group_name)) {
$contextual_links = $cache->data;
$this->pluginsByGroup[$group_name] = $contextual_links;
}
else {
$contextual_links = array();
foreach ($this->getDefinitions() as $plugin_id => $plugin_definition) {
if ($plugin_definition['group'] == $group_name) {
$contextual_links[$plugin_id] = $plugin_definition;
}
}
$this->cacheBackend->set($this->cacheKey . ':' . $group_name, $contextual_links);
$this->pluginsByGroup[$group_name] = $contextual_links;
}
return $contextual_links;
}
/**
* {@inheritdoc}
*/
public function getContextualLinksArrayByGroup($group_name, array $route_parameters, array $metadata = array()) {
$links = array();
$request = $this->requestStack->getCurrentRequest();
foreach ($this->getContextualLinkPluginsByGroup($group_name) as $plugin_id => $plugin_definition) {
/** @var $plugin \Drupal\Core\Menu\ContextualLinkInterface */
$plugin = $this->createInstance($plugin_id);
$route_name = $plugin->getRouteName();
// Check access.
if (!$this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account)) {
continue;
}
$links[$plugin_id] = array(
'route_name' => $route_name,
'route_parameters' => $route_parameters,
'title' => $plugin->getTitle($request),
'weight' => $plugin->getWeight(),
'localized_options' => $plugin->getOptions(),
'metadata' => $metadata,
);
}
$this->moduleHandler->alter('contextual_links', $links, $group_name, $route_parameters);
return $links;
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkManagerInterface.
*/
namespace Drupal\Core\Menu;
/**
* Provides an object which returns the available contextual links.
*/
interface ContextualLinkManagerInterface {
/**
* Gets the contextual link plugins by contextual link group.
*
* @param string $group_name
* The group name.
*
* @return array
* A list of contextual links plugin definitions.
*/
public function getContextualLinkPluginsByGroup($group_name);
/**
* Gets the contextual links prepared as expected by links.html.twig.
*
* @param string $group_name
* The group name.
* @param array $route_parameters
* The incoming route parameters. The route parameters need to have the same
* name on all contextual link routes, e.g. you cannot use 'node' and
* 'entity' in parallel.
* @param array $metadata
* Additional metadata of contextual links, like the position (optional).
*
* @return array
* An array of link information, keyed by the plugin ID. Each entry is an
* associative array with the following keys:
* - route_name: The route name to link to.
* - route_parameters: The route parameters for the contextual link.
* - title: The title of the contextual link.
* - weight: The weight of the contextual link.
* - localized_options: The options of the link, which will be passed
* to the link generator.
* - metadata: The array of additional metadata that was passed in.
*/
public function getContextualLinksArrayByGroup($group_name, array $route_parameters, array $metadata = array());
}

View file

@ -0,0 +1,268 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a couple of menu link tree manipulators.
*
* This class provides menu link tree manipulators to:
* - perform render cached menu-optimized access checking
* - optimized node 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)
*/
class DefaultMenuLinkTreeManipulators {
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The entity query factory.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $queryFactory;
/**
* Constructs a \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators object.
*
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
* The entity query factory.
*/
public function __construct(AccessManagerInterface $access_manager, AccountInterface $account, QueryFactory $query_factory) {
$this->accessManager = $access_manager;
$this->account = $account;
$this->queryFactory = $query_factory;
}
/**
* Performs access checks of a menu tree.
*
* Sets the 'access' property to AccessResultInterface objects on menu link
* tree elements. Descends into subtrees if the root of the subtree is
* accessible. Inaccessible subtrees are deleted, except the top-level
* inaccessible link, to be compatible with render caching.
*
* (This means that top-level inaccessible links are *not* removed; it is up
* to the code doing something with the tree to exclude inaccessible links,
* just like MenuLinkTree::build() does. This allows those things to specify
* the necessary cacheability metadata.)
*
* This is compatible with render caching, because of cache context bubbling:
* conditionally defined cache contexts (i.e. subtrees that are only
* accessible to some users) will bubble just like they do for render arrays.
* This is why inaccessible subtrees are deleted, except at the top-level
* inaccessible link: if we didn't keep the first (depth-wise) inaccessible
* link, we wouldn't be able to know which cache contexts would cause those
* subtrees to become accessible again, thus forcing us to conclude that that
* subtree is unconditionally inaccessible.
*
* @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->isAllowed()) {
if ($tree[$key]->subtree) {
$tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
}
}
else {
// Replace the link with an InaccessibleMenuLink object, so that if it
// is accidentally rendered, no sensitive information is divulged.
$tree[$key]->link = new InaccessibleMenuLink($tree[$key]->link);
// Always keep top-level inaccessible links: their cacheability metadata
// that indicates why they're not accessible by the current user must be
// bubbled. Otherwise, those subtrees will not be varied by any cache
// contexts at all, therefore forcing them to remain empty for all users
// unless some other part of the menu link tree accidentally varies by
// the same cache contexts.
// For deeper levels, we *can* remove the subtrees and therefore also
// not perform access checking on the subtree, thanks to bubbling/cache
// redirects. This therefore allows us to still do significantly less
// work in case of inaccessible subtrees, which is the entire reason why
// this deletes subtrees in the first place.
$tree[$key]->subtree = [];
}
}
return $tree;
}
/**
* Performs access checking for nodes in an optimized way.
*
* This manipulator should be added before the generic ::checkAccess() one,
* because it provides a performance optimization for ::checkAccess().
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function checkNodeAccess(array $tree) {
$node_links = array();
$this->collectNodeLinks($tree, $node_links);
if ($node_links) {
$nids = array_keys($node_links);
$query = $this->queryFactory->get('node');
$query->condition('nid', $nids, 'IN');
// Allows admins to view all nodes, by both disabling node_access
// query rewrite as well as not checking for the node status. The
// 'view own unpublished nodes' permission is ignored to not require cache
// entries per user.
$access_result = AccessResult::allowed()->cachePerPermissions();
if ($this->account->hasPermission('bypass node access')) {
$query->accessCheck(FALSE);
}
else {
$access_result->addCacheContexts(['user.node_grants:view']);
$query->condition('status', NODE_PUBLISHED);
}
$nids = $query->execute();
foreach ($nids as $nid) {
foreach ($node_links[$nid] as $key => $link) {
$node_links[$nid][$key]->access = $access_result;
}
}
}
return $tree;
}
/**
* Collects the node links in the menu tree.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
* @param array $node_links
* Stores references to menu link elements to effectively set access.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
protected function collectNodeLinks(array &$tree, array &$node_links) {
foreach ($tree as $key => &$element) {
if ($element->link->getRouteName() == 'entity.node.canonical') {
$nid = $element->link->getRouteParameters()['node'];
$node_links[$nid][$key] = $element;
// Deny access by default. checkNodeAccess() will re-add it.
$element->access = AccessResult::neutral();
}
if ($element->hasChildren) {
$this->collectNodeLinks($element->subtree, $node_links);
}
}
}
/**
* Checks access for one menu link instance.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $instance
* The menu link instance.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
$access_result = NULL;
if ($this->account->hasPermission('link to any page')) {
$access_result = AccessResult::allowed();
}
else {
// 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_result = AccessResult::allowed();
}
else {
$access_result = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account, TRUE);
}
}
return $access_result->cachePerPermissions();
}
/**
* 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;
}
}

View file

@ -0,0 +1,190 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\Form\MenuLinkDefaultForm.
*/
namespace Drupal\Core\Menu\Form;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\MenuParentFormSelectorInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an edit form for static menu links.
*
* @see \Drupal\Core\Menu\MenuLinkDefault
*/
class MenuLinkDefaultForm implements MenuLinkFormInterface, ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The edited menu link.
*
* @var \Drupal\Core\Menu\MenuLinkInterface
*/
protected $menuLink;
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The parent form selector service.
*
* @var \Drupal\Core\Menu\MenuParentFormSelectorInterface
*/
protected $menuParentSelector;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The module data from system_get_info().
*
* @var array
*/
protected $moduleData;
/**
* Constructs a new \Drupal\Core\Menu\Form\MenuLinkDefaultForm.
*
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
* @param \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector
* The menu parent form selector service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler;
*/
public function __construct(MenuLinkManagerInterface $menu_link_manager, MenuParentFormSelectorInterface $menu_parent_selector, TranslationInterface $string_translation, ModuleHandlerInterface $module_handler) {
$this->menuLinkManager = $menu_link_manager;
$this->menuParentSelector = $menu_parent_selector;
$this->stringTranslation = $string_translation;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.menu.link'),
$container->get('menu.parent_form_selector'),
$container->get('string_translation'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function setMenuLinkInstance(MenuLinkInterface $menu_link) {
$this->menuLink = $menu_link;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['#title'] = $this->t('Edit menu link %title', array('%title' => $this->menuLink->getTitle()));
$provider = $this->menuLink->getProvider();
$form['info'] = array(
'#type' => 'item',
'#title' => $this->t('This link is provided by the @name module. The title and path cannot be edited.', array('@name' => $this->moduleHandler->getName($provider))),
);
$link = array(
'#type' => 'link',
'#title' => $this->menuLink->getTitle(),
'#url' => $this->menuLink->getUrlObject(),
);
$form['path'] = array(
'link' => $link,
'#type' => 'item',
'#title' => $this->t('Link'),
);
$form['enabled'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Enable menu link'),
'#description' => $this->t('Menu links that are not enabled will not be listed in any menu.'),
'#default_value' => $this->menuLink->isEnabled(),
);
$form['expanded'] = array(
'#type' => 'checkbox',
'#title' => t('Show as expanded'),
'#description' => $this->t('If selected and this menu link has children, the menu will always appear expanded.'),
'#default_value' => $this->menuLink->isExpanded(),
);
$menu_parent = $this->menuLink->getMenuName() . ':' . $this->menuLink->getParent();
$form['menu_parent'] = $this->menuParentSelector->parentSelectElement($menu_parent, $this->menuLink->getPluginId());
$form['menu_parent']['#title'] = $this->t('Parent link');
$form['menu_parent']['#description'] = $this->t('The maximum depth for a link and all its children is fixed. Some menu links may not be available as parents if selecting them would exceed this limit.');
$form['menu_parent']['#attributes']['class'][] = 'menu-title-select';
$delta = max(abs($this->menuLink->getWeight()), 50);
$form['weight'] = array(
'#type' => 'number',
'#min' => -$delta,
'#max' => $delta,
'#default_value' => $this->menuLink->getWeight(),
'#title' => $this->t('Weight'),
'#description' => $this->t('Link weight among links in the same menu at the same depth. In the menu, the links with high weight will sink and links with a low weight will be positioned nearer the top.'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(array &$form, FormStateInterface $form_state) {
$new_definition = array();
$new_definition['enabled'] = $form_state->getValue('enabled') ? 1 : 0;
$new_definition['weight'] = (int) $form_state->getValue('weight');
$new_definition['expanded'] = $form_state->getValue('expanded') ? 1 : 0;
list($menu_name, $parent) = explode(':', $form_state->getValue('menu_parent'), 2);
if (!empty($menu_name)) {
$new_definition['menu_name'] = $menu_name;
}
if (isset($parent)) {
$new_definition['parent'] = $parent;
}
return $new_definition;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$new_definition = $this->extractFormValues($form, $form_state);
return $this->menuLinkManager->updateDefinition($this->menuLink->getPluginId(), $new_definition);
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\Form\MenuLinkFormInterface.
*/
namespace Drupal\Core\Menu\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Defines an interface for edit forms of menu links.
*
* All menu link plugins use the same interface for their configuration or
* editing form, but the implementations may differ.
*
* @see \Drupal\Core\Menu\MenuLinkInterface::getFormClass()
*/
interface MenuLinkFormInterface extends PluginFormInterface {
/**
* Injects the menu link plugin instance.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $menu_link
* A menu link plugin instance.
*/
public function setMenuLinkInstance(MenuLinkInterface $menu_link);
/**
* Extracts a plugin definition from form values.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The new plugin definition values taken from the form values.
*/
public function extractFormValues(array &$form, FormStateInterface $form_state);
}

View file

@ -0,0 +1,86 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\InaccessibleMenuLink.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\SafeMarkup;
/**
* A menu link plugin for wrapping another menu link, in sensitive situations.
*
* @see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess()
*/
class InaccessibleMenuLink extends MenuLinkBase {
/**
* The wrapped menu link.
*
* @var \Drupal\Core\Menu\MenuLinkInterface
*/
protected $wrappedLink;
/**
* Constructs a new InaccessibleMenuLink.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $wrapped_link
* The menu link to wrap.
*/
public function __construct(MenuLinkInterface $wrapped_link) {
$this->wrappedLink = $wrapped_link;
$plugin_definition = [
'route_name' => '<front>',
'route_parameters' => [],
'url' => NULL,
] + $this->wrappedLink->getPluginDefinition();
parent::__construct([], $this->wrappedLink->getPluginId(), $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
return $this->t('Inaccessible');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->wrappedLink->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->wrappedLink->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->wrappedLink->getCacheMaxAge();
}
/**
* {@inheritdoc}
*/
public function updateLink(array $new_definition_values, $persist) {
throw new PluginException(SafeMarkup::format('Inaccessible menu link plugins do not support updating'));
}
}

View file

@ -0,0 +1,129 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalActionDefault.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a default implementation for local action plugins.
*/
class LocalActionDefault extends PluginBase implements LocalActionInterface, ContainerFactoryPluginInterface {
/**
* The route provider to load routes by name.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* Constructs a LocalActionDefault object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider to load routes by name.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeProvider = $route_provider;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('router.route_provider')
);
}
/**
* {@inheritdoc}
*/
public function getRouteName() {
return $this->pluginDefinition['route_name'];
}
/**
* {@inheritdoc}
*/
public function getTitle(Request $request = NULL) {
// 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 getWeight() {
return $this->pluginDefinition['weight'];
}
/**
* {@inheritdoc}
*/
public function getRouteParameters(RouteMatchInterface $route_match) {
$parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : array();
$route = $this->routeProvider->getRouteByName($this->getRouteName());
$variables = $route->compile()->getVariables();
// Normally the \Drupal\Core\ParamConverter\ParamConverterManager has
// processed the Request attributes, and in that case the _raw_variables
// attribute holds the original path strings keyed to the corresponding
// slugs in the path patterns. For example, if the route's path pattern is
// /filter/tips/{filter_format} and the path is /filter/tips/plain_text then
// $raw_variables->get('filter_format') == 'plain_text'.
$raw_variables = $route_match->getRawParameters();
foreach ($variables as $name) {
if (isset($parameters[$name])) {
continue;
}
if ($raw_variables && $raw_variables->has($name)) {
$parameters[$name] = $raw_variables->get($name);
}
elseif ($value = $route_match->getRawParameter($name)) {
$parameters[$name] = $value;
}
}
// The UrlGenerator will throw an exception if expected parameters are
// missing. This method should be overridden if that is possible.
return $parameters;
}
/**
* {@inheritdoc}
*/
public function getOptions(RouteMatchInterface $route_match) {
return (array) $this->pluginDefinition['options'];
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalActionInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Defines an interface for menu local actions.
*/
interface LocalActionInterface {
/**
* Get the route name from the settings.
*
* @return string
* The name of the route this action links to.
*/
public function getRouteName();
/**
* Returns the route parameters needed to render a link for the local action.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return array
* An array of parameter names and values.
*/
public function getRouteParameters(RouteMatchInterface $route_match);
/**
* Returns the weight for the local action.
*
* @return int
*/
public function getWeight();
/**
* Returns options for rendering a link for the local action.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return array
* An associative array of options.
*/
public function getOptions(RouteMatchInterface $route_match);
/**
* Returns the localized title to be shown for this action.
*
* Subclasses may add optional arguments like NodeInterface $node = NULL that
* will be supplied by the ControllerResolver.
*
* @return string
* The title to be shown for this action.
*
* @see \Drupal\Core\Menu\LocalActionManager::getTitle()
*/
public function getTitle();
}

View file

@ -0,0 +1,200 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalActionManager.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides the default local action manager using YML as primary definition.
*/
class LocalActionManager extends DefaultPluginManager implements LocalActionManagerInterface {
/**
* Provides some default values for all local action plugins.
*
* @var array
*/
protected $defaults = array(
// The plugin id. Set by the plugin system based on the top-level YAML key.
'id' => NULL,
// The static title for the local action.
'title' => '',
// The weight of the local action.
'weight' => NULL,
// (Required) the route name used to generate a link.
'route_name' => NULL,
// Default route parameters for generating links.
'route_parameters' => array(),
// Associative array of link options.
'options' => array(),
// The route names where this local action appears.
'appears_on' => array(),
// Default class for local action implementations.
'class' => 'Drupal\Core\Menu\LocalActionDefault',
);
/**
* A controller resolver object.
*
* @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The route provider to load routes by name.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The plugin instances.
*
* @var \Drupal\Core\Menu\LocalActionInterface[]
*/
protected $instances = array();
/**
* Constructs a LocalActionManager object.
*
* @param \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface $controller_resolver
* An object to use in introspecting route methods.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(ControllerResolverInterface $controller_resolver, RequestStack $request_stack, RouteMatchInterface $route_match, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager, AccessManagerInterface $access_manager, AccountInterface $account) {
// Skip calling the parent constructor, since that assumes annotation-based
// discovery.
$this->factory = new ContainerFactory($this, 'Drupal\Core\Menu\LocalActionInterface');
$this->controllerResolver = $controller_resolver;
$this->requestStack = $request_stack;
$this->routeMatch = $route_match;
$this->routeProvider = $route_provider;
$this->accessManager = $access_manager;
$this->moduleHandler = $module_handler;
$this->account = $account;
$this->alterInfo('menu_local_actions');
$this->setCacheBackend($cache_backend, 'local_action_plugins:' . $language_manager->getCurrentLanguage()->getId(), array('local_action'));
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new YamlDiscovery('links.action', $this->moduleHandler->getModuleDirectories());
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
public function getTitle(LocalActionInterface $local_action) {
$controller = array($local_action, 'getTitle');
$arguments = $this->controllerResolver->getArguments($this->requestStack->getCurrentRequest(), $controller);
return call_user_func_array($controller, $arguments);
}
/**
* {@inheritdoc}
*/
public function getActionsForRoute($route_appears) {
if (!isset($this->instances[$route_appears])) {
$route_names = array();
$this->instances[$route_appears] = array();
// @todo - optimize this lookup by compiling or caching.
foreach ($this->getDefinitions() as $plugin_id => $action_info) {
if (in_array($route_appears, $action_info['appears_on'])) {
$plugin = $this->createInstance($plugin_id);
$route_names[] = $plugin->getRouteName();
$this->instances[$route_appears][$plugin_id] = $plugin;
}
}
// Pre-fetch all the action route objects. This reduces the number of SQL
// queries that would otherwise be triggered by the access manager.
if (!empty($route_names)) {
$this->routeProvider->getRoutesByNames($route_names);
}
}
$links = array();
/** @var $plugin \Drupal\Core\Menu\LocalActionInterface */
foreach ($this->instances[$route_appears] as $plugin_id => $plugin) {
$route_name = $plugin->getRouteName();
$route_parameters = $plugin->getRouteParameters($this->routeMatch);
$links[$plugin_id] = array(
'#theme' => 'menu_local_action',
'#link' => array(
'title' => $this->getTitle($plugin),
'url' => Url::fromRoute($route_name, $route_parameters),
'localized_options' => $plugin->getOptions($this->routeMatch),
),
'#access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account),
'#weight' => $plugin->getWeight(),
);
}
return $links;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalActionManagerInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Manages discovery and instantiation of menu local action plugins.
*
* Menu local actions are links that lead to actions like "add new". The plugin
* format allows them (if needed) to dynamically generate a title or the path
* they link to. The annotation on the plugin provides the default title,
* and the list of routes where the action should be rendered.
*/
interface LocalActionManagerInterface extends PluginManagerInterface {
/**
* Gets the title for a local action.
*
* @param \Drupal\Core\Menu\LocalActionInterface $local_action
* An object to get the title from.
*
* @return string
* The title (already localized).
*
* @throws \BadMethodCallException
* If the plugin does not implement the getTitle() method.
*/
public function getTitle(LocalActionInterface $local_action);
/**
* Finds all local actions that appear on a named route.
*
* @param string $route_appears
* The route name for which to find local actions.
*
* @return array
* An array of link render arrays.
*/
public function getActionsForRoute($route_appears);
}

View file

@ -0,0 +1,151 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalTaskDefault.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Default object used for LocalTaskPlugins.
*/
class LocalTaskDefault extends PluginBase implements LocalTaskInterface {
/**
* The route provider to load routes by name.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* TRUE if this plugin is forced active for options attributes.
*
* @var bool
*/
protected $active = FALSE;
/**
* {@inheritdoc}
*/
public function getRouteName() {
return $this->pluginDefinition['route_name'];
}
/**
* {@inheritdoc}
*/
public function getRouteParameters(RouteMatchInterface $route_match) {
$parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : array();
$route = $this->routeProvider()->getRouteByName($this->getRouteName());
$variables = $route->compile()->getVariables();
// Normally the \Drupal\Core\ParamConverter\ParamConverterManager has
// processed the Request attributes, and in that case the _raw_variables
// attribute holds the original path strings keyed to the corresponding
// slugs in the path patterns. For example, if the route's path pattern is
// /filter/tips/{filter_format} and the path is /filter/tips/plain_text then
// $raw_variables->get('filter_format') == 'plain_text'.
$raw_variables = $route_match->getRawParameters();
foreach ($variables as $name) {
if (isset($parameters[$name])) {
continue;
}
if ($raw_variables && $raw_variables->has($name)) {
$parameters[$name] = $raw_variables->get($name);
}
elseif ($value = $route_match->getRawParameter($name)) {
$parameters[$name] = $value;
}
}
// The UrlGenerator will throw an exception if expected parameters are
// missing. This method should be overridden if that is possible.
return $parameters;
}
/**
* {@inheritdoc}
*/
public function getTitle(Request $request = NULL) {
// 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);
}
/**
* Returns the weight of the local task.
*
* @return int
* The weight of the task. If not defined in the annotation returns 0 by
* default or -10 for the root tab.
*/
public function getWeight() {
// By default the weight is 0, or -10 for the root tab.
if (!isset($this->pluginDefinition['weight'])) {
if ($this->pluginDefinition['base_route'] == $this->pluginDefinition['route_name']) {
$this->pluginDefinition['weight'] = -10;
}
else {
$this->pluginDefinition['weight'] = 0;
}
}
return (int) $this->pluginDefinition['weight'];
}
/**
* {@inheritdoc}
*/
public function getOptions(RouteMatchInterface $route_match) {
$options = $this->pluginDefinition['options'];
if ($this->active) {
if (empty($options['attributes']['class']) || !in_array('is-active', $options['attributes']['class'])) {
$options['attributes']['class'][] = 'is-active';
}
}
return (array) $options;
}
/**
* {@inheritdoc}
*/
public function setActive($active = TRUE) {
$this->active = $active;
return $this;
}
/**
* {@inheritdoc}
*/
public function getActive() {
return $this->active;
}
/**
* Returns the route provider.
*
* @return \Drupal\Core\Routing\RouteProviderInterface
* The route provider.
*/
protected function routeProvider() {
if (!$this->routeProvider) {
$this->routeProvider = \Drupal::service('router.route_provider');
}
return $this->routeProvider;
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalTaskInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Defines an interface for menu local tasks.
*/
interface LocalTaskInterface {
/**
* Get the route name from the settings.
*
* @return string
* The name of the route this local task links to.
*/
public function getRouteName();
/**
* Returns the localized title to be shown for this tab.
*
* Subclasses may add optional arguments like NodeInterface $node = NULL that
* will be supplied by the ControllerResolver.
*
* @return string
* The title of the local task.
*/
public function getTitle();
/**
* Returns the route parameters needed to render a link for the local task.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return array
* An array of parameter names and values.
*/
public function getRouteParameters(RouteMatchInterface $route_match);
/**
* Returns the weight of the local task.
*
* @return int|null
* The weight of the task or NULL.
*/
public function getWeight();
/**
* Returns options for rendering a link to the local task.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return array
* An associative array of options.
*/
public function getOptions(RouteMatchInterface $route_match);
/**
* Sets the active status.
*
* @param bool $active
* Sets whether this tab is active (e.g. a parent of the current tab).
*
* @return \Drupal\Core\Menu\LocalTaskInterface
* The called object for chaining.
*/
public function setActive($active = TRUE);
/**
* Gets the active status.
*
* @return bool
* TRUE if the local task is active, FALSE otherwise.
*
* @see \Drupal\system\Plugin\MenuLocalTaskInterface::setActive()
*/
public function getActive();
}

View file

@ -0,0 +1,361 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalTaskManager.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides the default local task manager using YML as primary definition.
*/
class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerInterface {
/**
* {@inheritdoc}
*/
protected $defaults = array(
// (required) The name of the route this task links to.
'route_name' => '',
// Parameters for route variables when generating a link.
'route_parameters' => array(),
// The static title for the local task.
'title' => '',
// The route name where the root tab appears.
'base_route' => '',
// The plugin ID of the parent tab (or NULL for the top-level tab).
'parent_id' => NULL,
// The weight of the tab.
'weight' => NULL,
// The default link options.
'options' => array(),
// Default class for local task implementations.
'class' => 'Drupal\Core\Menu\LocalTaskDefault',
// The plugin id. Set by the plugin system based on the top-level YAML key.
'id' => '',
);
/**
* A controller resolver object.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The plugin instances.
*
* @var array
*/
protected $instances = array();
/**
* The route provider to load routes by name.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a \Drupal\Core\Menu\LocalTaskManager object.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* An object to use in introspecting route methods.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request object to use for building titles and paths for plugin instances.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider to load routes by name.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(ControllerResolverInterface $controller_resolver, RequestStack $request_stack, RouteMatchInterface $route_match, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, AccessManagerInterface $access_manager, AccountInterface $account) {
$this->factory = new ContainerFactory($this, '\Drupal\Core\Menu\LocalTaskInterface');
$this->controllerResolver = $controller_resolver;
$this->requestStack = $request_stack;
$this->routeMatch = $route_match;
$this->routeProvider = $route_provider;
$this->accessManager = $access_manager;
$this->account = $account;
$this->moduleHandler = $module_handler;
$this->alterInfo('local_tasks');
$this->setCacheBackend($cache, 'local_task_plugins:' . $language_manager->getCurrentLanguage()->getId(), array('local_task'));
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories());
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
// If there is no route name, this is a broken definition.
if (empty($definition['route_name'])) {
throw new PluginException(sprintf('Plugin (%s) definition must include "route_name"', $plugin_id));
}
}
/**
* {@inheritdoc}
*/
public function getTitle(LocalTaskInterface $local_task) {
$controller = array($local_task, 'getTitle');
$request = $this->requestStack->getCurrentRequest();
$arguments = $this->controllerResolver->getArguments($request, $controller);
return call_user_func_array($controller, $arguments);
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$definitions = parent::getDefinitions();
$count = 0;
foreach ($definitions as &$definition) {
if (isset($definition['weight'])) {
// Add some micro weight.
$definition['weight'] += ($count++) * 1e-6;
}
}
return $definitions;
}
/**
* {@inheritdoc}
*/
public function getLocalTasksForRoute($route_name) {
if (!isset($this->instances[$route_name])) {
$this->instances[$route_name] = array();
if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) {
$base_routes = $cache->data['base_routes'];
$parents = $cache->data['parents'];
$children = $cache->data['children'];
}
else {
$definitions = $this->getDefinitions();
// We build the hierarchy by finding all tabs that should
// appear on the current route.
$base_routes = array();
$parents = array();
$children = array();
foreach ($definitions as $plugin_id => $task_info) {
// Fill in the base_route from the parent to insure consistency.
if (!empty($task_info['parent_id']) && !empty($definitions[$task_info['parent_id']])) {
$task_info['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
// Populate the definitions we use in the next loop. Using a
// reference like &$task_info causes bugs.
$definitions[$plugin_id]['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
}
if ($route_name == $task_info['route_name']) {
if(!empty($task_info['base_route'])) {
$base_routes[$task_info['base_route']] = $task_info['base_route'];
}
// Tabs that link to the current route are viable parents
// and their parent and children should be visible also.
// @todo - this only works for 2 levels of tabs.
// instead need to iterate up.
$parents[$plugin_id] = TRUE;
if (!empty($task_info['parent_id'])) {
$parents[$task_info['parent_id']] = TRUE;
}
}
}
if ($base_routes) {
// Find all the plugins with the same root and that are at the top
// level or that have a visible parent.
foreach ($definitions as $plugin_id => $task_info) {
if (!empty($base_routes[$task_info['base_route']]) && (empty($task_info['parent_id']) || !empty($parents[$task_info['parent_id']]))) {
// Concat '> ' with root ID for the parent of top-level tabs.
$parent = empty($task_info['parent_id']) ? '> ' . $task_info['base_route'] : $task_info['parent_id'];
$children[$parent][$plugin_id] = $task_info;
}
}
}
$data = array(
'base_routes' => $base_routes,
'parents' => $parents,
'children' => $children,
);
$this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, Cache::PERMANENT, $this->cacheTags);
}
// Create a plugin instance for each element of the hierarchy.
foreach ($base_routes as $base_route) {
// Convert the tree keyed by plugin IDs into a simple one with
// integer depth. Create instances for each plugin along the way.
$level = 0;
// We used this above as the top-level parent array key.
$next_parent = '> ' . $base_route;
do {
$parent = $next_parent;
$next_parent = FALSE;
foreach ($children[$parent] as $plugin_id => $task_info) {
$plugin = $this->createInstance($plugin_id);
$this->instances[$route_name][$level][$plugin_id] = $plugin;
// Normally, _l() compares the href of every link with the current
// path and sets the active class accordingly. But the parents of
// the current local task may be on a different route in which
// case we have to set the class manually by flagging it active.
if (!empty($parents[$plugin_id]) && $route_name != $task_info['route_name']) {
$plugin->setActive();
}
if (isset($children[$plugin_id])) {
// This tab has visible children
$next_parent = $plugin_id;
}
}
$level++;
} while ($next_parent);
}
}
return $this->instances[$route_name];
}
/**
* {@inheritdoc}
*/
public function getTasksBuild($current_route_name) {
$tree = $this->getLocalTasksForRoute($current_route_name);
$build = array();
// Collect all route names.
$route_names = array();
foreach ($tree as $instances) {
foreach ($instances as $child) {
$route_names[] = $child->getRouteName();
}
}
// Pre-fetch all routes involved in the tree. This reduces the number
// of SQL queries that would otherwise be triggered by the access manager.
$routes = $route_names ? $this->routeProvider->getRoutesByNames($route_names) : array();
foreach ($tree as $level => $instances) {
/** @var $instances \Drupal\Core\Menu\LocalTaskInterface[] */
foreach ($instances as $plugin_id => $child) {
$route_name = $child->getRouteName();
$route_parameters = $child->getRouteParameters($this->routeMatch);
// Find out whether the user has access to the task.
$access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account);
if ($access) {
$active = $this->isRouteActive($current_route_name, $route_name, $route_parameters);
// The plugin may have been set active in getLocalTasksForRoute() if
// one of its child tabs is the active tab.
$active = $active || $child->getActive();
// @todo It might make sense to use link render elements instead.
$link = array(
'title' => $this->getTitle($child),
'url' => Url::fromRoute($route_name, $route_parameters),
'localized_options' => $child->getOptions($this->routeMatch),
);
$build[$level][$plugin_id] = array(
'#theme' => 'menu_local_task',
'#link' => $link,
'#active' => $active,
'#weight' => $child->getWeight(),
'#access' => $access,
);
}
}
}
return $build;
}
/**
* Determines whether the route of a certain local task is currently active.
*
* @param string $current_route_name
* The route name of the current main request.
* @param string $route_name
* The route name of the local task to determine the active status.
* @param array $route_parameters
*
* @return bool
* Returns TRUE if the passed route_name and route_parameters is considered
* as the same as the one from the request, otherwise FALSE.
*/
protected function isRouteActive($current_route_name, $route_name, $route_parameters) {
// Flag the list element as active if this tab's route and parameters match
// the current request's route and route variables.
$active = $current_route_name == $route_name;
if ($active) {
// The request is injected, so we need to verify that we have the expected
// _raw_variables attribute.
$raw_variables_bag = $this->routeMatch->getRawParameters();
// If we don't have _raw_variables, we assume the attributes are still the
// original values.
$raw_variables = $raw_variables_bag ? $raw_variables_bag->all() : $this->routeMatch->getParameters()->all();
$active = array_intersect_assoc($route_parameters, $raw_variables) == $route_parameters;
}
return $active;
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\LocalTaskManagerInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Manages discovery and instantiation of menu local task plugins.
*
* This manager finds plugins that are rendered as local tasks (usually tabs).
* Derivatives are supported for modules that wish to generate multiple tabs on
* behalf of something else.
*/
interface LocalTaskManagerInterface extends PluginManagerInterface {
/**
* Gets the title for a local task.
*
* @param \Drupal\Core\Menu\LocalTaskInterface $local_task
* A local task plugin instance to get the title for.
*
* @return string
* The localized title.
*/
public function getTitle(LocalTaskInterface $local_task);
/**
* Find all local tasks that appear on a named route.
*
* @param string $route_name
* The route for which to find local tasks.
*
* @return array
* Returns an array of task levels. Each task level contains instances
* of local tasks (LocalTaskInterface) which appear on the tab route.
* The array keys are the depths and the values are arrays of plugin
* instances.
*/
public function getLocalTasksForRoute($route_name);
/**
* Gets the render array for all local tasks.
*
* @param string $current_route_name
* The route for which to make renderable local tasks.
*
* @return array
* A render array as expected by menu-local-tasks.html.twig.
*/
public function getTasksBuild($current_route_name);
}

View file

@ -0,0 +1,148 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuActiveTrail.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Lock\LockBackendInterface;
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 extends CacheCollector 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.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
*/
public function __construct(MenuLinkManagerInterface $menu_link_manager, RouteMatchInterface $route_match, CacheBackendInterface $cache, LockBackendInterface $lock) {
parent::__construct(NULL, $cache, $lock);
$this->menuLinkManager = $menu_link_manager;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*
* @see ::getActiveTrailIds()
*/
protected function getCid() {
if (!isset($this->cid)) {
$route_parameters = $this->routeMatch->getRawParameters()->all();
ksort($route_parameters);
return 'active-trail:route:' . $this->routeMatch->getRouteName() . ':route_parameters:' . serialize($route_parameters);
}
return $this->cid;
}
/**
* {@inheritdoc}
*
* @see ::getActiveTrailIds()
*/
protected function resolveCacheMiss($menu_name) {
$this->storage[$menu_name] = $this->doGetActiveTrailIds($menu_name);
$this->tags[] = 'config:system.menu.' . $menu_name;
$this->persist($menu_name);
return $this->storage[$menu_name];
}
/**
* {@inheritdoc}
*
* This implementation caches all active trail IDs per route match for *all*
* menus whose active trails are calculated on that page. This ensures 1 cache
* get for all active trails per page load, rather than N.
*
* It uses the cache collector pattern to do this.
*
* @see ::get()
* @see \Drupal\Core\Cache\CacheCollectorInterface
* @see \Drupal\Core\Cache\CacheCollector
*/
public function getActiveTrailIds($menu_name) {
return $this->get($menu_name);
}
/**
* Helper method for ::getActiveTrailIds().
*/
protected function doGetActiveTrailIds($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 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;
}
}

View file

@ -0,0 +1,44 @@
<?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|NULL $menu_name
* (optional) The menu name of the requested tree. If omitted, all menu
* trees will be searched.
*
* @return array
* An array containing the active trail: a list of plugin IDs.
*/
public function getActiveTrailIds($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);
}

View file

@ -0,0 +1,197 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkBase.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Url;
/**
* Defines a base menu link class.
*/
abstract class MenuLinkBase extends PluginBase implements MenuLinkInterface {
/**
* The list of definition values where an override is allowed.
*
* The keys are definition names. The values are ignored.
*
* @var array
*/
protected $overrideAllowed = array();
/**
* {@inheritdoc}
*/
public function getWeight() {
// By default the weight is 0.
if (!isset($this->pluginDefinition['weight'])) {
$this->pluginDefinition['weight'] = 0;
}
return $this->pluginDefinition['weight'];
}
/**
* {@inheritdoc}
*/
public function getMenuName() {
return $this->pluginDefinition['menu_name'];
}
/**
* {@inheritdoc}
*/
public function getProvider() {
return $this->pluginDefinition['provider'];
}
/**
* {@inheritdoc}
*/
public function getParent() {
return $this->pluginDefinition['parent'];
}
/**
* {@inheritdoc}
*/
public function isEnabled() {
return (bool) $this->pluginDefinition['enabled'];
}
/**
* {@inheritdoc}
*/
public function isExpanded() {
return (bool) $this->pluginDefinition['expanded'];
}
/**
* {@inheritdoc}
*/
public function isResettable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
return (bool) $this->getTranslateRoute();
}
/**
* {@inheritdoc}
*/
public function isDeletable() {
return (bool) $this->getDeleteRoute();
}
/**
* {@inheritdoc}
*/
public function getOptions() {
return $this->pluginDefinition['options'] ?: array();
}
/**
* {@inheritdoc}
*/
public function getMetaData() {
return $this->pluginDefinition['metadata'] ?: array();
}
/**
* {@inheritdoc}
*/
public function getRouteName() {
return isset($this->pluginDefinition['route_name']) ? $this->pluginDefinition['route_name'] : '';
}
/**
* {@inheritdoc}
*/
public function getRouteParameters() {
return isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : array();
}
/**
* {@inheritdoc}
*/
public function getUrlObject($title_attribute = TRUE) {
$options = $this->getOptions();
if ($title_attribute && $description = $this->getDescription()) {
$options['attributes']['title'] = $description;
}
if (empty($this->pluginDefinition['url'])) {
return new Url($this->pluginDefinition['route_name'], $this->pluginDefinition['route_parameters'], $options);
}
else {
return Url::fromUri($this->pluginDefinition['url'], $options);
}
}
/**
* {@inheritdoc}
*/
public function getFormClass() {
return $this->pluginDefinition['form_class'];
}
/**
* {@inheritdoc}
*/
public function getDeleteRoute() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getEditRoute() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getTranslateRoute() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function deleteLink() {
throw new PluginException(SafeMarkup::format('Menu link plugin with ID @id does not support deletion', array('@id' => $this->getPluginId())));
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkDefault.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a default implementation for menu link plugins.
*/
class MenuLinkDefault extends MenuLinkBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
protected $overrideAllowed = array(
'menu_name' => 1,
'parent' => 1,
'weight' => 1,
'expanded' => 1,
'enabled' => 1,
);
/**
* The static menu link service used to store updates to weight/parent etc.
*
* @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
*/
protected $staticOverride;
/**
* Constructs a new MenuLinkDefault.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
* The static override storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->staticOverride = $static_override;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('menu_link.static.overrides')
);
}
/**
* {@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}
*/
public function isResettable() {
// The link can be reset if it has an override.
return (bool) $this->staticOverride->loadOverride($this->getPluginId());
}
/**
* {@inheritdoc}
*/
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);
}
return $this->pluginDefinition;
}
}

View file

@ -0,0 +1,236 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
/**
* Defines an interface for classes providing a type of menu link.
*/
interface MenuLinkInterface extends PluginInspectionInterface, DerivativeInspectionInterface, CacheableDependencyInterface {
/**
* Returns the weight of the menu link.
*
* @return int
* The weight of the menu link, 0 by default.
*/
public function getWeight();
/**
* Returns the localized title to be shown for this link.
*
* @return string
* The title of the menu link.
*/
public function getTitle();
/**
* Returns the description of the menu link.
*
* @return string
* The description of the menu link.
*/
public function getDescription();
/**
* Returns the menu name of the menu link.
*
* @return string
* The menu name of the menu link.
*/
public function getMenuName();
/**
* Returns the provider (module name) of the menu link.
*
* @return string
* The provider of the menu link.
*/
public function getProvider();
/**
* Returns the plugin ID of the menu link's parent, or an empty string.
*
* @return string
* The parent plugin ID.
*/
public function getParent();
/**
* Returns whether the menu link is enabled (not hidden).
*
* @return bool
* TRUE for enabled, FALSE otherwise.
*/
public function isEnabled();
/**
* Returns whether the child menu links should always been shown.
*
* @return bool
* TRUE for expanded, FALSE otherwise.
*/
public function isExpanded();
/**
* Returns whether this link can be reset.
*
* In general, only links that store overrides using the
* menu_link.static.overrides service should return TRUE for this method.
*
* @return bool
* TRUE if it can be reset, FALSE otherwise.
*/
public function isResettable();
/**
* Returns whether this link can be translated.
*
* @return bool
* TRUE if the link can be translated, FALSE otherwise.
*/
public function isTranslatable();
/**
* Returns whether this link can be deleted.
*
* @return bool
* TRUE if the link can be deleted, FALSE otherwise.
*/
public function isDeletable();
/**
* Returns the route name, if available.
*
* @return string
* The name of the route this menu link links to.
*/
public function getRouteName();
/**
* Returns the route parameters, if available.
*
* @return array
* An array of parameter names and values.
*/
public function getRouteParameters();
/**
* Returns a URL object containing either the external path or route.
*
* @param bool $title_attribute
* (optional) If TRUE, add the link description as the title attribute if
* the description is not empty.
*
* @return \Drupal\Core\Url
* A a URL object containing either the external path or route.
*/
public function getUrlObject($title_attribute = TRUE);
/**
* Returns the options for this link.
*
* @return array
* An associative array of options.
*/
public function getOptions();
/**
* Returns any metadata for this link.
*
* @return array
* The metadata for the menu link.
*/
public function getMetaData();
/**
* Updates the definition values for a menu link.
*
* Depending on the implementation details of the class, not all definition
* values may be changed. For example, changes to the title of a static link
* will be discarded.
*
* In general, this method should not be called directly, but will be called
* automatically from MenuLinkManagerInterface::updateDefinition().
*
* @param array $new_definition_values
* The new values for the link definition. This will usually be just a
* subset of the plugin definition.
* @param bool $persist
* TRUE to have the link persist the changed values to any additional
* storage.
*
* @return array
* The plugin definition incorporating any allowed changes.
*/
public function updateLink(array $new_definition_values, $persist);
/**
* Deletes a menu link.
*
* In general, this method should not be called directly, but will be called
* automatically from MenuLinkManagerInterface::removeDefinition().
*
* This method will only delete the link from any additional storage, but not
* from the plugin.manager.menu.link service.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the link is not deletable.
*/
public function deleteLink();
/**
* Returns the name of a class that can build an editing form for this link.
*
* To instantiate the form class, use an instance of the
* \Drupal\Core\DependencyInjection\ClassResolverInterface, such as from the
* class_resolver service. Then call the setMenuLinkInstance() method on the
* form instance with the menu link plugin instance.
*
* @todo Add a code example. https://www.drupal.org/node/2302849
*
* @return string
* A class that implements \Drupal\Core\Menu\Form\MenuLinkFormInterface.
*/
public function getFormClass();
/**
* Returns route information for a route to delete the menu link.
*
* @return \Drupal\Core\Url|null
* A Url object, or NULL if there is no route (e.g. when the link is not
* deletable).
*/
public function getDeleteRoute();
/**
* Returns route information for a custom edit form for the menu link.
*
* Plugins should return a value here if they have a special edit form, or if
* they need to define additional local tasks, local actions, etc. that are
* visible from the edit form.
*
* @return \Drupal\Core\Url|null
* A Url object, or NULL if there is no route because there is no custom
* edit route for this instance.
*/
public function getEditRoute();
/**
* Returns route information for a route to translate the menu link.
*
* @return \Drupal\Core\Url|null
* A Url object, or NULL if there is no route (e.g. when the link is not
* translatable).
*/
public function getTranslateRoute();
}

View file

@ -0,0 +1,422 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkManager.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
/**
* Manages discovery, instantiation, and tree building of menu link plugins.
*
* This manager finds plugins that are rendered as menu links.
*/
class MenuLinkManager implements MenuLinkManagerInterface {
/**
* Provides some default values for the definition of all menu link plugins.
*
* @todo Decide how to keep these field definitions in sync.
* https://www.drupal.org/node/2302085
*
* @var array
*/
protected $defaults = array(
// (required) The name of the menu for this link.
'menu_name' => 'tools',
// (required) The name of the route this links to, unless it's external.
'route_name' => '',
// Parameters for route variables when generating a link.
'route_parameters' => array(),
// The external URL if this link has one (required if route_name is empty).
'url' => '',
// The static title for the menu link. You can specify placeholders like on
// any translatable string and the values in title_arguments.
'title' => '',
// The values for the menu link placeholders.
'title_arguments' => array(),
// A context for the title string.
// @see \Drupal\Core\StringTranslation\TranslationInterface::translate()
'title_context' => '',
// The description.
'description' => '',
// The plugin ID of the parent link (or NULL for a top-level link).
'parent' => '',
// The weight of the link.
'weight' => 0,
// The default link options.
'options' => array(),
'expanded' => 0,
'enabled' => 1,
// The name of the module providing this link.
'provider' => '',
'metadata' => array(),
// Default class for local task implementations.
'class' => 'Drupal\Core\Menu\MenuLinkDefault',
'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm',
// The plugin ID. Set by the plugin system based on the top-level YAML key.
'id' => '',
);
/**
* The object that discovers plugins managed by this manager.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $discovery;
/**
* The object that instantiates plugins managed by this manager.
*
* @var \Drupal\Component\Plugin\Factory\FactoryInterface
*/
protected $factory;
/**
* The menu link tree storage.
*
* @var \Drupal\Core\Menu\MenuTreeStorageInterface
*/
protected $treeStorage;
/**
* Service providing overrides for static links.
*
* @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
*/
protected $overrides;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a \Drupal\Core\Menu\MenuLinkManager object.
*
* @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
* The menu link tree storage.
* @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides
* The service providing overrides for static links.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, ModuleHandlerInterface $module_handler) {
$this->treeStorage = $tree_storage;
$this->overrides = $overrides;
$this->moduleHandler = $module_handler;
}
/**
* Performs extra processing on plugin definitions.
*
* By default we add defaults for the type to the definition. If a type has
* additional processing logic, the logic can be added by replacing or
* extending this method.
*
* @param array $definition
* The definition to be processed and modified by reference.
* @param $plugin_id
* The ID of the plugin this definition is being used for.
*/
protected function processDefinition(array &$definition, $plugin_id) {
$definition = NestedArray::mergeDeep($this->defaults, $definition);
// Typecast so NULL, no parent, will be an empty string since the parent ID
// should be a string.
$definition['parent'] = (string) $definition['parent'];
$definition['id'] = $plugin_id;
}
/**
* Gets the plugin discovery.
*
* @return \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new YamlDiscovery('links.menu', $this->moduleHandler->getModuleDirectories());
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
}
return $this->discovery;
}
/**
* Gets the plugin factory.
*
* @return \Drupal\Component\Plugin\Factory\FactoryInterface
*/
protected function getFactory() {
if (!isset($this->factory)) {
$this->factory = new ContainerFactory($this);
}
return $this->factory;
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
// Since this function is called rarely, instantiate the discovery here.
$definitions = $this->getDiscovery()->getDefinitions();
$this->moduleHandler->alter('menu_links_discovered', $definitions);
foreach ($definitions as $plugin_id => &$definition) {
$definition['id'] = $plugin_id;
$this->processDefinition($definition, $plugin_id);
}
// If this plugin was provided by a module that does not exist, remove the
// plugin definition.
// @todo Address what to do with an invalid plugin.
// https://www.drupal.org/node/2302623
foreach ($definitions as $plugin_id => $plugin_definition) {
if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) {
unset($definitions[$plugin_id]);
}
}
return $definitions;
}
/**
* {@inheritdoc}
*/
public function rebuild() {
$definitions = $this->getDefinitions();
// Apply overrides from config.
$overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions));
foreach ($overrides as $id => $changes) {
if (!empty($definitions[$id])) {
$definitions[$id] = $changes + $definitions[$id];
}
}
$this->treeStorage->rebuild($definitions);
}
/**
* {@inheritdoc}
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
$definition = $this->treeStorage->load($plugin_id);
if (empty($definition) && $exception_on_invalid) {
throw new PluginNotFoundException($plugin_id);
}
return $definition;
}
/**
* {@inheritdoc}
*/
public function hasDefinition($plugin_id) {
return (bool) $this->getDefinition($plugin_id, FALSE);
}
/**
* Returns a pre-configured menu link plugin instance.
*
* @param string $plugin_id
* The ID of the plugin being instantiated.
* @param array $configuration
* An array of configuration relevant to the plugin instance.
*
* @return \Drupal\Core\Menu\MenuLinkInterface
* A menu link instance.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the instance cannot be created, such as if the ID is invalid.
*/
public function createInstance($plugin_id, array $configuration = array()) {
return $this->getFactory()->createInstance($plugin_id, $configuration);
}
/**
* {@inheritdoc}
*/
public function getInstance(array $options) {
if (isset($options['id'])) {
return $this->createInstance($options['id']);
}
}
/**
* {@inheritdoc}
*/
public function deleteLinksInMenu($menu_name) {
foreach ($this->treeStorage->loadByProperties(array('menu_name' => $menu_name)) as $plugin_id => $definition) {
$instance = $this->createInstance($plugin_id);
if ($instance->isDeletable()) {
$this->deleteInstance($instance, TRUE);
}
elseif ($instance->isResettable()) {
$new_instance = $this->resetInstance($instance);
$affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
}
}
}
/**
* Deletes a specific instance.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $instance
* The plugin instance to be deleted.
* @param bool $persist
* If TRUE, calls MenuLinkInterface::deleteLink() on the instance.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the plugin instance does not support deletion.
*/
protected function deleteInstance(MenuLinkInterface $instance, $persist) {
$id = $instance->getPluginId();
if ($instance->isDeletable()) {
if ($persist) {
$instance->deleteLink();
}
}
else {
throw new PluginException(SafeMarkup::format('Menu link plugin with ID @id does not support deletion', array('@id' => $id)));
}
$this->treeStorage->delete($id);
}
/**
* {@inheritdoc}
*/
public function removeDefinition($id, $persist = TRUE) {
$definition = $this->treeStorage->load($id);
// It's possible the definition has already been deleted, or doesn't exist.
if ($definition) {
$instance = $this->createInstance($id);
$this->deleteInstance($instance, $persist);
}
}
/**
* {@inheritdoc}
*/
public function menuNameInUse($menu_name) {
$this->treeStorage->menuNameInUse($menu_name);
}
/**
* {@inheritdoc}
*/
public function countMenuLinks($menu_name = NULL) {
return $this->treeStorage->countMenuLinks($menu_name);
}
/**
* {@inheritdoc}
*/
public function getParentIds($id) {
if ($this->getDefinition($id, FALSE)) {
return $this->treeStorage->getRootPathIds($id);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getChildIds($id) {
if ($this->getDefinition($id, FALSE)) {
return $this->treeStorage->getAllChildIds($id);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function loadLinksByRoute($route_name, array $route_parameters = array(), $menu_name = NULL) {
$instances = array();
$loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $menu_name);
foreach ($loaded as $plugin_id => $definition) {
$instances[$plugin_id] = $this->createInstance($plugin_id);
}
return $instances;
}
/**
* {@inheritdoc}
*/
public function addDefinition($id, array $definition) {
if ($this->treeStorage->load($id) || $id === '') {
throw new PluginException(SafeMarkup::format('The ID @id already exists as a plugin definition or is not valid', array('@id' => $id)));
}
// Add defaults, so there is no requirement to specify everything.
$this->processDefinition($definition, $id);
// Store the new link in the tree.
$this->treeStorage->save($definition);
return $this->createInstance($id);
}
/**
* {@inheritdoc}
*/
public function updateDefinition($id, array $new_definition_values, $persist = TRUE) {
$instance = $this->createInstance($id);
if ($instance) {
$new_definition_values['id'] = $id;
$changed_definition = $instance->updateLink($new_definition_values, $persist);
$this->treeStorage->save($changed_definition);
}
return $instance;
}
/**
* {@inheritdoc}
*/
public function resetLink($id) {
$instance = $this->createInstance($id);
$new_instance = $this->resetInstance($instance);
return $new_instance;
}
/**
* Resets the menu link to its default settings.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $instance
* The menu link which should be reset.
*
* @return \Drupal\Core\Menu\MenuLinkInterface
* The reset menu link.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown when the menu link is not resettable.
*/
protected function resetInstance(MenuLinkInterface $instance) {
$id = $instance->getPluginId();
if (!$instance->isResettable()) {
throw new PluginException(SafeMarkup::format('Menu link %id is not resettable', array('%id' => $id)));
}
// Get the original data from disk, reset the override and re-save the menu
// tree for this link.
$definition = $this->getDefinitions()[$id];
$this->overrides->deleteOverride($id);
$this->treeStorage->save($definition);
return $this->createInstance($id);
}
/**
* {@inheritdoc}
*/
public function resetDefinitions() {
$this->treeStorage->resetDefinitions();
}
}

View file

@ -0,0 +1,200 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkManagerInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Defines an interface for managing menu links and storing their definitions.
*
* Menu link managers support both automatic plugin definition discovery and
* manually maintaining plugin definitions.
*
* MenuLinkManagerInterface::updateDefinition() can be used to update a single
* menu link's definition and pass this onto the menu storage without requiring
* a full MenuLinkManagerInterface::rebuild().
*
* Implementations that do not use automatic discovery should call
* MenuLinkManagerInterface::addDefinition() or
* MenuLinkManagerInterface::removeDefinition() when they add or remove links,
* and MenuLinkManagerInterface::updateDefinition() to update links they have
* already defined.
*/
interface MenuLinkManagerInterface extends PluginManagerInterface {
/**
* Triggers discovery, save, and cleanup of discovered links.
*/
public function rebuild();
/**
* Deletes all links having a certain menu name.
*
* If a link is not deletable but is resettable, the link will be reset to have
* its original menu name, under the assumption that the original menu is not
* the one we are deleting it from. Note that when resetting, if the original
* menu name is the same as the menu name passed to this method, the link will
* not be moved or deleted.
*
* @param string $menu_name
* The name of the menu whose links will be deleted or reset.
*/
public function deleteLinksInMenu($menu_name);
/**
* Removes a single link definition from the menu tree storage.
*
* This is used for plugins not found through discovery to remove definitions.
*
* @param string $id
* The menu link plugin ID.
* @param bool $persist
* If TRUE, this method will attempt to persist the deletion from any
* external storage by invoking MenuLinkInterface::deleteLink() on the
* plugin that is being deleted.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown if the $id is not a valid, existing, plugin ID or if the link
* cannot be deleted.
*/
public function removeDefinition($id, $persist = TRUE);
/**
* Loads multiple plugin instances based on route.
*
* @param string $route_name
* The route name.
* @param array $route_parameters
* (optional) The route parameters. Defaults to an empty array.
* @param string $menu_name
* (optional) Restricts the found links to just those in the named menu.
*
* @return \Drupal\Core\Menu\MenuLinkInterface[]
* An array of instances keyed by plugin ID.
*/
public function loadLinksByRoute($route_name, array $route_parameters = array(), $menu_name = NULL);
/**
* Adds a new menu link definition to the menu tree storage.
*
* Use this function when you know there is no entry in the tree. This is
* used for plugins not found through discovery to add new definitions.
*
* @param string $id
* The plugin ID for the new menu link definition that is being added.
* @param array $definition
* The values of the link definition.
*
* @return \Drupal\Core\Menu\MenuLinkInterface
* A plugin instance created using the newly added definition.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown when the $id is not valid or is an already existing plugin ID.
*/
public function addDefinition($id, array $definition);
/**
* Updates the values for a menu link definition in the menu tree storage.
*
* This will update the definition for a discovered menu link without the
* need for a full rebuild. It is also used for plugins not found through
* discovery to update definitions.
*
* @param string $id
* The menu link plugin ID.
* @param array $new_definition_values
* The new values for the link definition. This will usually be just a
* subset of the plugin definition.
* @param bool $persist
* TRUE to also have the link instance itself persist the changed values to
* any additional storage by invoking MenuLinkInterface::updateDefinition()
* on the plugin that is being updated.
*
* @return \Drupal\Core\Menu\MenuLinkInterface
* A plugin instance created using the updated definition.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown if the $id is not a valid, existing, plugin ID.
*/
public function updateDefinition($id, array $new_definition_values, $persist = TRUE);
/**
* Resets the values for a menu link based on the values found by discovery.
*
* @param string $id
* The menu link plugin ID.
*
* @return \Drupal\Core\Menu\MenuLinkInterface
* The menu link instance after being reset.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown if the $id is not a valid, existing, plugin ID or if the link
* cannot be reset.
*/
public function resetLink($id);
/**
* Counts the total number of menu links.
*
* @param string $menu_name
* (optional) The menu name to count by. Defaults to all menus.
*
* @return int
* The number of menu links in the named menu, or in all menus if the
* menu name is NULL.
*/
public function countMenuLinks($menu_name = NULL);
/**
* Loads all parent link IDs of a given menu link.
*
* This method is very similar to getActiveTrailIds() but allows the link to
* be specified rather than being discovered based on the menu name and
* request. This method is mostly useful for testing.
*
* @param string $id
* The menu link plugin ID.
*
* @return array
* An ordered array of IDs representing the path to the root of the tree.
* The first element of the array will be equal to $id, unless $id is not
* valid, in which case the return value will be NULL.
*/
public function getParentIds($id);
/**
* Loads all child link IDs of a given menu link, regardless of visibility.
*
* This method is mostly useful for testing.
*
* @param string $id
* The menu link plugin ID.
*
* @return array
* An unordered array of IDs representing the IDs of all children, or NULL
* if the ID is invalid.
*/
public function getChildIds($id);
/**
* Determines if any links use a given menu name.
*
* @param string $menu_name
* The menu name.
*
* @return bool
* TRUE if any links are present in the named menu, FALSE otherwise.
*/
public function menuNameInUse($menu_name);
/**
* Resets any local definition cache. Used for testing.
*/
public function resetDefinitions();
}

View file

@ -0,0 +1,302 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkTree.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Template\Attribute;
/**
* 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) {
$tree_access_cacheability = new CacheableMetadata();
$tree_link_cacheability = new CacheableMetadata();
$items = $this->buildItems($tree, $tree_access_cacheability, $tree_link_cacheability);
$build = [];
// Apply the tree-wide gathered access cacheability metadata and link
// cacheability metadata to the render array. This ensures that the
// rendered menu is varied by the cache contexts that the access results
// and (dynamic) links depended upon, and invalidated by the cache tags
// that may change the values of the access results and links.
$tree_cacheability = $tree_access_cacheability->merge($tree_link_cacheability);
$tree_cacheability->applyTo($build);
if ($items) {
// Make sure drupal_render() does not re-order the links.
$build['#sorted'] = TRUE;
// Get the menu name from the last link.
$item = end($items);
$link = $item['original_link'];
$menu_name = $link->getMenuName();
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
$build['#items'] = $items;
// Set cache tag.
$build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
}
return $build;
}
/**
* Builds the #items property for a menu tree's renderable array.
*
* Helper function for ::build().
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* A data structure representing the tree, as returned from
* MenuLinkTreeInterface::load().
* @param \Drupal\Core\Cache\CacheableMetadata &$tree_access_cacheability
* Internal use only. The aggregated cacheability metadata for the access
* results across the entire tree. Used when rendering the root level.
* @param \Drupal\Core\Cache\CacheableMetadata &$tree_link_cacheability
* Internal use only. The aggregated cacheability metadata for the menu
* links across the entire tree. Used when rendering the root level.
*
* @return array
* The value to use for the #items property of a renderable menu.
*
* @throws \DomainException
*/
protected function buildItems(array $tree, CacheableMetadata &$tree_access_cacheability, CacheableMetadata &$tree_link_cacheability) {
$items = array();
foreach ($tree as $data) {
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $data->link;
// Generally we only deal with visible links, but just in case.
if (!$link->isEnabled()) {
continue;
}
if ($data->access !== NULL && !$data->access instanceof AccessResultInterface) {
throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.');
}
// Gather the access cacheability of every item in the menu link tree,
// including inaccessible items. This allows us to render cache the menu
// tree, yet still automatically vary the rendered menu by the same cache
// contexts that the access results vary by.
// However, if $data->access is not an AccessResultInterface object, this
// will still render the menu link, because this method does not want to
// require access checking to be able to render a menu tree.
if ($data->access instanceof AccessResultInterface) {
$tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($data->access));
}
// Gather the cacheability of every item in the menu link tree. Some links
// may be dynamic: they may have a dynamic text (e.g. a "Hi, <user>" link
// text, which would vary by 'user' cache context), or a dynamic route
// name or route parameters.
$tree_link_cacheability = $tree_link_cacheability->merge(CacheableMetadata::createFromObject($data->link));
// Only render accessible links.
if ($data->access instanceof AccessResultInterface && !$data->access->isAllowed()) {
continue;
}
$class = ['menu-item'];
// 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[] = 'menu-item--expanded';
}
elseif ($data->hasChildren) {
$class[] = 'menu-item--collapsed';
}
// Set a class if the link is in the active trail.
if ($data->inActiveTrail) {
$class[] = 'menu-item--active-trail';
}
// Note: links are rendered in the menu.html.twig template; and they
// automatically bubble their associated cacheability metadata.
$element = array();
$element['attributes'] = new Attribute();
$element['attributes']['class'] = $class;
$element['title'] = $link->getTitle();
$element['url'] = $link->getUrlObject();
$element['url']->setOption('set_active_class', TRUE);
$element['below'] = $data->subtree ? $this->buildItems($data->subtree, $tree_access_cacheability, $tree_link_cacheability) : 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.
$items[$link->getPluginId()] = $element;
}
return $items;
}
/**
* {@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);
}
}

View file

@ -0,0 +1,128 @@
<?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:
* - 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.
* - 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 an access result
* object, it was determined already.
*
* @var \Drupal\Core\Access\AccessResultInterface|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.
*/
public $options = array();
/**
* Constructs a new \Drupal\Core\Menu\MenuLinkTreeElement.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $link
* The menu link for this element in the menu link tree.
* @param bool $has_children
* A flag as to whether this element has children even if they are not
* included in the tree (i.e. this may be TRUE even if $subtree is empty).
* @param int $depth
* The depth of this element relative to the tree root.
* @param bool $in_active_trail
* A flag as to whether this link was included in the list of active trail
* IDs used to build the tree.
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $subtree
* The children of this element in the menu link tree.
*/
public function __construct(MenuLinkInterface $link, $has_children, $depth, $in_active_trail, array $subtree) {
// Essential properties.
$this->link = $link;
$this->hasChildren = $has_children;
$this->depth = $depth;
$this->subtree = $subtree;
$this->inActiveTrail = $in_active_trail;
}
/**
* Counts all menu links in the current subtree.
*
* @return int
* The number of menu links in this subtree (one plus the number of menu
* links in all descendants).
*/
public function count() {
$sum = function ($carry, MenuLinkTreeElement $element) {
return $carry + $element->count();
};
return 1 + array_reduce($this->subtree, $sum);
}
}

View file

@ -0,0 +1,134 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkTreeInterface.
*/
namespace Drupal\Core\Menu;
/**
* Defines an interface for loading, transforming and rendering menu link trees.
*
* The main purposes of this interface are:
* - Load a list of menu links, given a menu name, using
* MenuLinkTreeInterface::load(). Loaded menu links are returned as a
* tree by looking at the links' tree meta-data.
* - Which links are loaded can be specified in the menu link tree parameters
* that are passed to the load() method. You can build your own set of
* parameters, or you can start from typical defaults by calling the
* MenuLinkTreeInterface::getCurrentRouteMenuTreeParameters() method. See
* \Drupal\Core\Menu\MenuTreeParameters for more on menu tree parameters.
* - Transform a menu link tree, by calling MenuLinkTreeInterface::transform().
* Examples include access checking, adding custom classes, extracting a
* subtree depending on the active trail, etc. Note that translation is not
* a tree transformation, because menu links themselves are responsible
* for translation. Transformations are performed by "menu link tree
* manipulators", which are functions or methods; see
* \Drupal\menu_link\DefaultMenuTreeManipulators for examples.
* - Create a render array using MenuLinkTreeInterface::build().
*/
interface MenuLinkTreeInterface {
/**
* Gets the link tree parameters for rendering a specific menu.
*
* Builds menu link tree parameters that:
* - Expand all links in the active trail based on route being viewed.
* - Expand the descendents of the links in the active trail whose
* 'expanded' flag is enabled.
*
* This only sets the (relatively complex) parameters to achieve the two above
* goals, but you can still further customize these parameters.
*
* @param string $menu_name
* The menu name, needed for retrieving the active trail and links with the
* 'expanded' flag enabled.
*
* @return \Drupal\Core\Menu\MenuTreeParameters
* The parameters to determine which menu links to be loaded into a tree.
*
* @see \Drupal\Core\Menu\MenuTreeParameters
*/
public function getCurrentRouteMenuTreeParameters($menu_name);
/**
* Loads a menu tree with a menu link plugin instance at each element.
*
* @param string $menu_name
* The name of the menu.
* @param \Drupal\Core\Menu\MenuTreeParameters $parameters
* The parameters to determine which menu links to be loaded into a tree.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* A menu link tree.
*/
public function load($menu_name, MenuTreeParameters $parameters);
/**
* Applies menu link tree manipulators to transform the given tree.
*
* @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 \Drupal\Core\Controller\ControllerResolverInterface::getControllerFromDefinition()
* - args: optional array of arguments to pass to the callable after $tree.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function transform(array $tree, array $manipulators);
/**
* Builds a renderable array from a 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 that is not shown.
* - leaf: The menu item has no submenu.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* A data structure representing the tree, as returned from
* MenuLinkTreeInterface::load().
*
* @return array
* A renderable array.
*/
public function build(array $tree);
/**
* Returns the maximum depth of tree that is supported.
*
* @return int
* The maximum depth.
*/
public function maxDepth();
/**
* Finds the height of a subtree rooted by of the given ID.
*
* @param string $id
* The ID of an item in the storage.
*
* @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);
/**
* Finds expanded links in a menu given a set of possible parents.
*
* @param string $menu_name
* The menu name.
* @param array $parents
* One or more parent IDs to match.
*
* @return array
* The menu link IDs that are flagged as expanded in this menu.
*/
public function getExpanded($menu_name, array $parents);
}

View file

@ -0,0 +1,199 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuParentFormSelector.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Default implementation of the menu parent form selector service.
*
* The form selector is a list of all appropriate menu links.
*/
class MenuParentFormSelector implements MenuParentFormSelectorInterface {
use StringTranslationTrait;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuLinkTree;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a \Drupal\Core\Menu\MenuParentFormSelector
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(MenuLinkTreeInterface $menu_link_tree, EntityManagerInterface $entity_manager, TranslationInterface $string_translation) {
$this->menuLinkTree = $menu_link_tree;
$this->entityManager = $entity_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL) {
if (!isset($menus)) {
$menus = $this->getMenuOptions();
}
$options = array();
$depth_limit = $this->getParentDepthLimit($id);
foreach ($menus as $menu_name => $menu_title) {
$options[$menu_name . ':'] = '<' . $menu_title . '>';
$parameters = new MenuTreeParameters();
$parameters->setMaxDepth($depth_limit);
$tree = $this->menuLinkTree->load($menu_name, $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkNodeAccess'),
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
);
$tree = $this->menuLinkTree->transform($tree, $manipulators);
$this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit, $cacheability);
}
return $options;
}
/**
* {@inheritdoc}
*/
public function parentSelectElement($menu_parent, $id = '', array $menus = NULL) {
$options_cacheability = new CacheableMetadata();
$options = $this->getParentSelectOptions($id, $menus, $options_cacheability);
// If no options were found, there is nothing to select.
if ($options) {
$element = array(
'#type' => 'select',
'#options' => $options,
);
if (!isset($options[$menu_parent])) {
// The requested menu parent cannot be found in the menu anymore. Try
// setting it to the top level in the current menu.
list($menu_name, $parent) = explode(':', $menu_parent, 2);
$menu_parent = $menu_name . ':';
}
if (isset($options[$menu_parent])) {
// Only provide the default value if it is valid among the options.
$element += array('#default_value' => $menu_parent);
}
$options_cacheability->applyTo($element);
return $element;
}
return array();
}
/**
* Returns the maximum depth of the possible parents of the menu link.
*
* @param string $id
* The menu link plugin ID or an empty value for a new link.
*
* @return int
* The depth related to the depth of the given menu link.
*/
protected function getParentDepthLimit($id) {
if ($id) {
$limit = $this->menuLinkTree->maxDepth() - $this->menuLinkTree->getSubtreeHeight($id);
}
else {
$limit = $this->menuLinkTree->maxDepth() - 1;
}
return $limit;
}
/**
* Iterates over all items in the tree to prepare the parents select options.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu tree.
* @param string $menu_name
* The menu name.
* @param string $indent
* The indentation string used for the label.
* @param array $options
* The select options.
* @param string $exclude
* An excluded menu link.
* @param int $depth_limit
* The maximum depth of menu links considered for the select options.
* @param \Drupal\Core\Cache\CacheableMetadata|NULL &$cacheability
* The object to add cacheability metadata to, if not NULL.
*/
protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit, CacheableMetadata &$cacheability = NULL) {
foreach ($tree as $element) {
if ($element->depth > $depth_limit) {
// Don't iterate through any links on this level.
break;
}
// Collect the cacheability metadata of the access result, as well as the
// link.
if ($cacheability) {
$cacheability = $cacheability
->merge(CacheableMetadata::createFromObject($element->access))
->merge(CacheableMetadata::createFromObject($element->link));
}
// Only show accessible links.
if (!$element->access->isAllowed()) {
continue;
}
$link = $element->link;
if ($link->getPluginId() != $exclude) {
$title = $indent . ' ' . Unicode::truncate($link->getTitle(), 30, TRUE, FALSE);
if (!$link->isEnabled()) {
$title .= ' (' . $this->t('disabled') . ')';
}
$options[$menu_name . ':' . $link->getPluginId()] = $title;
if (!empty($element->subtree)) {
$this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit, $cacheability);
}
}
}
}
/**
* Gets a list of menu names for use as options.
*
* @param array $menu_names
* (optional) Array of menu names to limit the options, or NULL to load all.
*
* @return array
* Keys are menu names (ids) values are the menu labels.
*/
protected function getMenuOptions(array $menu_names = NULL) {
$menus = $this->entityManager->getStorage('menu')->loadMultiple($menu_names);
$options = array();
/** @var \Drupal\system\MenuInterface[] $menus */
foreach ($menus as $menu) {
$options[$menu->id()] = $menu->label();
}
return $options;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuParentFormSelectorInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines an interface for menu selector form elements and menu link options.
*/
interface MenuParentFormSelectorInterface {
/**
* Gets the options for a select element to choose a menu and parent.
*
* @param string $id
* Optional ID of a link plugin. This will exclude the link and its
* children from the select options.
* @param array $menus
* Optional array of menu names as keys and titles as values to limit
* the select options. If NULL, all menus will be included.
* @param \Drupal\Core\Cache\CacheableMetadata|NULL &$cacheability
* Optional cacheability metadata object, which will be populated based on
* the accessibility of the links and the cacheability of the links.
*
* @return array
* Keyed array where the keys are contain a menu name and parent ID and
* the values are a menu name or link title indented by depth.
*/
public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL);
/**
* Gets a form element to choose a menu and parent.
*
* The specific type of form element will vary depending on the
* implementation, but callers will normally need to set the #title for the
* element.
*
* @param string $menu_parent
* A menu name and parent ID concatenated with a ':' character to use as the
* default value.
* @param string $id
* (optional) ID of a link plugin. This will exclude the link and its
* children from being selected.
* @param array $menus
* (optional) array of menu names as keys and titles as values to limit
* the values that may be selected. If NULL, all menus will be included.
*
* @return array
* A form element to choose a parent, or an empty array if no possible
* parents exist for the given parameters. The resulting form value will be
* a single string containing the chosen menu name and parent ID separated
* by a ':' character.
*/
public function parentSelectElement($menu_parent, $id = '', array $menus = NULL);
}

View file

@ -0,0 +1,223 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuTreeParameters.
*/
namespace Drupal\Core\Menu;
/**
* Provides a value object to model menu tree parameters.
*
* Menu tree parameters are used to determine the set of definitions to be
* loaded from \Drupal\Core\Menu\MenuTreeStorageInterface. Hence they determine
* the shape and content of the tree:
* - Which parent IDs should be used to restrict the tree. Only links with
* a parent in the list will be included.
* - Which menu links are omitted, depending on the minimum and maximum depth.
*
* @todo Add getter methods and make all properties protected and define an
* interface instead of using the concrete class to type hint.
* https://www.drupal.org/node/2302041
*/
class MenuTreeParameters {
/**
* A menu link plugin ID that should be used as the root.
*
* By default the root ID of empty string '' is used. However, when only the
* descendants (subtree) of a certain menu link are needed, a custom root can
* be specified.
*
* @var string
*/
public $root = '';
/**
* The minimum depth of menu links in the resulting tree relative to the root.
*
* Defaults to 1, which is the default to build a whole tree for a menu
* (excluding the root).
*
* @var int|null
*/
public $minDepth = NULL;
/**
* The maximum depth of menu links in the resulting tree relative to the root.
*
* @var int|null
*/
public $maxDepth = NULL;
/**
* An array of parent link IDs.
*
* This restricts the tree to only menu links that are at the top level or
* have a parent ID in this list. If empty, the whole menu tree is built.
*
* @var string[]
*/
public $expandedParents = array();
/**
* The IDs from the currently active menu link to the root of the whole tree.
*
* This is an array of menu link plugin IDs, representing the trail from the
* currently active menu link to the ("real") root of that menu link's menu.
* This does not affect the way the tree is built. It is only used to set the
* value of the inActiveTrail property for each tree element.
*
* @var string[]
*/
public $activeTrail = array();
/**
* The conditions used to restrict which links are loaded.
*
* An associative array of custom query condition key/value pairs.
*
* @var array
*/
public $conditions = array();
/**
* Sets a root for menu tree loading.
*
* @param string $root
* A menu link plugin ID, or empty string '' to use the root of the whole
* tree.
*
* @return $this
*
* @codeCoverageIgnore
*/
public function setRoot($root) {
$this->root = (string) $root;
return $this;
}
/**
* Sets a minimum depth for menu tree loading.
*
* @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 maximum depth for menu tree loading.
*
* @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 parent menu links IDs to restrict the tree.
*
* @param string[] $parents
* An array containing parent IDs. If supplied, the tree is limited to
* links that have these parents.
*
* @return $this
*/
public function addExpandedParents(array $parents) {
$this->expandedParents = array_merge($this->expandedParents, $parents);
$this->expandedParents = array_unique($this->expandedParents);
return $this;
}
/**
* Sets the active trail IDs used to set the inActiveTrail property.
*
* @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
* (optional) The comparison operator, such as =, <, or >=. It also accepts
* more complex options such as IN, LIKE, or BETWEEN. If NULL, defaults to
* the = operator.
*
* @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 links that are not enabled.
*
* @return $this
*/
public function onlyEnabledLinks() {
$this->addCondition('enabled', 1);
return $this;
}
/**
* Ensures only the top level of the tree is loaded.
*
* @return $this
*/
public function setTopLevelOnly() {
$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 normal root ID is the empty string, '', which does not correspond to an
* actual menu link. Hence when loading a menu link tree without specifying a
* custom root the tree will start at the children even if this method has not
* been called.
*
* @return $this
*/
public function excludeRoot() {
$this->setMinDepth(1);
return $this;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,281 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuTreeStorageInterface.
*/
namespace Drupal\Core\Menu;
/**
* Defines an interface for storing a menu tree containing menu link IDs.
*
* The tree contains a hierarchy of menu links which have an ID as well as a
* route name or external URL.
*/
interface MenuTreeStorageInterface {
/**
* The maximum depth of tree the storage implementation supports.
*
* @return int
* The maximum depth.
*/
public function maxDepth();
/**
* Clears all definitions cached in memory.
*/
public function resetDefinitions();
/**
* Rebuilds the stored menu link definitions.
*
* Links that saved by passing definitions into this method must be included
* on all future calls, or they will be purged. This allows for automatic
* cleanup e.g. when modules are uninstalled.
*
* @param array $definitions
* The new menu link definitions.
*
*/
public function rebuild(array $definitions);
/**
* Loads a menu link plugin definition from the storage.
*
* @param string $id
* The menu link plugin ID.
*
* @return array|FALSE
* The plugin definition, or FALSE if no definition was found for the ID.
*/
public function load($id);
/**
* Loads multiple plugin definitions from the storage.
*
* @param array $ids
* An array of plugin IDs.
*
* @return array
* An array of plugin definition arrays keyed by plugin ID, which are the
* actual definitions after the loadMultiple() including all those plugins
* from $ids.
*/
public function loadMultiple(array $ids);
/**
* Loads multiple plugin definitions from the storage based on properties.
*
* @param array $properties
* The properties to filter by.
*
* @throws \InvalidArgumentException
* Thrown if an invalid property name is specified in $properties.
*
* @return array
* An array of menu link definition arrays.
*/
public function loadByProperties(array $properties);
/**
* Loads multiple plugin definitions from the storage based on route.
*
* @param string $route_name
* The route name.
* @param array $route_parameters
* (optional) The route parameters. Defaults to an empty array.
* @param string $menu_name
* (optional) Restricts the found links to just those in the named menu.
*
* @return array
* An array of menu link definitions keyed by ID and ordered by depth.
*/
public function loadByRoute($route_name, array $route_parameters = array(), $menu_name = NULL);
/**
* Saves a plugin definition to the storage.
*
* @param array $definition
* A definition for a \Drupal\Core\Menu\MenuLinkInterface plugin.
*
* @return array
* The menu names affected by the save operation. This will be one menu
* name if the link is saved to the sane menu, or two if it is saved to a
* new menu.
*
* @throws \Exception
* Thrown if the storage back-end does not exist and could not be created.
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown if the definition is invalid - for example, if the specified
* parent would cause the links children to be moved to greater than the
* maximum depth.
*/
public function save(array $definition);
/**
* Deletes a menu link definition from the storage.
*
* @param string $id
* The menu link plugin ID.
*/
public function delete($id);
/**
* Loads a menu link tree from the storage.
*
* This function may be used build the data for a menu tree only, for example
* to further massage the data manually before further processing happens.
* MenuLinkTree::checkAccess() needs to be invoked afterwards.
*
* The tree order is maintained using an optimized algorithm, for example by
* storing each parent in an individual field, see
* https://www.drupal.org/node/141866 for more details. However, any details
* of the storage should not be relied upon since it may be swapped with a
* different implementation.
*
* @param string $menu_name
* The name of the menu.
* @param \Drupal\Core\Menu\MenuTreeParameters $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 containing an array.
* @see static::treeDataRecursive()
* - route_names: An array of all route names used in the tree.
*/
public function loadTreeData($menu_name, MenuTreeParameters $parameters);
/**
* Loads all the enabled menu links that are below the given ID.
*
* The returned links are not ordered, and visible children will be included
* even if they have parent that is not enabled or ancestor so would not
* normally appear in a rendered tree.
*
* @param string $id
* The parent menu link ID.
* @param int $max_relative_depth
* The maximum relative depth of the children relative to the passed parent.
*
* @return array
* An array of enabled link definitions, keyed by ID.
*/
public function loadAllChildren($id, $max_relative_depth = NULL);
/**
* Loads all the IDs for menu links that are below the given ID.
*
* @param string $id
* The parent menu link ID.
*
* @return array
* An unordered array of plugin IDs corresponding to all children.
*/
public function getAllChildIds($id);
/**
* Loads a subtree rooted by the given ID.
*
* The returned links are structured like those from loadTreeData().
*
* @param string $id
* The menu link plugin ID.
* @param int $max_relative_depth
* (optional) The maximum depth of child menu links relative to the passed
* in. Defaults to NULL, in which case the full subtree will be returned.
*
* @return array
* An array with 2 elements:
* - subtree: A fully built menu tree element or FALSE.
* - route_names: An array of all route names used in the subtree.
*/
public function loadSubtreeData($id, $max_relative_depth = NULL);
/**
* Returns all the IDs that represent the path to the root of the tree.
*
* @param string $id
* A menu link ID.
*
* @return array
* An associative array of IDs with keys equal to values that represents the
* path from the given ID to the root of the tree. If $id is an ID that
* exists, the returned array will at least include it. An empty array is
* returned if the ID does not exist in the storage. An example $id (8) with
* two parents (1, 6) looks like the following:
* @code
* array(
* 'p1' => 1,
* 'p2' => 6,
* 'p3' => 8,
* 'p4' => 0,
* 'p5' => 0,
* 'p6' => 0,
* 'p7' => 0,
* 'p8' => 0,
* 'p9' => 0
* )
* @endcode
*/
public function getRootPathIds($id);
/**
* Finds expanded links in a menu given a set of possible parents.
*
* @param string $menu_name
* The menu name.
* @param array $parents
* One or more parent IDs to match.
*
* @return array
* The menu link IDs that are flagged as expanded in this menu.
*/
public function getExpanded($menu_name, array $parents);
/**
* Finds the height of a subtree rooted by the given ID.
*
* @param string $id
* The ID of an item in the storage.
*
* @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);
/**
* Determines whether a specific menu name is used in the tree.
*
* @param string $menu_name
* The menu name.
*
* @return bool
* Returns TRUE if the given menu name is used, otherwise FALSE.
*/
public function menuNameInUse($menu_name);
/**
* Returns the used menu names in the tree storage.
*
* @return array
* The menu names.
*/
public function getMenuNames();
/**
* Counts the total number of menu links in one menu or all menus.
*
* @param string $menu_name
* (optional) The menu name to count by. Defaults to all menus.
*
* @return int
* The number of menu links in the named menu, or in all menus if the menu
* name is NULL.
*/
public function countMenuLinks($menu_name = NULL);
}

View file

@ -0,0 +1,184 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\StaticMenuLinkOverrides.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Config\ConfigFactoryInterface;
/**
* Defines an implementation of the menu link override using a config file.
*/
class StaticMenuLinkOverrides implements StaticMenuLinkOverridesInterface {
/**
* The config name used to store the overrides.
*
* This configuration can not be overridden by configuration overrides because
* menu links and these overrides are cached in a way that is not override
* aware.
*
* @var string
*/
protected $configName = 'core.menu.static_menu_link_overrides';
/**
* The menu link overrides config object.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The config factory object.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a StaticMenuLinkOverrides object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A configuration factory instance.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* Gets the configuration object when needed.
*
* Since this service is injected into all static menu link objects, but
* only used when updating one, avoid actually loading the config when it's
* not needed.
*/
protected function getConfig() {
if (empty($this->config)) {
// Get an override free and editable configuration object.
$this->config = $this->configFactory->getEditable($this->configName);
}
return $this->config;
}
/**
* {@inheritdoc}
*/
public function reload() {
$this->config = NULL;
$this->configFactory->reset($this->configName);
}
/**
* {@inheritdoc}
*/
public function loadOverride($id) {
$all_overrides = $this->getConfig()->get('definitions');
$id = static::encodeId($id);
return $id && isset($all_overrides[$id]) ? $all_overrides[$id] : array();
}
/**
* {@inheritdoc}
*/
public function deleteMultipleOverrides(array $ids) {
$all_overrides = $this->getConfig()->get('definitions');
$save = FALSE;
foreach ($ids as $id) {
$id = static::encodeId($id);
if (isset($all_overrides[$id])) {
unset($all_overrides[$id]);
$save = TRUE;
}
}
if ($save) {
$this->getConfig()->set('definitions', $all_overrides)->save();
}
return $save;
}
/**
* {@inheritdoc}
*/
public function deleteOverride($id) {
return $this->deleteMultipleOverrides(array($id));
}
/**
* {@inheritdoc}
*/
public function loadMultipleOverrides(array $ids) {
$result = array();
if ($ids) {
$all_overrides = $this->getConfig()->get('definitions') ?: array();
foreach ($ids as $id) {
$encoded_id = static::encodeId($id);
if (isset($all_overrides[$encoded_id])) {
$result[$id] = $all_overrides[$encoded_id];
}
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function saveOverride($id, array $definition) {
// Only allow to override a specific subset of the keys.
$expected = array(
'menu_name' => '',
'parent' => '',
'weight' => 0,
'expanded' => FALSE,
'enabled' => FALSE,
);
// Filter the overrides to only those that are expected.
$definition = array_intersect_key($definition, $expected);
// Ensure all values are set.
$definition = $definition + $expected;
if ($definition) {
// Cast keys to avoid config schema during save.
$definition['menu_name'] = (string) $definition['menu_name'];
$definition['parent'] = (string) $definition['parent'];
$definition['weight'] = (int) $definition['weight'];
$definition['expanded'] = (bool) $definition['expanded'];
$definition['enabled'] = (bool) $definition['enabled'];
$id = static::encodeId($id);
$all_overrides = $this->getConfig()->get('definitions');
// Combine with any existing data.
$all_overrides[$id] = $definition + $this->loadOverride($id);
$this->getConfig()->set('definitions', $all_overrides)->save(TRUE);
}
return array_keys($definition);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->getConfig()->getCacheTags();
}
/**
* Encodes the ID by replacing dots with double underscores.
*
* This is done because config schema uses dots for its internal type
* hierarchy. Double underscores are converted to triple underscores to
* avoid accidental conflicts.
*
* @param string $id
* The menu plugin ID.
*
* @return string
* The menu plugin ID with double underscore instead of dots.
*/
protected static function encodeId($id) {
return strtr($id, array('.' => '__', '__' => '___'));
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\StaticMenuLinkOverridesInterface.
*/
namespace Drupal\Core\Menu;
/**
* Defines an interface for objects which overrides menu links defined in YAML.
*/
interface StaticMenuLinkOverridesInterface {
/**
* Reloads the overrides from config.
*
* Forces all overrides to be reloaded from config storage to compare the
* override value with the value submitted during test form submission.
*/
public function reload();
/**
* Loads any overrides to the definition of a static (YAML-defined) link.
*
* @param string $id
* A menu link plugin ID.
*
* @return array|NULL
* An override with following supported keys:
* - parent
* - weight
* - menu_name
* - expanded
* - enabled
* or NULL if there is no override for the given ID.
*/
public function loadOverride($id);
/**
* Deletes any overrides to the definition of a static (YAML-defined) link.
*
* @param string $id
* A menu link plugin ID.
*/
public function deleteOverride($id);
/**
* Deletes multiple overrides to definitions of static (YAML-defined) links.
*
* @param array $ids
* Array of menu link plugin IDs.
*/
public function deleteMultipleOverrides(array $ids);
/**
* Loads overrides to multiple definitions of a static (YAML-defined) link.
*
* @param array $ids
* Array of menu link plugin IDs.
*
* @return array
* One or override keys by plugin ID.
*
* @see \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
*/
public function loadMultipleOverrides(array $ids);
/**
* Saves the override.
*
* @param string $id
* A menu link plugin ID.
* @param array $definition
* The definition values to override. Supported keys:
* - menu_name
* - parent
* - weight
* - expanded
* - enabled
*
* @return array
* A list of properties which got saved.
*/
public function saveOverride($id, array $definition);
/**
* The unique cache tag associated with this menu link override.
*
* @return string[]
* An array of cache tags.
*/
public function getCacheTags();
}

View file

@ -0,0 +1,603 @@
<?php
/**
* @file
* Hooks and documentation related to the menu system, routing, and links.
*/
/**
* @defgroup menu Menu and routing system
* @{
* Define the navigation menus, and route page requests to code based on URLs.
*
* @section sec_overview Overview and terminology
* The Drupal routing system defines how Drupal responds to URL requests that
* the web server passes on to Drupal. The routing system is based on the
* @link http://symfony.com Symfony framework. @endlink The central idea is
* that Drupal subsystems and modules can register routes (basically, URL
* paths and context); they can also register to respond dynamically to
* routes, for more flexibility. When Drupal receives a URL request, it will
* attempt to match the request to a registered route, and query dynamic
* responders. If a match is made, Drupal will then instantiate the required
* classes, gather the data, format it, and send it back to the web browser.
* Otherwise, Drupal will return a 404 or 403 response.
*
* The menu system uses routes; it is used for navigation menus, local tasks,
* local actions, and contextual links:
* - Navigation menus are hierarchies of menu links; links point to routes or
* URLs.
* - Menu links and their hierarchies can be defined by Drupal subsystems
* and modules, or created in the user interface using the Menu UI module.
* - Local tasks are groups of related routes. Local tasks are usually rendered
* as a group of tabs.
* - Local actions are used for operations such as adding a new item on a page
* that lists items of some type. Local actions are usually rendered as
* buttons.
* - Contextual links are actions that are related to sections of rendered
* output, and are usually rendered as a pop-up list of links. The
* Contextual Links module handles the gathering and rendering of contextual
* links.
*
* The following sections of this topic provide an overview of the routing and
* menu APIs. For more detailed information, see
* https://www.drupal.org/developing/api/8/routing and
* https://www.drupal.org/developing/api/8/menu
*
* @section sec_register Registering simple routes
* To register a route, add lines similar to this to a module_name.routing.yml
* file in your top-level module directory:
* @code
* dblog.overview:
* path: '/admin/reports/dblog'
* defaults:
* _controller: '\Drupal\dblog\Controller\DbLogController::overview'
* _title: 'Recent log messages'
* requirements:
* _permission: 'access site reports'
* @endcode
* Some notes:
* - The first line is the machine name of the route. Typically, it is prefixed
* by the machine name of the module that defines the route, or the name of
* a subsystem.
* - The 'path' line gives the URL path of the route (relative to the site's
* base URL).
* - The 'defaults' section tells how to build the main content of the route,
* and can also give other information, such as the page title and additional
* arguments for the route controller method. There are several possibilities
* for how to build the main content, including:
* - _controller: A callable, usually a method on a page controller class
* (see @ref sec_controller below for details).
* - _form: A form controller class. See the
* @link form_api Form API topic @endlink for more information about
* form controllers.
* - _entity_form: A form for editing an entity. See the
* @link entity_api Entity API topic @endlink for more information.
* - The 'requirements' section is used in Drupal to give access permission
* instructions (it has other uses in the Symfony framework). Most
* routes have a simple permission-based access scheme, as shown in this
* example. See the @link user_api Permission system topic @endlink for
* more information about permissions.
*
* See https://www.drupal.org/node/2092643 for more details about *.routing.yml
* files, and https://www.drupal.org/node/2122201 for information on how to
* set up dynamic routes. The @link events Events topic @endlink is also
* relevant to dynamic routes.
*
* @section sec_placeholders Defining routes with placeholders
* Some routes have placeholders in them, and these can also be defined in a
* module_name.routing.yml file, as in this example from the Block module:
* @code
* entity.block.edit_form:
* path: '/admin/structure/block/manage/{block}'
* defaults:
* _entity_form: 'block.default'
* _title: 'Configure block'
* requirements:
* _entity_access: 'block.update'
* @endcode
* In the path, '{block}' is a placeholder - it will be replaced by the
* ID of the block that is being configured by the entity system. See the
* @link entity_api Entity API topic @endlink for more information.
*
* @section sec_controller Route controllers for simple routes
* For simple routes, after you have defined the route in a *.routing.yml file
* (see @ref sec_register above), the next step is to define a page controller
* class and method. Page controller classes do not necessarily need to
* implement any particular interface or extend any particular base class. The
* only requirement is that the method specified in your *.routing.yml file
* returns:
* - A render array (see the
* @link theme_render Theme and render topic @endlink for more information).
* This render array is then rendered in the requested format (HTML, dialog,
* modal, AJAX are supported by default). In the case of HTML, it will be
* surrounded by blocks by default: the Block module is enabled by default,
* and hence its Page Display Variant that surrounds the main content with
* blocks is also used by default.
* - A \Symfony\Component\HttpFoundation\Response object.
* As a note, if your module registers multiple simple routes, it is usual
* (and usually easiest) to put all of their methods on one controller class.
*
* If the route has placeholders (see @ref sec_placeholders above) the
* placeholders will be passed to the method (using reflection) by name.
* For example, the placeholder '{myvar}' in a route will become the $myvar
* parameter to the method.
*
* Most controllers will need to display some information stored in the Drupal
* database, which will involve using one or more Drupal services (see the
* @link container Services and container topic @endlink). In order to properly
* inject services, a controller should implement
* \Drupal\Core\DependencyInjection\ContainerInjectionInterface; simple
* controllers can do this by extending the
* \Drupal\Core\Controller\ControllerBase class. See
* \Drupal\dblog\Controller\DbLogController for a straightforward example of
* a controller class.
*
* @section sec_links Defining menu links for the administrative menu
* Routes for administrative tasks can be added to the main Drupal
* administrative menu hierarchy. To do this, add lines like the following to a
* module_name.links.menu.yml file (in the top-level directory for your module):
* @code
* dblog.overview:
* title: 'Recent log messages'
* parent: system.admin_reports
* description: 'View events that have recently been logged.'
* route_name: dblog.overview
* weight: -1
* @endcode
* Some notes:
* - The first line is the machine name for your menu link, which usually
* matches the machine name of the route (given in the 'route_name' line).
* - parent: The machine name of the menu link that is the parent in the
* administrative hierarchy. See system.links.menu.yml to find the main
* skeleton of the hierarchy.
* - weight: Lower (negative) numbers come before higher (positive) numbers,
* for menu items with the same parent.
*
* 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.
*
* @section sec_tasks Defining groups of local tasks (tabs)
* Local tasks appear as tabs on a page when there are at least two defined for
* a route, including the base route as the main tab, and additional routes as
* other tabs. Static local tasks can be defined by adding lines like the
* following to a module_name.links.task.yml file (in the top-level directory
* for your module):
* @code
* book.admin:
* route_name: book.admin
* title: 'List'
* base_route: book.admin
* book.settings:
* route_name: book.settings
* title: 'Settings'
* base_route: book.admin
* weight: 100
* @endcode
* Some notes:
* - The first line is the machine name for your local task, which usually
* matches the machine name of the route (given in the 'route_name' line).
* - base_route: The machine name of the main task (tab) for the set of local
* tasks.
* - weight: Lower (negative) numbers come before higher (positive) numbers,
* for tasks on the same base route. If there is a tab whose route
* matches the base route, that will be the default/first tab shown.
*
* Local tasks from other modules can be altered using
* hook_menu_local_tasks_alter().
*
* @todo Derivatives are in flux for these; when they are more stable, add
* documentation here.
*
* @section sec_actions Defining local actions for routes
* Local actions can be defined for operations related to a given route. For
* instance, adding content is a common operation for the content management
* page, so it should be a local action. Static local actions can be
* defined by adding lines like the following to a
* module_name.links.action.yml file (in the top-level directory for your
* module):
* @code
* node.add_page:
* route_name: node.add_page
* title: 'Add content'
* appears_on:
* - system.admin_content
* @endcode
* Some notes:
* - The first line is the machine name for your local action, which usually
* matches the machine name of the route (given in the 'route_name' line).
* - appears_on: Machine names of one or more routes that this local task
* should appear on.
*
* Local actions from other modules can be altered using
* hook_menu_local_actions_alter().
*
* @todo Derivatives are in flux for these; when they are more stable, add
* documentation here.
*
* @section sec_contextual Defining contextual links
* Contextual links are displayed by the Contextual Links module for user
* interface elements whose render arrays have a '#contextual_links' element
* defined. For example, a block render array might look like this, in part:
* @code
* array(
* '#contextual_links' => array(
* 'block' => array(
* 'route_parameters' => array('block' => $entity->id()),
* ),
* ),
* @endcode
* In this array, the outer key 'block' defines a "group" for contextual
* links, and the inner array provides values for the route's placeholder
* parameters (see @ref sec_placeholders above).
*
* To declare that a defined route should be a contextual link for a
* contextual links group, put lines like the following in a
* module_name.links.contextual.yml file (in the top-level directory for your
* module):
* @code
* block_configure:
* title: 'Configure block'
* route_name: 'entity.block.edit_form'
* group: 'block'
* @endcode
* Some notes:
* - The first line is the machine name for your contextual link, which usually
* matches the machine name of the route (given in the 'route_name' line).
* - group: This needs to match the link group defined in the render array.
*
* Contextual links from other modules can be altered using
* hook_contextual_links_alter().
*
* @todo Derivatives are in flux for these; when they are more stable, add
* documentation here.
*
* @section sec_rendering 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::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
* $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);
*
* $menu_html = drupal_render($menu);
* @endcode
*
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alters all the menu links discovered by the menu link plugin manager.
*
* @param array $links
* The link definitions to be altered.
*
* @return array
* An array of discovered menu links. Each link has a key that is the machine
* name, which must be unique. By default, use the route name as the
* machine name. In cases where multiple links use the same route name, such
* as two links to the same page in different menus, or two links using the
* same route name but different route parameters, the suggested machine name
* patten is the route name followed by a dot and a unique suffix. For
* example, an additional logout link might have a machine name of
* user.logout.navigation, and default links provided to edit the article and
* page content types could use machine names
* entity.node_type.edit_form.article and entity.node_type.edit_form.page.
* Since the machine name may be arbitrary, you should never write code that
* assumes it is identical to the route name.
*
* The value corresponding to each machine name key is an associative array
* that may contain the following key-value pairs:
* - title: (required) The untranslated title of the menu link.
* - description: The untranslated description of the link.
* - route_name: (optional) The route name to be used to build the path.
* Either the route_name or url element must be provided.
* - route_parameters: (optional) The route parameters to build the path.
* - url: (optional) If you have an external link use this element instead of
* providing route_name.
* - parent: (optional) The machine name of the link that is this link's menu
* parent.
* - weight: (optional) An integer that determines the relative position of
* items in the menu; higher-weighted items sink. Defaults to 0. Menu items
* with the same weight are ordered alphabetically.
* - menu_name: (optional) The machine name of a menu to put the link in, if
* not the default Tools menu.
* - expanded: (optional) If set to TRUE, and if a menu link is provided for
* this menu item (as a result of other properties), then the menu link is
* always expanded, equivalent to its 'always expanded' checkbox being set
* in the UI.
* - options: (optional) An array of options to be passed to _l() when
* generating a link from this menu item.
*
* @ingroup menu
*/
function hook_menu_links_discovered_alter(&$links) {
// Change the weight and title of the user.logout link.
$links['user.logout']['weight'] = -10;
$links['user.logout']['title'] = 'Logout';
}
/**
* Alter tabs and actions displayed on the page before they are rendered.
*
* This hook is invoked by menu_local_tasks(). The system-determined tabs and
* actions are passed in by reference. Additional tabs or actions may be added.
*
* Each tab or action is an associative array containing:
* - #theme: The theme function to use to render.
* - #link: An associative array containing:
* - title: The localized title of the link.
* - href: The system path to link to.
* - localized_options: An array of options to pass to _l().
* - #weight: The link's weight compared to other links.
* - #active: Whether the link should be marked as 'active'.
*
* @param array $data
* An associative array containing:
* - actions: A list of of actions keyed by their href, each one being an
* associative array as described above.
* - tabs: A list of (up to 2) tab levels that contain a list of of tabs keyed
* by their href, each one being an associative array as described above.
* @param string $route_name
* The route name of the page.
*
* @ingroup menu
*/
function hook_menu_local_tasks(&$data, $route_name) {
// Add an action linking to node/add to all pages.
$data['actions']['node/add'] = array(
'#theme' => 'menu_local_action',
'#link' => array(
'title' => t('Add content'),
'url' => Url::fromRoute('node.add_page'),
'localized_options' => array(
'attributes' => array(
'title' => t('Add content'),
),
),
),
);
// Add a tab linking to node/add to all pages.
$data['tabs'][0]['node/add'] = array(
'#theme' => 'menu_local_task',
'#link' => array(
'title' => t('Example tab'),
'url' => Url::fromRoute('node.add_page'),
'localized_options' => array(
'attributes' => array(
'title' => t('Add content'),
),
),
),
);
}
/**
* Alter tabs and actions displayed on the page before they are rendered.
*
* This hook is invoked by menu_local_tasks(). The system-determined tabs and
* actions are passed in by reference. Existing tabs or actions may be altered.
*
* @param array $data
* An associative array containing tabs and actions. See
* hook_menu_local_tasks() for details.
* @param string $route_name
* The route name of the page.
*
* @see hook_menu_local_tasks()
*
* @ingroup menu
*/
function hook_menu_local_tasks_alter(&$data, $route_name) {
}
/**
* Alter local actions plugins.
*
* @param array $local_actions
* The array of local action plugin definitions, keyed by plugin ID.
*
* @see \Drupal\Core\Menu\LocalActionInterface
* @see \Drupal\Core\Menu\LocalActionManager
*
* @ingroup menu
*/
function hook_menu_local_actions_alter(&$local_actions) {
}
/**
* Alter local tasks plugins.
*
* @param array $local_tasks
* The array of local tasks plugin definitions, keyed by plugin ID.
*
* @see \Drupal\Core\Menu\LocalTaskInterface
* @see \Drupal\Core\Menu\LocalTaskManager
*
* @ingroup menu
*/
function hook_local_tasks_alter(&$local_tasks) {
// Remove a specified local task plugin.
unset($local_tasks['example_plugin_id']);
}
/**
* Alter contextual links before they are rendered.
*
* This hook is invoked by
* \Drupal\Core\Menu\ContextualLinkManager::getContextualLinkPluginsByGroup().
* The system-determined contextual links are passed in by reference. Additional
* links may be added and existing links can be altered.
*
* Each contextual link contains the following entries:
* - title: The localized title of the link.
* - route_name: The route name of the link.
* - route_parameters: The route parameters of the link.
* - localized_options: An array of URL options.
* - (optional) weight: The weight of the link, which is used to sort the links.
*
*
* @param array $links
* An associative array containing contextual links for the given $group,
* as described above. The array keys are used to build CSS class names for
* contextual links and must therefore be unique for each set of contextual
* links.
* @param string $group
* The group of contextual links being rendered.
* @param array $route_parameters.
* The route parameters passed to each route_name of the contextual links.
* For example:
* @code
* array('node' => $node->id())
* @endcode
*
* @see \Drupal\Core\Menu\ContextualLinkManager
*
* @ingroup menu
*/
function hook_contextual_links_alter(array &$links, $group, array $route_parameters) {
if ($group == 'menu') {
// Dynamically use the menu name for the title of the menu_edit contextual
// link.
$menu = \Drupal::entityManager()->getStorage('menu')->load($route_parameters['menu']);
$links['menu_edit']['title'] = t('Edit menu: !label', array('!label' => $menu->label()));
}
}
/**
* Alter the plugin definition of contextual links.
*
* @param array $contextual_links
* An array of contextual_links plugin definitions, keyed by contextual link
* ID. Each entry contains the following keys:
* - title: The displayed title of the link
* - route_name: The route_name of the contextual link to be displayed
* - group: The group under which the contextual links should be added to.
* Possible values are e.g. 'node' or 'menu'.
*
* @see \Drupal\Core\Menu\ContextualLinkManager
*
* @ingroup menu
*/
function hook_contextual_links_plugins_alter(array &$contextual_links) {
$contextual_links['menu_edit']['title'] = 'Edit the menu';
}
/**
* Perform alterations to the breadcrumb built by the BreadcrumbManager.
*
* @param array $breadcrumb
* An array of breadcrumb link a tags, returned by the breadcrumb manager
* build method, for example
* @code
* array('<a href="/">Home</a>');
* @endcode
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param array $context
* May include the following key:
* - builder: the instance of
* \Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface that constructed this
* breadcrumb, or NULL if no builder acted based on the current attributes.
*
* @ingroup menu
*/
function hook_system_breadcrumb_alter(array &$breadcrumb, \Drupal\Core\Routing\RouteMatchInterface $route_match, array $context) {
// Add an item to the end of the breadcrumb.
$breadcrumb[] = Drupal::l(t('Text'), 'example_route_name');
}
/**
* Alter the parameters for links.
*
* @param array $variables
* An associative array of variables defining a link. The link may be either a
* "route link" using \Drupal\Core\Utility\LinkGenerator::link(), which is
* exposed as the 'link_generator' service or a link generated by _l(). If the
* link is a "route link", 'route_name' will be set, otherwise 'path' will be
* set. The following keys can be altered:
* - text: The link text for the anchor tag as a translated string.
* - url_is_active: Whether or not the link points to the currently active
* URL.
* - url: The \Drupal\Core\Url object.
* - options: An associative array of additional options that will be passed
* to either \Drupal\Core\Routing\UrlGenerator::generateFromPath() or
* \Drupal\Core\Routing\UrlGenerator::generateFromRoute() to generate the
* href attribute for this link, and also used when generating the link.
* Defaults to an empty array. It may contain the following elements:
* - 'query': An array of query key/value-pairs (without any URL-encoding) to
* append to the URL.
* - absolute: Whether to force the output to be an absolute link (beginning
* with http:). Useful for links that will be displayed outside the site,
* such as in an RSS feed. Defaults to FALSE.
* - language: An optional language object. May affect the rendering of
* the anchor tag, such as by adding a language prefix to the path.
* - attributes: An associative array of HTML attributes to apply to the
* anchor tag. If element 'class' is included, it must be an array; 'title'
* must be a string; other elements are more flexible, as they just need
* to work as an argument for the constructor of the class
* Drupal\Core\Template\Attribute($options['attributes']).
* - html: Whether or not HTML should be allowed as the link text. If FALSE,
* the text will be run through
* \Drupal\Component\Utility\SafeMarkup::checkPlain() before being output.
*
* @see \Drupal\Core\Routing\UrlGenerator::generateFromPath()
* @see \Drupal\Core\Routing\UrlGenerator::generateFromRoute()
*/
function hook_link_alter(&$variables) {
// Add a warning to the end of route links to the admin section.
if (isset($variables['route_name']) && strpos($variables['route_name'], 'admin') !== FALSE) {
$variables['text'] .= ' (Warning!)';
}
}
/**
* @} End of "addtogroup hooks".
*/