3 namespace Drupal\Core\Menu;
5 use Drupal\Component\Plugin\Exception\PluginException;
6 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
7 use Drupal\Component\Utility\NestedArray;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
10 use Drupal\Core\Plugin\Discovery\YamlDiscovery;
11 use Drupal\Core\Plugin\Factory\ContainerFactory;
15 * Manages discovery, instantiation, and tree building of menu link plugins.
17 * This manager finds plugins that are rendered as menu links.
19 class MenuLinkManager implements MenuLinkManagerInterface {
22 * Provides some default values for the definition of all menu link plugins.
24 * @todo Decide how to keep these field definitions in sync.
25 * https://www.drupal.org/node/2302085
29 protected $defaults = [
30 // (required) The name of the menu for this link.
31 'menu_name' => 'tools',
32 // (required) The name of the route this links to, unless it's external.
34 // Parameters for route variables when generating a link.
35 'route_parameters' => [],
36 // The external URL if this link has one (required if route_name is empty).
38 // The static title for the menu link. If this came from a YAML definition
39 // or other safe source this may be a TranslatableMarkup object.
41 // The description. If this came from a YAML definition or other safe source
42 // this may be be a TranslatableMarkup object.
44 // The plugin ID of the parent link (or NULL for a top-level link).
46 // The weight of the link.
48 // The default link options.
52 // The name of the module providing this link.
55 // Default class for local task implementations.
56 'class' => 'Drupal\Core\Menu\MenuLinkDefault',
57 'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm',
58 // The plugin ID. Set by the plugin system based on the top-level YAML key.
63 * The object that discovers plugins managed by this manager.
65 * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
70 * The object that instantiates plugins managed by this manager.
72 * @var \Drupal\Component\Plugin\Factory\FactoryInterface
77 * The menu link tree storage.
79 * @var \Drupal\Core\Menu\MenuTreeStorageInterface
81 protected $treeStorage;
84 * Service providing overrides for static links.
86 * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
93 * @var \Drupal\Core\Extension\ModuleHandlerInterface
95 protected $moduleHandler;
99 * Constructs a \Drupal\Core\Menu\MenuLinkManager object.
101 * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
102 * The menu link tree storage.
103 * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides
104 * The service providing overrides for static links.
105 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
106 * The module handler.
108 public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, ModuleHandlerInterface $module_handler) {
109 $this->treeStorage = $tree_storage;
110 $this->overrides = $overrides;
111 $this->moduleHandler = $module_handler;
115 * Performs extra processing on plugin definitions.
117 * By default we add defaults for the type to the definition. If a type has
118 * additional processing logic, the logic can be added by replacing or
119 * extending this method.
121 * @param array $definition
122 * The definition to be processed and modified by reference.
124 * The ID of the plugin this definition is being used for.
126 protected function processDefinition(array &$definition, $plugin_id) {
127 $definition = NestedArray::mergeDeep($this->defaults, $definition);
128 // Typecast so NULL, no parent, will be an empty string since the parent ID
129 // should be a string.
130 $definition['parent'] = (string) $definition['parent'];
131 $definition['id'] = $plugin_id;
135 * Gets the plugin discovery.
137 * @return \Drupal\Component\Plugin\Discovery\DiscoveryInterface
139 protected function getDiscovery() {
140 if (!isset($this->discovery)) {
141 $yaml_discovery = new YamlDiscovery('links.menu', $this->moduleHandler->getModuleDirectories());
142 $yaml_discovery->addTranslatableProperty('title', 'title_context');
143 $yaml_discovery->addTranslatableProperty('description', 'description_context');
144 $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
146 return $this->discovery;
150 * Gets the plugin factory.
152 * @return \Drupal\Component\Plugin\Factory\FactoryInterface
154 protected function getFactory() {
155 if (!isset($this->factory)) {
156 $this->factory = new ContainerFactory($this);
158 return $this->factory;
164 public function getDefinitions() {
165 // Since this function is called rarely, instantiate the discovery here.
166 $definitions = $this->getDiscovery()->getDefinitions();
168 $this->moduleHandler->alter('menu_links_discovered', $definitions);
170 foreach ($definitions as $plugin_id => &$definition) {
171 $definition['id'] = $plugin_id;
172 $this->processDefinition($definition, $plugin_id);
175 // If this plugin was provided by a module that does not exist, remove the
176 // plugin definition.
177 // @todo Address what to do with an invalid plugin.
178 // https://www.drupal.org/node/2302623
179 foreach ($definitions as $plugin_id => $plugin_definition) {
180 if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) {
181 unset($definitions[$plugin_id]);
190 public function rebuild() {
191 $definitions = $this->getDefinitions();
192 // Apply overrides from config.
193 $overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions));
194 foreach ($overrides as $id => $changes) {
195 if (!empty($definitions[$id])) {
196 $definitions[$id] = $changes + $definitions[$id];
199 $this->treeStorage->rebuild($definitions);
205 public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
206 $definition = $this->treeStorage->load($plugin_id);
207 if (empty($definition) && $exception_on_invalid) {
208 throw new PluginNotFoundException($plugin_id);
216 public function hasDefinition($plugin_id) {
217 return (bool) $this->getDefinition($plugin_id, FALSE);
221 * Returns a pre-configured menu link plugin instance.
223 * @param string $plugin_id
224 * The ID of the plugin being instantiated.
225 * @param array $configuration
226 * An array of configuration relevant to the plugin instance.
228 * @return \Drupal\Core\Menu\MenuLinkInterface
229 * A menu link instance.
231 * @throws \Drupal\Component\Plugin\Exception\PluginException
232 * If the instance cannot be created, such as if the ID is invalid.
234 public function createInstance($plugin_id, array $configuration = []) {
235 return $this->getFactory()->createInstance($plugin_id, $configuration);
241 public function getInstance(array $options) {
242 if (isset($options['id'])) {
243 return $this->createInstance($options['id']);
250 public function deleteLinksInMenu($menu_name) {
251 foreach ($this->treeStorage->loadByProperties(['menu_name' => $menu_name]) as $plugin_id => $definition) {
252 $instance = $this->createInstance($plugin_id);
253 if ($instance->isDeletable()) {
254 $this->deleteInstance($instance, TRUE);
256 elseif ($instance->isResettable()) {
257 $new_instance = $this->resetInstance($instance);
258 $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
264 * Deletes a specific instance.
266 * @param \Drupal\Core\Menu\MenuLinkInterface $instance
267 * The plugin instance to be deleted.
268 * @param bool $persist
269 * If TRUE, calls MenuLinkInterface::deleteLink() on the instance.
271 * @throws \Drupal\Component\Plugin\Exception\PluginException
272 * If the plugin instance does not support deletion.
274 protected function deleteInstance(MenuLinkInterface $instance, $persist) {
275 $id = $instance->getPluginId();
276 if ($instance->isDeletable()) {
278 $instance->deleteLink();
282 throw new PluginException("Menu link plugin with ID '$id' does not support deletion");
284 $this->treeStorage->delete($id);
290 public function removeDefinition($id, $persist = TRUE) {
291 $definition = $this->treeStorage->load($id);
292 // It's possible the definition has already been deleted, or doesn't exist.
294 $instance = $this->createInstance($id);
295 $this->deleteInstance($instance, $persist);
302 public function menuNameInUse($menu_name) {
303 $this->treeStorage->menuNameInUse($menu_name);
309 public function countMenuLinks($menu_name = NULL) {
310 return $this->treeStorage->countMenuLinks($menu_name);
316 public function getParentIds($id) {
317 if ($this->getDefinition($id, FALSE)) {
318 return $this->treeStorage->getRootPathIds($id);
326 public function getChildIds($id) {
327 if ($this->getDefinition($id, FALSE)) {
328 return $this->treeStorage->getAllChildIds($id);
336 public function loadLinksByRoute($route_name, array $route_parameters = [], $menu_name = NULL) {
338 $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $menu_name);
339 foreach ($loaded as $plugin_id => $definition) {
340 $instances[$plugin_id] = $this->createInstance($plugin_id);
348 public function addDefinition($id, array $definition) {
349 if ($this->treeStorage->load($id)) {
350 throw new PluginException("The menu link ID $id already exists as a plugin definition");
352 elseif ($id === '') {
353 throw new PluginException("The menu link ID cannot be empty");
355 // Add defaults, so there is no requirement to specify everything.
356 $this->processDefinition($definition, $id);
357 // Store the new link in the tree.
358 $this->treeStorage->save($definition);
359 return $this->createInstance($id);
365 public function updateDefinition($id, array $new_definition_values, $persist = TRUE) {
366 $instance = $this->createInstance($id);
368 $new_definition_values['id'] = $id;
369 $changed_definition = $instance->updateLink($new_definition_values, $persist);
370 $this->treeStorage->save($changed_definition);
378 public function resetLink($id) {
379 $instance = $this->createInstance($id);
380 $new_instance = $this->resetInstance($instance);
381 return $new_instance;
385 * Resets the menu link to its default settings.
387 * @param \Drupal\Core\Menu\MenuLinkInterface $instance
388 * The menu link which should be reset.
390 * @return \Drupal\Core\Menu\MenuLinkInterface
391 * The reset menu link.
393 * @throws \Drupal\Component\Plugin\Exception\PluginException
394 * Thrown when the menu link is not resettable.
396 protected function resetInstance(MenuLinkInterface $instance) {
397 $id = $instance->getPluginId();
399 if (!$instance->isResettable()) {
400 throw new PluginException("Menu link $id is not resettable");
402 // Get the original data from disk, reset the override and re-save the menu
403 // tree for this link.
404 $definition = $this->getDefinitions()[$id];
405 $this->overrides->deleteOverride($id);
406 $this->treeStorage->save($definition);
407 return $this->createInstance($id);
413 public function resetDefinitions() {
414 $this->treeStorage->resetDefinitions();