Skip to content
Snippets Groups Projects
Commit 8bb62da2 authored by Alex Pott's avatar Alex Pott
Browse files

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

Issue #2301239 by pwolanin, dawehner, Wim Leers, effulgentsia, joelpittet, larowlan, xjm, YesCT, kgoel, victoru, berdir, likin, and plach: MenuLinkNG part1 (no UI or conversions): plugins (static + MenuLinkContent) + MenuLinkManager + MenuTreeStorage.
parent 2093f346
No related branches found
No related tags found
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
Showing
with 4613 additions and 2 deletions
......@@ -63,6 +63,13 @@ services:
factory_method: get
factory_service: cache_factory
arguments: [entity]
cache.menu:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory_method: get
factory_service: cache_factory
arguments: [menu]
cache.render:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
......@@ -265,6 +272,9 @@ services:
plugin.manager.action:
class: Drupal\Core\Action\ActionManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
plugin.manager.menu.link:
class: Drupal\Core\Menu\MenuLinkManager
arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler']
plugin.manager.menu.local_action:
class: Drupal\Core\Menu\LocalActionManager
arguments: ['@controller_resolver', '@request_stack', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user']
......@@ -279,6 +289,13 @@ services:
parent: default_plugin_manager
plugin.cache_clearer:
class: Drupal\Core\Plugin\CachedDiscoveryClearer
menu.tree_storage:
class: Drupal\Core\Menu\MenuTreeStorage
arguments: ['@database', '@cache.menu', 'menu_tree']
public: false # Private to plugin.manager.menu.link and menu.link_tree
menu_link.static.overrides:
class: Drupal\Core\Menu\StaticMenuLinkOverrides
arguments: ['@config.factory']
request_stack:
class: Symfony\Component\HttpFoundation\RequestStack
tags:
......
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkBase.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\String;
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 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 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 isHidden() {
return (bool) $this->pluginDefinition['hidden'];
}
/**
* {@inheritdoc}
*/
public function isExpanded() {
return (bool) $this->pluginDefinition['expanded'];
}
/**
* {@inheritdoc}
*/
public function isResetable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
return (bool) $this->getTranslateRoute();
}
/**
* {@inheritdoc}
*/
public function isDeletable() {
return (bool) $this->getDeleteRoute();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
if ($this->pluginDefinition['description']) {
return $this->t($this->pluginDefinition['description']);
}
return '';
}
/**
* {@inheritdoc}
*/
public function getOptions() {
return $this->pluginDefinition['options'] ?: array();
}
/**
* {@inheritdoc}
*/
public function getMetaData() {
return $this->pluginDefinition['metadata'] ?: array();
}
/**
* {@inheritdoc}
*/
public function isCacheable() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getUrlObject($title_attribute = TRUE) {
$options = $this->getOptions();
$description = $this->getDescription();
if ($title_attribute && $description) {
$options['attributes']['title'] = $description;
}
if (empty($this->pluginDefinition['url'])) {
return new Url($this->pluginDefinition['route_name'], $this->pluginDefinition['route_parameters'], $options);
}
else {
$url = Url::createFromPath($this->pluginDefinition['url']);
$url->setOptions($options);
return $url;
}
}
/**
* {@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(String::format('Menu link plugin with ID @id does not support deletion', array('@id' => $this->getPluginId())));
}
}
<?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,
'hidden' => 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 isResetable() {
// 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);
if ($persist) {
$this->staticOverride->saveOverride($this->getPluginId(), $overrides);
}
// Update the definition.
$this->pluginDefinition = $overrides + $this->getPluginDefinition();
return $this->pluginDefinition;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLinkInterface.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
/**
* Defines an interface for classes providing a type of menu link.
*/
interface MenuLinkInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
/**
* 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 hidden.
*
* @return bool
* TRUE for hidden, FALSE otherwise.
*/
public function isHidden();
/**
* 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 isResetable();
/**
* 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 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
* The options for the menu link.
*/
public function getOptions();
/**
* Returns any metadata for this link.
*
* @return array
* The metadata for the menu link.
*/
public function getMetaData();
/**
* Returns whether the rendered link can be cached.
*
* The plugin class may make some or all of the data used in the Url object
* and build array dynamic. For example, it could include the current user
* name in the title, the current time in the description, or a destination
* query string. In addition the route parameters may be dynamic so an access
* check should be performed for each user.
*
* @return bool
* TRUE if the link can be cached, FALSE otherwise.
*/
public function isCacheable();
/**
* 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 array|null
* An array with keys route_name and route_parameters, 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 array|null
* An array with keys route_name and route_parameters, 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 array
* An array with keys route_name and route_parameters, or NULL if there is
* no route (e.g. when the link is not translatable).
*/
public function getTranslateRoute();
}
<?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\String;
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,
'hidden' => 0,
// 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->factory = new ContainerFactory($this);
$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;
}
/**
* Instantiates if necessary and returns a YamlDiscovery instance.
*
* Since the discovery is very rarely used - only when the rebuild() method
* is called - it's instantiated only when actually needed instead of in the
* constructor.
*
* @return \Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator
* A plugin discovery instance.
*/
protected function getDiscovery() {
if (empty($this->discovery)) {
$yaml = new YamlDiscovery('menu_links', $this->moduleHandler->getModuleDirectories());
$this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml);
}
return $this->discovery;
}
/**
* {@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(String::format('@plugin_id could not be found', array('@plugin_id' => $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->factory->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->isResetable()) {
$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(String::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(String::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->isResetable()) {
throw new PluginException(String::format('Menu link %id is not resettable', array('%id' => $id)));
}
// Get the original data from disk, reset the override and re-save the menu
// 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();
}
}
<?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();
}
<?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, i.e. only links with
* a parent in the list will be included.
* - which menu links are omitted, i.e. 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 only is 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; loads a menu tree with this menu link plugin ID as root.
*
* @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; loads a menu tree from the given level.
*
* @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 minimum depth; loads a menu tree up to the given level.
*
* @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 (only show children).
*
* @param string[] $parents
* An array containing the parent IDs to limit the tree.
*
* @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 hidden links.
*
* @return $this
*/
public function excludeHiddenLinks() {
$this->addCondition('hidden', 0);
return $this;
}
/**
* Ensures only the top level of the tree is loaded.
*
* @return $this
*/
public function topLevelOnly() {
$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;
}
}
This diff is collapsed.
<?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.
*
* @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
* http://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 visible menu links that are below the given ID.
*
* The returned links are not ordered, and visible children will be included
* even if they have a hidden parent 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 visible (not hidden) 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);
}
<?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.
*
* @var string
*/
protected $configName = 'menu_link.static.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)) {
$this->config = $this->configFactory->get($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' => 1,
'parent' => 1,
'weight' => 1,
'expanded' => 1,
'hidden' => 1,
);
// Filter the overrides to only those that are expected.
$definition = array_intersect_key($definition, $expected);
if ($definition) {
$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();
}
return array_keys($definition);
}
/**
* 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('.' => '__', '__' => '___'));
}
}
<?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
* - hidden
* 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
* - hidden
*
* @return array
* A list of properties which got saved.
*/
public function saveOverride($id, array $definition);
}
......@@ -21,7 +21,7 @@ class CachedDiscoveryClearer {
*
* @var \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface[]
*/
protected $cachedDiscoveries;
protected $cachedDiscoveries = array();
/**
* Adds a plugin manager to the active list.
......
......@@ -23,7 +23,9 @@ public function process(ContainerBuilder $container) {
$cache_clearer_definition = $container->getDefinition('plugin.cache_clearer');
foreach ($container->getDefinitions() as $service_id => $definition) {
if (strpos($service_id, 'plugin.manager.') === 0 || $definition->hasTag('plugin_manager_cache_clear')) {
$cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id)));
if (is_subclass_of($definition->getClass(), '\Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface')) {
$cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id)));
}
}
}
}
......
name: 'Custom Menu Links'
type: module
description: 'Allows administrators to create custom menu links.'
package: Core
version: VERSION
core: 8.x
<?php
/**
* @file
* Allows administrators to create custom menu links.
*/
use Drupal\system\MenuInterface;
/**
* Implements hook_menu_delete().
*/
function menu_link_content_menu_delete(MenuInterface $menu) {
$storage = \Drupal::entityManager()->getStorage('menu_link_content');
$menu_links = $storage->loadByProperties(array('menu_name' => $menu->id()));
$storage->delete($menu_links);
}
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Entity\MenuLinkContent.
*/
namespace Drupal\menu_link_content\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
use Drupal\Core\Url;
/**
* Defines the menu link content entity class.
*
* @ContentEntityType(
* id = "menu_link_content",
* label = @Translation("Custom menu link"),
* controllers = {
* "storage" = "Drupal\Core\Entity\ContentEntityDatabaseStorage",
* "access" = "Drupal\menu_link_content\MenuLinkContentAccessController",
* "form" = {
* "default" = "Drupal\menu_link_content\Form\MenuLinkContentForm",
* "delete" = "Drupal\menu_link_content\Form\MenuLinkContentDeleteForm"
* }
* },
* admin_permission = "administer menu",
* base_table = "menu_link_content",
* data_table = "menu_link_content_data",
* fieldable = TRUE,
* translatable = TRUE,
* entity_keys = {
* "id" = "id",
* "label" = "title",
* "uuid" = "uuid",
* "bundle" = "bundle"
* },
* links = {
* "canonical" = "menu_link_content.link_edit",
* "edit-form" = "menu_link_content.link_edit",
* }
* )
*/
class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterface {
/**
* A flag for whether this entity is wrapped in a plugin instance.
*
* @var bool
*/
protected $insidePlugin = FALSE;
/**
* {@inheritdoc}
*/
public function setInsidePlugin() {
$this->insidePlugin = TRUE;
}
/**
* {@inheritdoc}
*/
public function getTitle() {
return $this->get('title')->value;
}
/**
* {@inheritdoc}
*/
public function getRouteName() {
return $this->get('route_name')->value;
}
/**
* {@inheritdoc}
*/
public function getRouteParameters() {
return $this->get('route_parameters')->first()->getValue();
}
/**
* {@inheritdoc}
*/
public function setRouteParameters(array $route_parameters) {
$this->set('route_parameters', array($route_parameters));
return $this;
}
/**
* {@inheritdoc}
*/
public function getUrl() {
return $this->get('url')->value ?: NULL;
}
/**
* {@inheritdoc}
*/
public function getUrlObject() {
if ($route_name = $this->getRouteName()) {
$url = new Url($route_name, $this->getRouteParameters(), $this->getOptions());
}
else {
$path = $this->getUrl();
if (isset($path)) {
$url = Url::createFromPath($path);
}
else {
$url = new Url('<front>');
}
}
return $url;
}
/**
* {@inheritdoc}
*/
public function getMenuName() {
return $this->get('menu_name')->value;
}
/**
* {@inheritdoc}
*/
public function getOptions() {
return $this->get('options')->first()->getValue();
}
/**
* {@inheritdoc}
*/
public function setOptions(array $options) {
$this->set('options', array($options));
return $this;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->get('description')->value;
}
/**
* {@inheritdoc}
*/
public function getPluginId() {
return 'menu_link_content:' . $this->uuid();
}
/**
* {@inheritdoc}
*/
public function isHidden() {
return (bool) $this->get('hidden')->value;
}
/**
* {@inheritdoc}
*/
public function isExpanded() {
return (bool) $this->get('expanded')->value;
}
/**
* {@inheritdoc}
*/
public function getParentId() {
return $this->get('parent')->value;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return (int) $this->get('weight')->value;
}
/**
* Builds up the menu link plugin definition for this entity.
*
* @return array
* The plugin definition corresponding to this entity.
*
* @see \Drupal\Core\Menu\MenuLinkTree::$defaults
*/
protected function getPluginDefinition() {
$definition = array();
$definition['class'] = 'Drupal\menu_link_content\Plugin\Menu\MenuLinkContent';
$definition['menu_name'] = $this->getMenuName();
$definition['route_name'] = $this->getRouteName();
$definition['route_parameters'] = $this->getRouteParameters();
$definition['url'] = $this->getUrl();
$definition['options'] = $this->getOptions();
$definition['title'] = $this->getTitle();
$definition['description'] = $this->getDescription();
$definition['weight'] = $this->getWeight();
$definition['id'] = $this->getPluginId();
$definition['metadata'] = array('entity_id' => $this->id());
$definition['form_class'] = '\Drupal\menu_link_content\Form\MenuLinkContentForm';
$definition['hidden'] = $this->isHidden() ? 1 : 0;
$definition['expanded'] = $this->isExpanded() ? 1 : 0;
$definition['provider'] = 'menu_link_content';
$definition['discovered'] = 0;
$definition['parent'] = $this->getParentId();
return $definition;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
// The menu link can just be updated if there is already an menu link entry
// on both entity and menu link plugin level.
if ($update && $menu_link_manager->getDefinition($this->getPluginId())) {
// When the entity is saved via a plugin instance, we should not call
// the menu tree manager to update the definition a second time.
if (!$this->insidePlugin) {
$menu_link_manager->updateDefinition($this->getPluginId(), $this->getPluginDefinition(), FALSE);
}
}
else {
$menu_link_manager->addDefinition($this->getPluginId(), $this->getPluginDefinition());
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
foreach ($entities as $menu_link) {
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */
$menu_link_manager->removeDefinition($menu_link->getPluginId(), FALSE);
}
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['id'] = FieldDefinition::create('integer')
->setLabel(t('Entity ID'))
->setDescription(t('The entity ID for this menu link content entity.'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$fields['uuid'] = FieldDefinition::create('uuid')
->setLabel(t('UUID'))
->setDescription(t('The content menu link UUID.'))
->setReadOnly(TRUE);
$fields['bundle'] = FieldDefinition::create('string')
->setLabel(t('Bundle'))
->setDescription(t('The content menu link bundle.'))
->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH)
->setReadOnly(TRUE);
$fields['title'] = FieldDefinition::create('string')
->setLabel(t('Menu link title'))
->setDescription(t('The text to be used for this link in the menu.'))
->setRequired(TRUE)
->setTranslatable(TRUE)
->setSettings(array(
'default_value' => '',
'max_length' => 255,
))
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
))
->setDisplayOptions('form', array(
'type' => 'string',
'weight' => -5,
))
->setDisplayConfigurable('form', TRUE);
$fields['description'] = FieldDefinition::create('string')
->setLabel(t('Description'))
->setDescription(t('Shown when hovering over the menu link.'))
->setTranslatable(TRUE)
->setSettings(array(
'default_value' => '',
'max_length' => 255,
))
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'string',
'weight' => 0,
))
->setDisplayOptions('form', array(
'type' => 'string',
'weight' => 0,
));
$fields['menu_name'] = FieldDefinition::create('string')
->setLabel(t('Menu name'))
->setDescription(t('The menu name. All links with the same menu name (such as "tools") are part of the same menu.'))
->setSetting('default_value', 'tools');
// @todo Use a link field https://www.drupal.org/node/2302205.
$fields['route_name'] = FieldDefinition::create('string')
->setLabel(t('Route name'))
->setDescription(t('The machine name of a defined Symfony Route this menu item represents.'));
$fields['route_parameters'] = FieldDefinition::create('map')
->setLabel(t('Route parameters'))
->setDescription(t('A serialized array of route parameters of this menu link.'));
$fields['url'] = FieldDefinition::create('uri')
->setLabel(t('External link url'))
->setDescription(t('The url of the link, in case you have an external link.'));
$fields['options'] = FieldDefinition::create('map')
->setLabel(t('Options'))
->setDescription(t('A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.'))
->setSetting('default_value', array());
$fields['external'] = FieldDefinition::create('boolean')
->setLabel(t('External'))
->setDescription(t('A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).'))
->setSetting('default_value', FALSE);
// The form widget doesn't work yet for core fields, so we skip the
// for display and manually create form elements for the boolean fields.
// @see https://drupal.org/node/2226493
// @see https://drupal.org/node/2150511
$fields['expanded'] = FieldDefinition::create('boolean')
->setLabel(t('Expanded'))
->setDescription(t('Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded).'))
->setSetting('default_value', FALSE)
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'boolean',
'weight' => 0,
));
// We manually create a form element for this, since the form logic is
// is inverted to show enabled.
$fields['hidden'] = FieldDefinition::create('boolean')
->setLabel(t('Hidden'))
->setDescription(t('A flag for whether the link should be hidden in menus or rendered normally.'))
->setSetting('default_value', FALSE);
$fields['weight'] = FieldDefinition::create('integer')
->setLabel(t('Weight'))
->setDescription(t('Link weight among links in the same menu at the same depth.'))
->setSetting('default_value', 0)
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'integer',
'weight' => 0,
))
->setDisplayOptions('form', array(
'type' => 'integer',
'weight' => 0,
));
$fields['langcode'] = FieldDefinition::create('language')
->setLabel(t('Language code'))
->setDescription(t('The node language code.'));
$fields['parent'] = FieldDefinition::create('string')
->setLabel(t('Parent plugin ID'))
->setDescription(t('The ID of the parent menu link plugin, or empty string when at the top level of the hierarchy.'));
return $fields;
}
}
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Entity\MenuLinkContentInterface.
*/
namespace Drupal\menu_link_content\Entity;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Defines an interface for custom menu links.
*/
interface MenuLinkContentInterface extends ContentEntityInterface {
/**
* Flags this instance as being wrapped in a menu link plugin instance.
*/
public function setInsidePlugin();
/**
* Gets the title of the menu link.
*
* @return string
* The title of the link.
*/
public function getTitle();
/**
* Gets the route name of the menu link.
*
* @return string|NULL
* Returns the route name, or NULL if it is an external link.
*/
public function getRouteName();
/**
* Gets the route parameters of the menu link content entity.
*
* @return array
* The route parameters, or an empty array.
*/
public function getRouteParameters();
/**
* Sets the route parameters of the custom menu link.
*
* @param array $route_parameters
* The route parameters, usually derived from the path entered by the
* administrator. For example, for a link to a node with route 'node.view'
* the route needs the node ID as a parameter:
* @code
* array('node' => 2)
* @endcode
*
* @return $this
*/
public function setRouteParameters(array $route_parameters);
/**
* Gets the external URL.
*
* @return string|NULL
* Returns the external URL if the menu link points to an external URL,
* otherwise NULL.
*/
public function getUrl();
/**
* Gets the url object pointing to the URL of the menu link content entity.
*
* @return \Drupal\Core\Url
* A Url object instance.
*/
public function getUrlObject();
/**
* Gets the menu name of the custom menu link.
*
* @return string
* The menu ID.
*/
public function getMenuName();
/**
* Gets the options for the menu link content entity.
*
* @return array
* The options that may be passed to the URL generator.
*/
public function getOptions();
/**
* Sets the query options of the menu link content entity.
*
* @param array $options
* The new option.
*
* @return $this
*/
public function setOptions(array $options);
/**
* Gets the description of the menu link for the UI.
*
* @return string
* The description to use on admin pages or as a title attribute.
*/
public function getDescription();
/**
* Gets the menu plugin ID associated with this entity.
*
* @return string
* The plugin ID.
*/
public function getPluginId();
/**
* Returns whether the menu link is marked as hidden.
*
* @return bool
* TRUE if is not enabled, otherwise FALSE.
*/
public function isHidden();
/**
* Returns whether the menu link is marked as always expanded.
*
* @return bool
* TRUE for expanded, FALSE otherwise.
*/
public function isExpanded();
/**
* Gets the plugin ID of the parent menu link.
*
* @return string
* A plugin ID, or empty string if this link is at the top level.
*/
public function getParentId();
/**
* Returns the weight of the menu link content entity.
*
* @return int
* A weight for use when ordering links.
*/
public function getWeight();
}
<?php
/**
* @file
* Contains \Drupal\menu_link_content\MenuLinkContentAccessController.
*/
namespace Drupal\menu_link_content;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Entity\EntityControllerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessController;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access controller for the user entity type.
*/
class MenuLinkContentAccessController extends EntityAccessController implements EntityControllerInterface {
/**
* The access manager to check routes by name.
*
* @var \Drupal\Core\Access\AccessManager
*/
protected $accessManager;
/**
* Creates a new MenuLinkContentAccessController.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Access\AccessManager $access_manager
* The access manager to check routes by name.
*/
public function __construct(EntityTypeInterface $entity_type, AccessManager $access_manager) {
parent::__construct($entity_type);
$this->accessManager = $access_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static($entity_type, $container->get('access_manager'));
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
switch ($operation) {
case 'view':
// There is no direct view.
return FALSE;
case 'update':
// If there is a URL, this is an external link so always accessible.
return $account->hasPermission('administer menu') && ($entity->getUrl() || $this->accessManager->checkNamedRoute($entity->getRouteName(), $entity->getRouteParameters(), $account));
case 'delete':
return !$entity->isNew() && $account->hasPermission('administer menu');
}
}
}
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent.
*/
namespace Drupal\menu_link_content\Plugin\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\MenuLinkBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the menu link plugin for content menu links.
*/
class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInterface {
/**
* Entities IDs to load.
*
* It is an array of entity IDs keyed by entity IDs.
*
* @var array
*/
protected static $entityIdsToLoad = array();
/**
* {@inheritdoc}
*/
protected $overrideAllowed = array(
'menu_name' => 1,
'parent' => 1,
'weight' => 1,
'expanded' => 1,
'hidden' => 1,
'title' => 1,
'description' => 1,
'route_name' => 1,
'route_parameters' => 1,
'url' => 1,
'options' => 1,
);
/**
* The menu link content entity connected to this plugin instance.
*
* @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface
*/
protected $entity;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new MenuLinkContent.
*
* @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\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
if (!empty($this->pluginDefinition['metadata']['entity_id'])) {
$entity_id = $this->pluginDefinition['metadata']['entity_id'];
// Builds a list of entity IDs to take advantage of the more efficient
// EntityStorageInterface::loadMultiple() in getEntity() at render time.
static::$entityIdsToLoad[$entity_id] = $entity_id;
}
$this->entityManager = $entity_manager;
$this->langaugeManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager'),
$container->get('language_manager')
);
}
/**
* Loads the entity associated with this menu link.
*
* @return \Drupal\menu_link_content\Entity\MenuLinkContentInterface
* The menu link content entity.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the entity ID and UUID are both invalid or missing.
*/
protected function getEntity() {
if (empty($this->entity)) {
$entity = NULL;
$storage = $this->entityManager->getStorage('menu_link_content');
if (!empty($this->pluginDefinition['metadata']['entity_id'])) {
$entity_id = $this->pluginDefinition['metadata']['entity_id'];
// Make sure the current ID is in the list, since each plugin empties
// the list after calling loadMultple(). Note that the list may include
// multiple IDs added earlier in each plugin's constructor.
static::$entityIdsToLoad[$entity_id] = $entity_id;
$entities = $storage->loadMultiple(array_values(static::$entityIdsToLoad));
$entity = isset($entities[$entity_id]) ? $entities[$entity_id] : NULL;
static::$entityIdsToLoad = array();
}
if (!$entity) {
// Fallback to the loading by the UUID.
$uuid = $this->getDerivativeId();
$loaded_entities = $storage->loadByProperties(array('uuid' => $uuid));
$entity = reset($loaded_entities);
}
if (!$entity) {
throw new PluginException(String::format('Entity not found through the menu link plugin definition and could not fallback on UUID @uuid', array('@uuid' => $uuid)));
}
// Clone the entity object to avoid tampering with the static cache.
$this->entity = clone $entity;
$the_entity = $this->entityManager->getTranslationFromContext($this->entity);
/** @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface $the_entity */
$this->entity = $the_entity;
$this->entity->setInsidePlugin();
}
return $this->entity;
}
/**
* {@inheritdoc}
*/
public function getTitle() {
// We only need to get the title from the actual entity if it may be a
// translation based on the current language context. This can only happen
// if the site is configured to be multilingual.
if ($this->langaugeManager->isMultilingual()) {
return $this->getEntity()->getTitle();
}
return $this->pluginDefinition['title'];
}
/**
* {@inheritdoc}
*/
public function getDescription() {
// We only need to get the description from the actual entity if it may be a
// translation based on the current language context. This can only happen
// if the site is configured to be multilingual.
if ($this->langaugeManager->isMultilingual()) {
return $this->getEntity()->getDescription();
}
return $this->pluginDefinition['description'];
}
/**
* {@inheritdoc}
*/
public function getDeleteRoute() {
return array(
'route_name' => 'menu_link_content.link_delete',
'route_parameters' => array('menu_link_content' => $this->getEntity()->id()),
);
}
/**
* {@inheritdoc}
*/
public function getEditRoute() {
return array(
'route_name' => 'menu_link_content.link_edit',
'route_parameters' => array('menu_link_content' => $this->getEntity()->id()),
);
}
/**
* {@inheritdoc}
*/
public function getTranslateRoute() {
$entity_type = 'menu_link_content';
return array(
'route_name' => 'content_translation.translation_overview_' . $entity_type,
'route_parameters' => array( $entity_type => $this->getEntity()->id()),
);
}
/**
* {@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) {
$entity = $this->getEntity();
foreach ($overrides as $key => $value) {
$entity->{$key}->value = $value;
}
$this->entityManager->getStorage('menu_link_content')->save($entity);
}
return $this->pluginDefinition;
}
/**
* {@inheritdoc}
*/
public function isDeletable() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
return $this->getEntity()->isTranslatable();
}
/**
* {@inheritdoc}
*/
public function deleteLink() {
$this->getEntity()->delete();
}
}
<?php
/**
* @file
* Contains \Drupal\system\Tests\Menu\MenuTreeStorageTest.
*/
namespace Drupal\system\Tests\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Menu\MenuTreeStorage;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the menu tree storage.
*
* @group Menu
*
* @see \Drupal\Core\Menu\MenuTreeStorage
*/
class MenuTreeStorageTest extends KernelTestBase {
/**
* The tested tree storage.
*
* @var \Drupal\Core\Menu\MenuTreeStorage
*/
protected $treeStorage;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'menu_link_content');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->treeStorage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), 'menu_tree');
$this->connection = $this->container->get('database');
$this->installEntitySchema('menu_link_content');
}
/**
* Tests the tree storage when no tree was built yet.
*/
public function testBasicMethods() {
$this->doTestEmptyStorage();
$this->doTestTable();
}
/**
* Ensures that there are no menu links by default.
*/
protected function doTestEmptyStorage() {
$this->assertEqual(0, $this->treeStorage->countMenuLinks());
}
/**
* Ensures that table gets created on the fly.
*/
protected function doTestTable() {
// Test that we can create a tree storage with an arbitrary table name and
// that selecting from the storage creates the table.
$tree_storage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), 'test_menu_tree');
$this->assertFalse($this->connection->schema()->tableExists('test_menu_tree'), 'Test table is not yet created');
$tree_storage->countMenuLinks();
$this->assertTrue($this->connection->schema()->tableExists('test_menu_tree'), 'Test table was created');
}
/**
* Tests with a simple linear hierarchy.
*/
public function testSimpleHierarchy() {
// Add some links with parent on the previous one and test some values.
// <tools>
// - test1
// -- test2
// --- test3
$this->addMenuLink('test1', '');
$this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
$this->addMenuLink('test2', 'test1');
$this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2'));
$this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2), array('test1'));
$this->addMenuLink('test3', 'test2');
$this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3'));
$this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3'));
$this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 3), array('test2', 'test1'));
}
/**
* Tests the tree with moving links inside the hierarchy.
*/
public function testMenuLinkMoving() {
// Before the move.
// <tools>
// - test1
// -- test2
// --- test3
// - test4
// -- test5
// --- test6
$this->addMenuLink('test1', '');
$this->addMenuLink('test2', 'test1');
$this->addMenuLink('test3', 'test2');
$this->addMenuLink('test4', '');
$this->addMenuLink('test5', 'test4');
$this->addMenuLink('test6', 'test5');
$this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3'));
$this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3'));
$this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test6'));
$this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test6'));
$this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4'));
$this->moveMenuLink('test2', 'test5');
// After the 1st move.
// <tools>
// - test1
// - test4
// -- test5
// --- test2
// ---- test3
// --- test6
$this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
$this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 3), array('test5', 'test4'), array('test3'));
$this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 4), array('test2', 'test5', 'test4'));
$this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6'));
$this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test2', 'test3', 'test6'));
$this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4'));
$this->moveMenuLink('test4', 'test1');
$this->moveMenuLink('test3', 'test1');
// After the next 2 moves.
// <tools>
// - test1
// -- test3
// -- test4
// --- test5
// ---- test2
// ---- test6
$this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test4', 'test5', 'test2', 'test3', 'test6'));
$this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1'));
$this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1'));
$this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test5', 'test6'));
$this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 3), array('test4', 'test1'), array('test2', 'test6'));
$this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1'));
// Deleting a link in the middle should re-attach child links to the parent.
$this->treeStorage->delete('test4');
// After the delete.
// <tools>
// - test1
// -- test3
// -- test5
// --- test2
// --- test6
$this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6'));
$this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 3), array('test5', 'test1'));
$this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1'));
$this->assertFalse($this->treeStorage->load('test4'));
$this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test6'));
$this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test1'));
}
/**
* Tests with hidden child links.
*/
public function testMenuHiddenChildLinks() {
// Add some links with parent on the previous one and test some values.
// <tools>
// - test1
// -- test2 (hidden)
$this->addMenuLink('test1', '');
$this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
$this->addMenuLink('test2', 'test1', '<front>', array(), 'tools', array('hidden' => 1));
// The 1st link does not have any visible children, so has_children is 0.
$this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
$this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2, 'hidden' => 1), array('test1'));
// Add more links with parent on the previous one.
// <footer>
// - footerA
// ===============
// <tools>
// - test1
// -- test2 (hidden)
// --- test3
// ---- test4
// ----- test5
// ------ test6
// ------- test7
// -------- test8
// --------- test9
$this->addMenuLink('footerA', '', '<front>', array(), 'footer');
$visible_children = array();
for ($i = 3; $i <= $this->treeStorage->maxDepth(); $i++) {
$parent = $i - 1;
$this->addMenuLink("test$i", "test$parent");
$visible_children[] = "test$i";
}
// The 1st link does not have any visible children, so has_children is still
// 0. However, it has visible links below it that will be found.
$this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1), array(), $visible_children);
// This should fail since test9 would end up at greater than max depth.
try {
$this->moveMenuLink('test1', 'footerA');
$this->fail('Exception was not thrown');
}
catch (PluginException $e) {
$this->pass($e->getMessage());
}
// The opposite move should work, and change the has_children flag.
$this->moveMenuLink('footerA', 'test1');
$visible_children[] = 'footerA';
$this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), $visible_children);
}
/**
* Tests the loadTreeData method.
*/
public function testLoadTree() {
$this->addMenuLink('test1', '');
$this->addMenuLink('test2', 'test1');
$this->addMenuLink('test3', 'test2');
$this->addMenuLink('test4');
$this->addMenuLink('test5', 'test4');
$data = $this->treeStorage->loadTreeData('tools', new MenuTreeParameters());
$tree = $data['tree'];
$this->assertEqual(count($tree['test1']['subtree']), 1);
$this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']), 1);
$this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']['test3']['subtree']), 0);
$this->assertEqual(count($tree['test4']['subtree']), 1);
$this->assertEqual(count($tree['test4']['subtree']['test5']['subtree']), 0);
$parameters = new MenuTreeParameters();
$parameters->setActiveTrail(array('test4', 'test5'));
$data = $this->treeStorage->loadTreeData('tools', $parameters);
$tree = $data['tree'];
$this->assertEqual(count($tree['test1']['subtree']), 1);
$this->assertFalse($tree['test1']['in_active_trail']);
$this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']), 1);
$this->assertFalse($tree['test1']['subtree']['test2']['in_active_trail']);
$this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']['test3']['subtree']), 0);
$this->assertFalse($tree['test1']['subtree']['test2']['subtree']['test3']['in_active_trail']);
$this->assertEqual(count($tree['test4']['subtree']), 1);
$this->assertTrue($tree['test4']['in_active_trail']);
$this->assertEqual(count($tree['test4']['subtree']['test5']['subtree']), 0);
$this->assertTrue($tree['test4']['subtree']['test5']['in_active_trail']);
}
/**
* Tests finding the subtree height with content menu links.
*/
public function testSubtreeHeight() {
$storage = \Drupal::entityManager()->getStorage('menu_link_content');
// root
// - child1
// -- child2
// --- child3
// ---- child4
$root = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'));
$root->save();
$child1 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $root->getPluginId()));
$child1->save();
$child2 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child1->getPluginId()));
$child2->save();
$child3 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child2->getPluginId()));
$child3->save();
$child4 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child3->getPluginId()));
$child4->save();
$this->assertEqual($this->treeStorage->getSubtreeHeight($root->getPluginId()), 5);
$this->assertEqual($this->treeStorage->getSubtreeHeight($child1->getPluginId()), 4);
$this->assertEqual($this->treeStorage->getSubtreeHeight($child2->getPluginId()), 3);
$this->assertEqual($this->treeStorage->getSubtreeHeight($child3->getPluginId()), 2);
$this->assertEqual($this->treeStorage->getSubtreeHeight($child4->getPluginId()), 1);
}
/**
* Adds a link with the given ID and supply defaults.
*/
protected function addMenuLink($id, $parent = '', $route_name = 'test', $route_parameters = array(), $menu_name = 'tools', $extra = array()) {
$link = array(
'id' => $id,
'menu_name' => $menu_name,
'route_name' => $route_name,
'route_parameters' => $route_parameters,
'title_arguments' => array(),
'title' => 'test',
'parent' => $parent,
'options' => array(),
'metadata' => array(),
) + $extra;
$this->treeStorage->save($link);
}
/**
* Moves the link with the given ID so it's under a new parent.
*
* @param string $id
* The ID of the menu link to move.
* @param string $new_parent
* The ID of the new parent link.
*/
protected function moveMenuLink($id, $new_parent) {
$menu_link = $this->treeStorage->load($id);
$menu_link['parent'] = $new_parent;
$this->treeStorage->save($menu_link);
}
/**
* Tests that a link's stored representation matches the expected values.
*
* @param string $id
* The ID of the menu link to test
* @param array $expected_properties
* A keyed array of column names and values like has_children and depth.
* @param array $parents
* An ordered array of the IDs of the menu links that are the parents.
* @param array $children
* Array of child IDs that are visible (hidden == 0).
*/
protected function assertMenuLink($id, array $expected_properties, array $parents = array(), array $children = array()) {
$query = $this->connection->select('menu_tree');
$query->fields('menu_tree');
$query->condition('id', $id);
foreach ($expected_properties as $field => $value) {
$query->condition($field, $value);
}
$all = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
$this->assertEqual(count($all), 1, "Found link $id matching all the expected properties");
$raw = reset($all);
// Put the current link onto the front.
array_unshift($parents, $raw['id']);
$query = $this->connection->select('menu_tree');
$query->fields('menu_tree', array('id', 'mlid'));
$query->condition('id', $parents, 'IN');
$found_parents = $query->execute()->fetchAllKeyed(0, 1);
$this->assertEqual(count($parents), count($found_parents), 'Found expected number of parents');
$this->assertEqual($raw['depth'], count($found_parents), 'Number of parents is the same as the depth');
$materialized_path = $this->treeStorage->getRootPathIds($id);
$this->assertEqual(array_values($materialized_path), array_values($parents), 'Parents match the materialized path');
// Check that the selected mlid values of the parents are in the correct
// column, including the link's own.
for ($i = $raw['depth']; $i >= 1; $i--) {
$parent_id = array_shift($parents);
$this->assertEqual($raw["p$i"], $found_parents[$parent_id], "mlid of parent matches at column p$i");
}
for ($i = $raw['depth'] + 1; $i <= $this->treeStorage->maxDepth(); $i++) {
$this->assertEqual($raw["p$i"], 0, "parent is 0 at column p$i greater than depth");
}
if ($parents) {
$this->assertEqual($raw['parent'], end($parents), 'Ensure that the parent field is set properly');
}
$found_children = array_keys($this->treeStorage->loadAllChildren($id));
// We need both these checks since the 2nd will pass if there are extra
// IDs loaded in $found_children.
$this->assertEqual(count($children), count($found_children), "Found expected number of children for $id");
$this->assertEqual(array_intersect($children, $found_children), $children, 'Child IDs match');
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment