Version 1
[yaffs-website] / web / core / lib / Drupal / Core / Menu / MenuLinkManager.php
1 <?php
2
3 namespace Drupal\Core\Menu;
4
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;
12
13
14 /**
15  * Manages discovery, instantiation, and tree building of menu link plugins.
16  *
17  * This manager finds plugins that are rendered as menu links.
18  */
19 class MenuLinkManager implements MenuLinkManagerInterface {
20
21   /**
22    * Provides some default values for the definition of all menu link plugins.
23    *
24    * @todo Decide how to keep these field definitions in sync.
25    *   https://www.drupal.org/node/2302085
26    *
27    * @var array
28    */
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.
33     'route_name' => '',
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).
37     'url' => '',
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.
40     'title' => '',
41     // The description. If this came from a YAML definition or other safe source
42     // this may be be a TranslatableMarkup object.
43     'description' => '',
44     // The plugin ID of the parent link (or NULL for a top-level link).
45     'parent' => '',
46     // The weight of the link.
47     'weight' => 0,
48     // The default link options.
49     'options' => [],
50     'expanded' => 0,
51     'enabled' => 1,
52     // The name of the module providing this link.
53     'provider' => '',
54     'metadata' => [],
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.
59     'id' => '',
60   ];
61
62   /**
63    * The object that discovers plugins managed by this manager.
64    *
65    * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
66    */
67   protected $discovery;
68
69   /**
70    * The object that instantiates plugins managed by this manager.
71    *
72    * @var \Drupal\Component\Plugin\Factory\FactoryInterface
73    */
74   protected $factory;
75
76   /**
77    * The menu link tree storage.
78    *
79    * @var \Drupal\Core\Menu\MenuTreeStorageInterface
80    */
81   protected $treeStorage;
82
83   /**
84    * Service providing overrides for static links.
85    *
86    * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
87    */
88   protected $overrides;
89
90   /**
91    * The module handler.
92    *
93    * @var \Drupal\Core\Extension\ModuleHandlerInterface
94    */
95   protected $moduleHandler;
96
97
98   /**
99    * Constructs a \Drupal\Core\Menu\MenuLinkManager object.
100    *
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.
107    */
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;
112   }
113
114   /**
115    * Performs extra processing on plugin definitions.
116    *
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.
120    *
121    * @param array $definition
122    *   The definition to be processed and modified by reference.
123    * @param $plugin_id
124    *   The ID of the plugin this definition is being used for.
125    */
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;
132   }
133
134   /**
135    * Gets the plugin discovery.
136    *
137    * @return \Drupal\Component\Plugin\Discovery\DiscoveryInterface
138    */
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);
145     }
146     return $this->discovery;
147   }
148
149   /**
150    * Gets the plugin factory.
151    *
152    * @return \Drupal\Component\Plugin\Factory\FactoryInterface
153    */
154   protected function getFactory() {
155     if (!isset($this->factory)) {
156       $this->factory = new ContainerFactory($this);
157     }
158     return $this->factory;
159   }
160
161   /**
162    * {@inheritdoc}
163    */
164   public function getDefinitions() {
165     // Since this function is called rarely, instantiate the discovery here.
166     $definitions = $this->getDiscovery()->getDefinitions();
167
168     $this->moduleHandler->alter('menu_links_discovered', $definitions);
169
170     foreach ($definitions as $plugin_id => &$definition) {
171       $definition['id'] = $plugin_id;
172       $this->processDefinition($definition, $plugin_id);
173     }
174
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]);
182       }
183     }
184     return $definitions;
185   }
186
187   /**
188    * {@inheritdoc}
189    */
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];
197       }
198     }
199     $this->treeStorage->rebuild($definitions);
200   }
201
202   /**
203    * {@inheritdoc}
204    */
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);
209     }
210     return $definition;
211   }
212
213   /**
214    * {@inheritdoc}
215    */
216   public function hasDefinition($plugin_id) {
217     return (bool) $this->getDefinition($plugin_id, FALSE);
218   }
219
220   /**
221    * Returns a pre-configured menu link plugin instance.
222    *
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.
227    *
228    * @return \Drupal\Core\Menu\MenuLinkInterface
229    *   A menu link instance.
230    *
231    * @throws \Drupal\Component\Plugin\Exception\PluginException
232    *   If the instance cannot be created, such as if the ID is invalid.
233    */
234   public function createInstance($plugin_id, array $configuration = []) {
235     return $this->getFactory()->createInstance($plugin_id, $configuration);
236   }
237
238   /**
239    * {@inheritdoc}
240    */
241   public function getInstance(array $options) {
242     if (isset($options['id'])) {
243       return $this->createInstance($options['id']);
244     }
245   }
246
247   /**
248    * {@inheritdoc}
249    */
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);
255       }
256       elseif ($instance->isResettable()) {
257         $new_instance = $this->resetInstance($instance);
258         $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
259       }
260     }
261   }
262
263   /**
264    * Deletes a specific instance.
265    *
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.
270    *
271    * @throws \Drupal\Component\Plugin\Exception\PluginException
272    *   If the plugin instance does not support deletion.
273    */
274   protected function deleteInstance(MenuLinkInterface $instance, $persist) {
275     $id = $instance->getPluginId();
276     if ($instance->isDeletable()) {
277       if ($persist) {
278         $instance->deleteLink();
279       }
280     }
281     else {
282       throw new PluginException("Menu link plugin with ID '$id' does not support deletion");
283     }
284     $this->treeStorage->delete($id);
285   }
286
287   /**
288    * {@inheritdoc}
289    */
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.
293     if ($definition) {
294       $instance = $this->createInstance($id);
295       $this->deleteInstance($instance, $persist);
296     }
297   }
298
299   /**
300    * {@inheritdoc}
301    */
302   public function menuNameInUse($menu_name) {
303     $this->treeStorage->menuNameInUse($menu_name);
304   }
305
306   /**
307    * {@inheritdoc}
308    */
309   public function countMenuLinks($menu_name = NULL) {
310     return $this->treeStorage->countMenuLinks($menu_name);
311   }
312
313   /**
314    * {@inheritdoc}
315    */
316   public function getParentIds($id) {
317     if ($this->getDefinition($id, FALSE)) {
318       return $this->treeStorage->getRootPathIds($id);
319     }
320     return NULL;
321   }
322
323   /**
324    * {@inheritdoc}
325    */
326   public function getChildIds($id) {
327     if ($this->getDefinition($id, FALSE)) {
328       return $this->treeStorage->getAllChildIds($id);
329     }
330     return NULL;
331   }
332
333   /**
334    * {@inheritdoc}
335    */
336   public function loadLinksByRoute($route_name, array $route_parameters = [], $menu_name = NULL) {
337     $instances = [];
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);
341     }
342     return $instances;
343   }
344
345   /**
346    * {@inheritdoc}
347    */
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");
351     }
352     elseif ($id === '') {
353       throw new PluginException("The menu link ID cannot be empty");
354     }
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);
360   }
361
362   /**
363    * {@inheritdoc}
364    */
365   public function updateDefinition($id, array $new_definition_values, $persist = TRUE) {
366     $instance = $this->createInstance($id);
367     if ($instance) {
368       $new_definition_values['id'] = $id;
369       $changed_definition = $instance->updateLink($new_definition_values, $persist);
370       $this->treeStorage->save($changed_definition);
371     }
372     return $instance;
373   }
374
375   /**
376    * {@inheritdoc}
377    */
378   public function resetLink($id) {
379     $instance = $this->createInstance($id);
380     $new_instance = $this->resetInstance($instance);
381     return $new_instance;
382   }
383
384   /**
385    * Resets the menu link to its default settings.
386    *
387    * @param \Drupal\Core\Menu\MenuLinkInterface $instance
388    *   The menu link which should be reset.
389    *
390    * @return \Drupal\Core\Menu\MenuLinkInterface
391    *   The reset menu link.
392    *
393    * @throws \Drupal\Component\Plugin\Exception\PluginException
394    *   Thrown when the menu link is not resettable.
395    */
396   protected function resetInstance(MenuLinkInterface $instance) {
397     $id = $instance->getPluginId();
398
399     if (!$instance->isResettable()) {
400       throw new PluginException("Menu link $id is not resettable");
401     }
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);
408   }
409
410   /**
411    * {@inheritdoc}
412    */
413   public function resetDefinitions() {
414     $this->treeStorage->resetDefinitions();
415   }
416
417 }