diff --git a/core/lib/Drupal/Core/Menu/MenuLinkBase.php b/core/lib/Drupal/Core/Menu/MenuLinkBase.php index d376830..438d862 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkBase.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkBase.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Menu; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Component\Utility\String; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Url; @@ -26,10 +27,7 @@ protected $overrideAllowed = array(); /** - * Returns the weight of the menu link. - * - * @return int - * The weight of the menu link, 0 by default. + * {@inheritdoc} */ public function getWeight() { // By default the weight is 0. @@ -193,7 +191,7 @@ public function getTranslateRoute() { * {@inheritdoc} */ public function deleteLink() { - throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $this->getPluginId())); + throw new PluginException(String::format('Menu link plugin with ID @id does not support deletion', array('@id' => $this->getPluginId()))); } } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkDefault.php b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php index 24cf8d2..0439937 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkDefault.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php @@ -75,6 +75,7 @@ public function isResetable() { * {@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); diff --git a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php index a269495..6ab1434 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php @@ -192,13 +192,15 @@ public function deleteLink(); * 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 parameters for a delete link. + * 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 @@ -207,7 +209,7 @@ public function getFormClass(); public function getDeleteRoute(); /** - * Returns parameters for a custom edit link. + * 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 @@ -220,7 +222,7 @@ public function getDeleteRoute(); public function getEditRoute(); /** - * Returns parameters for a translate link. + * 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 diff --git a/core/lib/Drupal/Core/Menu/MenuLinkManager.php b/core/lib/Drupal/Core/Menu/MenuLinkManager.php index 0b27be2..73ec36f 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkManager.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkManager.php @@ -41,9 +41,13 @@ class MenuLinkManager implements MenuLinkManagerInterface { '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. + // 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' => '', @@ -61,7 +65,7 @@ class MenuLinkManager implements MenuLinkManagerInterface { // 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. + // The plugin ID. Set by the plugin system based on the top-level YAML key. 'id' => '', ); @@ -87,7 +91,7 @@ class MenuLinkManager implements MenuLinkManagerInterface { protected $treeStorage; /** - * Service providing overrides for static links + * Service providing overrides for static links. * * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface */ @@ -102,12 +106,12 @@ class MenuLinkManager implements MenuLinkManagerInterface { /** - * Constructs a \Drupal\Core\Menu\MenuLinkTree object. + * 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 - * Service providing overrides for static links + * The service providing overrides for static links. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ @@ -122,8 +126,8 @@ public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLi * Performs extra processing on plugin definitions. * * By default we add defaults for the type to the definition. If a type has - * additional processing logic they can do that by replacing or extending the - * method. + * 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. @@ -132,6 +136,8 @@ public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLi */ 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; } @@ -170,6 +176,8 @@ public function getDefinitions() { // 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]); @@ -199,7 +207,7 @@ public function rebuild() { 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 could not be found."); + throw new PluginNotFoundException(String::format('@plugin_id could not be found', array('@plugin_id' => $plugin_id))); } return $definition; } @@ -212,7 +220,7 @@ public function hasDefinition($plugin_id) { } /** - * Returns a pre-configured meu link plugin instance. + * Returns a pre-configured menu link plugin instance. * * @param string $plugin_id * The ID of the plugin being instantiated. @@ -273,7 +281,7 @@ protected function deleteInstance(MenuLinkInterface $instance, $persist) { } } else { - throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $id)); + throw new PluginException(String::format('Menu link plugin with ID @id does not support deletion', array('@id' => $id))); } $this->treeStorage->delete($id); } @@ -341,7 +349,7 @@ public function loadLinksByRoute($route_name, array $route_parameters = array(), */ public function addDefinition($id, array $definition) { if ($this->treeStorage->load($id) || $id === '') { - throw new PluginException(sprintf('The ID %s already exists as a plugin definition or is not valid', $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); diff --git a/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php index f6757a7..42d4d48 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php @@ -10,12 +10,25 @@ use Drupal\Component\Plugin\PluginManagerInterface; /** - * Defines an interface for creating menu links and storing their definitions. + * 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 static links. + * Triggers discovery, save, and cleanup of discovered links. */ public function rebuild(); @@ -88,6 +101,10 @@ 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 @@ -95,8 +112,8 @@ public function addDefinition($id, array $definition); * 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. + * 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. diff --git a/core/lib/Drupal/Core/Menu/MenuTreeParameters.php b/core/lib/Drupal/Core/Menu/MenuTreeParameters.php index 277b43c..23a0992 100644 --- a/core/lib/Drupal/Core/Menu/MenuTreeParameters.php +++ b/core/lib/Drupal/Core/Menu/MenuTreeParameters.php @@ -15,7 +15,7 @@ * 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 + * - 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. @@ -35,7 +35,7 @@ class MenuTreeParameters { public $root = ''; /** - * The minimum depth of menu links in the resulting tree. Root-relative. + * 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). @@ -45,38 +45,38 @@ class MenuTreeParameters { public $minDepth = NULL; /** - * The maximum depth of menu links in the resulting tree. Root-relative. + * 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. + * 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(); /** - * 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. + * The IDs from the currently active menu link to the root of the whole tree. * - * Defaults to the empty array. + * 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(); /** - * An associative array of custom query condition key/value pairs to restrict - * the links loaded. + * The conditions used to restrict which links are loaded. * - * Defaults to the empty array. + * An associative array of custom query condition key/value pairs. * * @var array */ @@ -166,7 +166,7 @@ public function setActiveTrail(array $active_trail) { * 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 + * @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. diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php index 0382f0e..6741d35 100644 --- a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Menu; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Component\Utility\String; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; @@ -17,7 +18,7 @@ use Drupal\Core\Database\SchemaObjectExistsException; /** - * Provides a tree storage using the database. + * Provides a menu tree storage using the database. */ class MenuTreeStorage implements MenuTreeStorageInterface { @@ -56,6 +57,10 @@ class MenuTreeStorage implements MenuTreeStorageInterface { /** * Stores definitions that have already been loaded for better performance. + * + * An array of plugin definition arrays, keyed by plugin ID. + * + * @var array */ protected $definitions = array(); @@ -72,6 +77,8 @@ class MenuTreeStorage implements MenuTreeStorageInterface { * @todo Decide how to keep these field definitions in sync. * https://www.drupal.org/node/2302085 * + * @see \Drupal\Core\Menu\MenuLinkManager::$defaults + * * @var array */ protected $definitionFields = array( @@ -170,6 +177,8 @@ public function rebuild(array $definitions) { $query->addField($this->table, 'id'); $query->condition('discovered', 1); $query->condition('id', array_keys($definitions), 'NOT IN'); + // Starting from links with the greatest depth will minimize the amount + // of re-parenting done by the menu storage. $query->orderBy('depth', 'DESC'); $result = $query->execute()->fetchCol(); } @@ -177,8 +186,7 @@ public function rebuild(array $definitions) { $result = array(); } - // Remove all such items. Starting from those with the greatest depth will - // minimize the amount of re-parenting done by the menu link controller. + // Remove all such items. if ($result) { $this->purgeMultiple($result); } @@ -259,7 +267,9 @@ public function save(array $link) { * \Drupal\Core\Menu\MenuLinkInterface plugin. * * @return array - * The menu names affected by the save operation (1 or 2 names). + * 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. @@ -352,6 +362,7 @@ protected function preSave(array &$link, array $original) { } } $fields = array_intersect_key($link, $schema_fields); + // Sort the route parameters so that the query string will be the same. asort($fields['route_parameters']); // Since this will be urlencoded, it's safe to store and match against a // text field. @@ -384,7 +395,7 @@ protected function preSave(array &$link, array $original) { $limit = $this->maxDepth() - 1; } if ($parent['depth'] > $limit) { - throw new PluginException(sprintf('The link with ID %s or its children exceeded the maximum depth of %d', $link['id'], $this->maxDepth())); + throw new PluginException(String::format('The link with ID @id or its children exceeded the maximum depth of @depth', array('@id' => $link['id'], '@depth' => $this->maxDepth()))); } $this->setParents($fields, $parent); } @@ -397,7 +408,7 @@ protected function preSave(array &$link, array $original) { // We needed the mlid above, but not in the update query. unset($fields['mlid']); - // Cast booleans to int, if needed. + // Cast Booleans to int, if needed. $fields['hidden'] = (int) $fields['hidden']; $fields['expanded'] = (int) $fields['expanded']; return $fields; @@ -487,7 +498,7 @@ protected function setParents(array &$fields, array $parent) { } /** - * Moves the link's children using the query fields value and original values. + * Re-parents a link's children when the link itself is moved. * * @param array $fields * The changed menu link. @@ -642,6 +653,7 @@ public function loadByProperties(array $properties) { * {@inheritdoc} */ public function loadByRoute($route_name, array $route_parameters = array(), $menu_name = NULL) { + // Sort the route parameters so that the query string will be the same. asort($route_parameters); // Since this will be urlencoded, it's safe to store and match against a // text field. @@ -670,14 +682,18 @@ public function loadByRoute($route_name, array $route_parameters = array(), $men * {@inheritdoc} */ public function loadMultiple(array $ids) { - $query = $this->connection->select($this->table, $this->options); - $query->fields($this->table, $this->definitionFields()); - $query->condition('id', $ids, 'IN'); - $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); - foreach ($loaded as $id => $link) { - $loaded[$id] = $this->prepareLink($link); + $missing_ids = array_diff($ids, array_keys($this->definitions)); + + if ($missing_ids) { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, $this->definitionFields()); + $query->condition('id', $missing_ids, 'IN'); + $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + foreach ($loaded as $id => $link) { + $this->definitions[$id] = $this->prepareLink($link); + } } - return $loaded; + return array_intersect_key($this->definitions, array_flip($ids)); } /** @@ -706,7 +722,7 @@ protected function loadFull($id) { } /** - * Loads multiple menu link definitions by ID. + * Loads all table fields for multiple menu link definitions by ID. * * @param array $ids * The IDs to load. @@ -802,7 +818,7 @@ protected function saveRecursive($id, &$children, &$links) { * {@inheritdoc} */ public function loadTreeData($menu_name, MenuTreeParameters $parameters) { - // Build the cache id; sort 'expanded' and 'conditions' to prevent duplicate + // Build the cache ID; sort 'expanded' and 'conditions' to prevent duplicate // cache items. sort($parameters->expandedParents); sort($parameters->conditions); @@ -858,7 +874,7 @@ protected function loadLinks($menu_name, MenuTreeParameters $parameters) { } // When specifying a custom root, we only want to find links whose - // parent IDs match that of the root; that's how ignore the rest of the + // parent IDs match that of the root; that's how we ignore the rest of the // tree. In other words: we exclude everything unreachable from the // custom root. for ($i = 1; $i <= $root['depth']; $i++) { @@ -1145,7 +1161,6 @@ protected function ensureTableExists() { * A list of fields that are serialized in the database. */ protected function serializedFields() { - // For now, build the list from the schema since it's in active development. if (empty($this->serializedFields)) { $schema = static::schemaDefinition(); foreach ($schema['fields'] as $name => $field) { diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php index 0deff75..a5d512d 100644 --- a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php @@ -8,7 +8,10 @@ namespace Drupal\Core\Menu; /** - * Defines an interface for the menu tree storage. + * 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 { @@ -56,7 +59,9 @@ public function load($id); * An array of plugin IDs. * * @return array - * An array of plugin definition arrays. + * 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); @@ -93,7 +98,9 @@ public function loadByRoute($route_name, array $route_parameters = array(), $men * A definition for a \Drupal\Core\Menu\MenuLinkInterface plugin. * * @return array - * The menu names affected by the save operation (1 or 2 names). + * 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. @@ -132,7 +139,8 @@ public function delete($id); * * @return array * An array with 2 elements: - * - tree: A fully built menu tree. + * - 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); @@ -191,9 +199,23 @@ public function loadSubtreeData($id, $max_relative_depth = NULL); * * @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 + * 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. + * 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); @@ -211,10 +233,10 @@ public function getRootPathIds($id); public function getExpanded($menu_name, array $parents); /** - * Finds the height of a subtree rooted by of the given ID. + * Finds the height of a subtree rooted by the given ID. * * @param string $id - * The the ID of an item in the storage. + * 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 @@ -223,7 +245,7 @@ public function getExpanded($menu_name, array $parents); public function getSubtreeHeight($id); /** - * Determines whether a specific menu named is used in the tree. + * Determines whether a specific menu name is used in the tree. * * @param string $menu_name * The menu name. diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php index 991d277..8b79bff 100644 --- a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php +++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php @@ -123,7 +123,7 @@ public function loadMultipleOverrides(array $ids) { * {@inheritdoc} */ public function saveOverride($id, array $definition) { - // Remove unexpected keys. + // Only allow to override a specific subset of the keys. $expected = array( 'menu_name' => 1, 'parent' => 1, @@ -131,6 +131,7 @@ public function saveOverride($id, array $definition) { '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); diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php index 3d7b4d5..0ffb21a 100644 --- a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php +++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php @@ -13,6 +13,8 @@ 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. */ diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php index ae84e07..c5f28db 100644 --- a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php +++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php @@ -18,7 +18,7 @@ * * @ContentEntityType( * id = "menu_link_content", - * label = @Translation("Menu link content"), + * label = @Translation("Custom menu link"), * controllers = { * "storage" = "Drupal\Core\Entity\ContentEntityDatabaseStorage", * "access" = "Drupal\menu_link_content\MenuLinkContentAccessController", @@ -196,8 +196,6 @@ protected function getPluginDefinition() { $definition['route_parameters'] = $this->getRouteParameters(); $definition['url'] = $this->getUrl(); $definition['options'] = $this->getOptions(); - // Don't bother saving title and description strings, since they are never - // used. $definition['title'] = $this->getTitle(); $definition['description'] = $this->getDescription(); $definition['weight'] = $this->getWeight(); @@ -356,7 +354,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // is inverted to show enabled. $fields['hidden'] = FieldDefinition::create('boolean') ->setLabel(t('Hidden')) - ->setDescription(t('A flag for whether the link should be rendered in menus. (1 = a disabled menu item that may be shown on admin screens, -1 = a menu callback, 0 = a normal, visible link).')) + ->setDescription(t('A flag for whether the link should be hidden in menus or rendered normally.')) ->setSetting('default_value', 0); $fields['weight'] = FieldDefinition::create('integer') diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessController.php b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php index 718e151..c99109e 100644 --- a/core/modules/menu_link_content/src/MenuLinkContentAccessController.php +++ b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php @@ -6,14 +6,18 @@ 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 { +class MenuLinkContentAccessController extends EntityAccessController implements EntityControllerInterface { /** * The access manager to check routes by name. @@ -23,6 +27,27 @@ class MenuLinkContentAccessController extends EntityAccessController { 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) { @@ -33,23 +58,11 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A 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)); + return $account->hasPermission('administer menu') && ($entity->getUrl() || $this->accessManager->checkNamedRoute($entity->getRouteName(), $entity->getRouteParameters(), $account)); case 'delete': return !$entity->isNew() && $account->hasPermission('administer menu'); } } - /** - * Returns the access manager. - * - * @return \Drupal\Core\Access\AccessManager - * The access manager. - */ - protected function accessManager() { - if (!$this->accessManager) { - $this->accessManager = \Drupal::service('access_manager'); - } - return $this->accessManager; - } } diff --git a/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php index 5781044..70fbeae 100644 --- a/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php +++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php @@ -8,6 +8,7 @@ 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; @@ -114,7 +115,7 @@ public static function create(ContainerInterface $container, array $configuratio * The menu link content entity. * * @throws \Drupal\Component\Plugin\Exception\PluginException - * If the entity ID and uuid are both invalid or missing. + * If the entity ID and UUID are both invalid or missing. */ protected function getEntity() { if (empty($this->entity)) { @@ -129,14 +130,14 @@ protected function getEntity() { $entity = isset($entities[$entity_id]) ? $entities[$entity_id] : NULL; static::$entityIdsToLoad = array(); } - else { - // Fallback to the loading by the uuid. + if(!$entity) { + // Fallback to the loading by the UUID. $uuid = $this->getDerivativeId(); $links = $storage->loadByProperties(array('uuid' => $uuid)); $entity = reset($links); } if (!$entity) { - throw new PluginException("Invalid entity ID or uuid"); + 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; @@ -209,6 +210,7 @@ public function getTranslateRoute() { * {@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(); diff --git a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php index 8064984..75b4f1e 100644 --- a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php +++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php @@ -15,6 +15,8 @@ /** * Tests the menu tree storage. * + * @group Menu + * * @see \Drupal\Core\Menu\MenuTreeStorage */ class MenuTreeStorageTest extends KernelTestBase { @@ -43,17 +45,6 @@ class MenuTreeStorageTest extends KernelTestBase { /** * {@inheritdoc} */ - public static function getInfo() { - return array( - 'name' => 'Menu tree storage tests', - 'description' => 'Tests menu tree storage tests', - 'group' => 'Menu', - ); - } - - /** - * {@inheritdoc} - */ protected function setUp() { parent::setUp(); diff --git a/core/tests/Drupal/Tests/Core/Menu/StaticMenuLinkOverridesTest.php b/core/tests/Drupal/Tests/Core/Menu/StaticMenuLinkOverridesTest.php new file mode 100644 index 0000000..63f1875 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Menu/StaticMenuLinkOverridesTest.php @@ -0,0 +1,220 @@ +getConfigFactoryStub(array('menu_link.static.overrides' => array())); + $static_override = new StaticMenuLinkOverrides($config_factory); + + $this->assertAttributeEquals($config_factory, 'configFactory', $static_override); + } + + /** + * Tests the reload method. + * + * @covers ::reload + */ + public function testReload() { + $config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface'); + $config_factory->expects($this->at(0)) + ->method('reset') + ->with('menu_link.static.overrides'); + + $static_override = new StaticMenuLinkOverrides($config_factory); + + $static_override->reload(); + } + + /** + * Tests the loadOverride method. + * + * @dataProvider providerTestLoadOverride + * + * @covers ::loadOverride + * @covers ::getConfig + */ + public function testLoadOverride($overrides, $id, $expected) { + $config_factory = $this->getConfigFactoryStub(array('menu_link.static.overrides' => array('definitions' => $overrides))); + $static_override = new StaticMenuLinkOverrides($config_factory); + + $this->assertEquals($expected, $static_override->loadOverride($id)); + } + + /** + * Provides test data for testLoadOverride. + */ + public function providerTestLoadOverride() { + $data = array(); + // No specified ID. + $data[] = array(array('test1' => array('parent' => 'test0')), NULL, array()); + // Valid ID. + $data[] = array(array('test1' => array('parent' => 'test0')), 'test1', array('parent' => 'test0')); + // Non existing ID. + $data[] = array(array('test1' => array('parent' => 'test0')), 'test2', array()); + // Ensure that the ID is encoded properly + $data[] = array(array('test1__la___ma' => array('parent' => 'test0')), 'test1.la__ma', array('parent' => 'test0')); + + return $data; + } + + /** + * Tests the loadMultipleOverrides method. + * + * @covers ::loadMultipleOverrides + * @covers ::getConfig + */ + public function testLoadMultipleOverrides() { + $overrides = array(); + $overrides['test1'] = array('parent' => 'test0'); + $overrides['test2'] = array('parent' => 'test1'); + $overrides['test1__la___ma'] = array('parent' => 'test2'); + + $config_factory = $this->getConfigFactoryStub(array('menu_link.static.overrides' => array('definitions' => $overrides))); + $static_override = new StaticMenuLinkOverrides($config_factory); + + $this->assertEquals(array('test1' => array('parent' => 'test0'), 'test1.la__ma' => array('parent' => 'test2')), $static_override->loadMultipleOverrides(array('test1', 'test1.la__ma'))); + } + + /** + * Tests the saveOverride method. + * + * @covers ::saveOverride + * @covers ::loadOverride + * @covers ::getConfig + */ + public function testSaveOverride() { + $config = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config->expects($this->at(0)) + ->method('get') + ->with('definitions') + ->will($this->returnValue(array())); + $config->expects($this->at(1)) + ->method('get') + ->with('definitions') + ->will($this->returnValue(array())); + + $definition_save_1 = array('definitions' => array('test1' => array('parent' => 'test0'))); + $definitions_save_2 = array( + 'definitions' => array( + 'test1' => array('parent' => 'test0'), + 'test1__la___ma' => array('parent' => 'test1') + ) + ); + $config->expects($this->at(2)) + ->method('set') + ->with('definitions', $definition_save_1['definitions']) + ->will($this->returnSelf()); + $config->expects($this->at(3)) + ->method('save'); + $config->expects($this->at(4)) + ->method('get') + ->with('definitions') + ->will($this->returnValue($definition_save_1['definitions'])); + $config->expects($this->at(5)) + ->method('get') + ->with('definitions') + ->will($this->returnValue($definition_save_1['definitions'])); + $config->expects($this->at(6)) + ->method('set') + ->with('definitions', $definitions_save_2['definitions']) + ->will($this->returnSelf()); + $config->expects($this->at(7)) + ->method('save'); + + $config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface'); + $config_factory->expects($this->once()) + ->method('get') + ->will($this->returnValue($config)); + + $static_override = new StaticMenuLinkOverrides($config_factory); + + $static_override->saveOverride('test1', array('parent' => 'test0')); + $static_override->saveOverride('test1.la__ma', array('parent' => 'test1')); + } + + /** + * Tests the deleteOverride and deleteOverrides method. + * + * @param array|string $ids + * Either a single ID or multiple ones as array. + * @param array $old_definitions + * The definitions before the deleting + * @param array $new_definitions + * The definitions after the deleting. + * + * @dataProvider providerTestDeleteOverrides + */ + public function testDeleteOverrides($ids, array $old_definitions, array $new_definitions) { + $config = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config->expects($this->at(0)) + ->method('get') + ->with('definitions') + ->will($this->returnValue($old_definitions)); + + // Only save if the definitions changes. + if ($old_definitions != $new_definitions) { + $config->expects($this->at(1)) + ->method('set') + ->with('definitions', $new_definitions) + ->will($this->returnSelf()); + $config->expects($this->at(2)) + ->method('save'); + } + + $config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface'); + $config_factory->expects($this->once()) + ->method('get') + ->will($this->returnValue($config)); + + $static_override = new StaticMenuLinkOverrides($config_factory); + + if (is_array($ids)) { + $static_override->deleteMultipleOverrides($ids); + } + else { + $static_override->deleteOverride($ids); + } + } + + /** + * Provides test data for testDeleteOverrides. + */ + public function providerTestDeleteOverrides() { + $data = array(); + // Delete a non existing ID. + $data[] = array('test0', array(), array()); + // Delete an existing ID. + $data[] = array('test1', array('test1' => array('parent' => 'test0')), array()); + // Delete an existing ID with a special ID. + $data[] = array('test1.la__ma', array('test1__la___ma' => array('parent' => 'test0')), array()); + // Delete multiple IDs. + $data[] = array(array('test1.la__ma', 'test1'), array('test1' => array('parent' => 'test0'), 'test1__la___ma' => array('parent' => 'test0')), array()); + + return $data; + } + +}