1<?php
2
3namespace Drupal\Core\Menu;
4
5use Drupal\Component\Plugin\Exception\PluginException;
6use Drupal\Component\Plugin\Exception\PluginNotFoundException;
7use Drupal\Component\Utility\NestedArray;
8use Drupal\Core\Extension\ModuleHandlerInterface;
9use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
10use Drupal\Core\Plugin\Discovery\YamlDiscovery;
11use Drupal\Core\Plugin\Factory\ContainerFactory;
12
13/**
14 * Manages discovery, instantiation, and tree building of menu link plugins.
15 *
16 * This manager finds plugins that are rendered as menu links.
17 */
18class MenuLinkManager implements MenuLinkManagerInterface {
19
20  /**
21   * Provides some default values for the definition of all menu link plugins.
22   *
23   * @todo Decide how to keep these field definitions in sync.
24   *   https://www.drupal.org/node/2302085
25   *
26   * @var array
27   */
28  protected $defaults = [
29    // (required) The name of the menu for this link.
30    'menu_name' => 'tools',
31    // (required) The name of the route this links to, unless it's external.
32    'route_name' => '',
33    // Parameters for route variables when generating a link.
34    'route_parameters' => [],
35    // The external URL if this link has one (required if route_name is empty).
36    'url' => '',
37    // The static title for the menu link. If this came from a YAML definition
38    // or other safe source this may be a TranslatableMarkup object.
39    'title' => '',
40    // The description. If this came from a YAML definition or other safe source
41    // this may be a TranslatableMarkup object.
42    'description' => '',
43    // The plugin ID of the parent link (or NULL for a top-level link).
44    'parent' => '',
45    // The weight of the link.
46    'weight' => 0,
47    // The default link options.
48    'options' => [],
49    'expanded' => 0,
50    'enabled' => 1,
51    // The name of the module providing this link.
52    'provider' => '',
53    'metadata' => [],
54    // Default class for local task implementations.
55    'class' => 'Drupal\Core\Menu\MenuLinkDefault',
56    'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm',
57    // The plugin ID. Set by the plugin system based on the top-level YAML key.
58    'id' => '',
59  ];
60
61  /**
62   * The object that discovers plugins managed by this manager.
63   *
64   * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
65   */
66  protected $discovery;
67
68  /**
69   * The object that instantiates plugins managed by this manager.
70   *
71   * @var \Drupal\Component\Plugin\Factory\FactoryInterface
72   */
73  protected $factory;
74
75  /**
76   * The menu link tree storage.
77   *
78   * @var \Drupal\Core\Menu\MenuTreeStorageInterface
79   */
80  protected $treeStorage;
81
82  /**
83   * Service providing overrides for static links.
84   *
85   * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
86   */
87  protected $overrides;
88
89  /**
90   * The module handler.
91   *
92   * @var \Drupal\Core\Extension\ModuleHandlerInterface
93   */
94  protected $moduleHandler;
95
96  /**
97   * Constructs a \Drupal\Core\Menu\MenuLinkManager object.
98   *
99   * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
100   *   The menu link tree storage.
101   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides
102   *   The service providing overrides for static links.
103   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
104   *   The module handler.
105   */
106  public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, ModuleHandlerInterface $module_handler) {
107    $this->treeStorage = $tree_storage;
108    $this->overrides = $overrides;
109    $this->moduleHandler = $module_handler;
110  }
111
112  /**
113   * Performs extra processing on plugin definitions.
114   *
115   * By default we add defaults for the type to the definition. If a type has
116   * additional processing logic, the logic can be added by replacing or
117   * extending this method.
118   *
119   * @param array $definition
120   *   The definition to be processed and modified by reference.
121   * @param $plugin_id
122   *   The ID of the plugin this definition is being used for.
123   */
124  protected function processDefinition(array &$definition, $plugin_id) {
125    $definition = NestedArray::mergeDeep($this->defaults, $definition);
126    // Typecast so NULL, no parent, will be an empty string since the parent ID
127    // should be a string.
128    $definition['parent'] = (string) $definition['parent'];
129    $definition['id'] = $plugin_id;
130  }
131
132  /**
133   * Gets the plugin discovery.
134   *
135   * @return \Drupal\Component\Plugin\Discovery\DiscoveryInterface
136   */
137  protected function getDiscovery() {
138    if (!isset($this->discovery)) {
139      $yaml_discovery = new YamlDiscovery('links.menu', $this->moduleHandler->getModuleDirectories());
140      $yaml_discovery->addTranslatableProperty('title', 'title_context');
141      $yaml_discovery->addTranslatableProperty('description', 'description_context');
142      $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
143    }
144    return $this->discovery;
145  }
146
147  /**
148   * Gets the plugin factory.
149   *
150   * @return \Drupal\Component\Plugin\Factory\FactoryInterface
151   */
152  protected function getFactory() {
153    if (!isset($this->factory)) {
154      $this->factory = new ContainerFactory($this);
155    }
156    return $this->factory;
157  }
158
159  /**
160   * {@inheritdoc}
161   */
162  public function getDefinitions() {
163    // Since this function is called rarely, instantiate the discovery here.
164    $definitions = $this->getDiscovery()->getDefinitions();
165
166    $this->moduleHandler->alter('menu_links_discovered', $definitions);
167
168    foreach ($definitions as $plugin_id => &$definition) {
169      $definition['id'] = $plugin_id;
170      $this->processDefinition($definition, $plugin_id);
171    }
172
173    // If this plugin was provided by a module that does not exist, remove the
174    // plugin definition.
175    // @todo Address what to do with an invalid plugin.
176    //   https://www.drupal.org/node/2302623
177    foreach ($definitions as $plugin_id => $plugin_definition) {
178      if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) {
179        unset($definitions[$plugin_id]);
180      }
181    }
182    return $definitions;
183  }
184
185  /**
186   * {@inheritdoc}
187   */
188  public function rebuild() {
189    $definitions = $this->getDefinitions();
190    // Apply overrides from config.
191    $overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions));
192    foreach ($overrides as $id => $changes) {
193      if (!empty($definitions[$id])) {
194        $definitions[$id] = $changes + $definitions[$id];
195      }
196    }
197    $this->treeStorage->rebuild($definitions);
198  }
199
200  /**
201   * {@inheritdoc}
202   */
203  public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
204    $definition = $this->treeStorage->load($plugin_id);
205    if (empty($definition) && $exception_on_invalid) {
206      throw new PluginNotFoundException($plugin_id);
207    }
208    return $definition;
209  }
210
211  /**
212   * {@inheritdoc}
213   */
214  public function hasDefinition($plugin_id) {
215    return (bool) $this->getDefinition($plugin_id, FALSE);
216  }
217
218  /**
219   * Returns a pre-configured menu link plugin instance.
220   *
221   * @param string $plugin_id
222   *   The ID of the plugin being instantiated.
223   * @param array $configuration
224   *   An array of configuration relevant to the plugin instance.
225   *
226   * @return \Drupal\Core\Menu\MenuLinkInterface
227   *   A menu link instance.
228   *
229   * @throws \Drupal\Component\Plugin\Exception\PluginException
230   *   If the instance cannot be created, such as if the ID is invalid.
231   */
232  public function createInstance($plugin_id, array $configuration = []) {
233    return $this->getFactory()->createInstance($plugin_id, $configuration);
234  }
235
236  /**
237   * {@inheritdoc}
238   */
239  public function getInstance(array $options) {
240    if (isset($options['id'])) {
241      return $this->createInstance($options['id']);
242    }
243  }
244
245  /**
246   * {@inheritdoc}
247   */
248  public function deleteLinksInMenu($menu_name) {
249    foreach ($this->treeStorage->loadByProperties(['menu_name' => $menu_name]) as $plugin_id => $definition) {
250      $instance = $this->createInstance($plugin_id);
251      if ($instance->isDeletable()) {
252        $this->deleteInstance($instance, TRUE);
253      }
254      elseif ($instance->isResettable()) {
255        $new_instance = $this->resetInstance($instance);
256        $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
257      }
258    }
259  }
260
261  /**
262   * Deletes a specific instance.
263   *
264   * @param \Drupal\Core\Menu\MenuLinkInterface $instance
265   *   The plugin instance to be deleted.
266   * @param bool $persist
267   *   If TRUE, calls MenuLinkInterface::deleteLink() on the instance.
268   *
269   * @throws \Drupal\Component\Plugin\Exception\PluginException
270   *   If the plugin instance does not support deletion.
271   */
272  protected function deleteInstance(MenuLinkInterface $instance, $persist) {
273    $id = $instance->getPluginId();
274    if ($instance->isDeletable()) {
275      if ($persist) {
276        $instance->deleteLink();
277      }
278    }
279    else {
280      throw new PluginException("Menu link plugin with ID '$id' does not support deletion");
281    }
282    $this->treeStorage->delete($id);
283  }
284
285  /**
286   * {@inheritdoc}
287   */
288  public function removeDefinition($id, $persist = TRUE) {
289    $definition = $this->treeStorage->load($id);
290    // It's possible the definition has already been deleted, or doesn't exist.
291    if ($definition) {
292      $instance = $this->createInstance($id);
293      $this->deleteInstance($instance, $persist);
294    }
295  }
296
297  /**
298   * {@inheritdoc}
299   */
300  public function menuNameInUse($menu_name) {
301    $this->treeStorage->menuNameInUse($menu_name);
302  }
303
304  /**
305   * {@inheritdoc}
306   */
307  public function countMenuLinks($menu_name = NULL) {
308    return $this->treeStorage->countMenuLinks($menu_name);
309  }
310
311  /**
312   * {@inheritdoc}
313   */
314  public function getParentIds($id) {
315    if ($this->getDefinition($id, FALSE)) {
316      return $this->treeStorage->getRootPathIds($id);
317    }
318    return NULL;
319  }
320
321  /**
322   * {@inheritdoc}
323   */
324  public function getChildIds($id) {
325    if ($this->getDefinition($id, FALSE)) {
326      return $this->treeStorage->getAllChildIds($id);
327    }
328    return NULL;
329  }
330
331  /**
332   * {@inheritdoc}
333   */
334  public function loadLinksByRoute($route_name, array $route_parameters = [], $menu_name = NULL) {
335    $instances = [];
336    $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $menu_name);
337    foreach ($loaded as $plugin_id => $definition) {
338      $instances[$plugin_id] = $this->createInstance($plugin_id);
339    }
340    return $instances;
341  }
342
343  /**
344   * {@inheritdoc}
345   */
346  public function addDefinition($id, array $definition) {
347    if ($this->treeStorage->load($id)) {
348      throw new PluginException("The menu link ID $id already exists as a plugin definition");
349    }
350    elseif ($id === '') {
351      throw new PluginException("The menu link ID cannot be empty");
352    }
353    // Add defaults, so there is no requirement to specify everything.
354    $this->processDefinition($definition, $id);
355    // Store the new link in the tree.
356    $this->treeStorage->save($definition);
357    return $this->createInstance($id);
358  }
359
360  /**
361   * {@inheritdoc}
362   */
363  public function updateDefinition($id, array $new_definition_values, $persist = TRUE) {
364    $instance = $this->createInstance($id);
365    if ($instance) {
366      $new_definition_values['id'] = $id;
367      $changed_definition = $instance->updateLink($new_definition_values, $persist);
368      $this->treeStorage->save($changed_definition);
369    }
370    return $instance;
371  }
372
373  /**
374   * {@inheritdoc}
375   */
376  public function resetLink($id) {
377    $instance = $this->createInstance($id);
378    $new_instance = $this->resetInstance($instance);
379    return $new_instance;
380  }
381
382  /**
383   * Resets the menu link to its default settings.
384   *
385   * @param \Drupal\Core\Menu\MenuLinkInterface $instance
386   *   The menu link which should be reset.
387   *
388   * @return \Drupal\Core\Menu\MenuLinkInterface
389   *   The reset menu link.
390   *
391   * @throws \Drupal\Component\Plugin\Exception\PluginException
392   *   Thrown when the menu link is not resettable.
393   */
394  protected function resetInstance(MenuLinkInterface $instance) {
395    $id = $instance->getPluginId();
396
397    if (!$instance->isResettable()) {
398      throw new PluginException("Menu link $id is not resettable");
399    }
400    // Get the original data from disk, reset the override and re-save the menu
401    // tree for this link.
402    $definition = $this->getDefinitions()[$id];
403    $this->overrides->deleteOverride($id);
404    $this->treeStorage->save($definition);
405    return $this->createInstance($id);
406  }
407
408  /**
409   * {@inheritdoc}
410   */
411  public function resetDefinitions() {
412    $this->treeStorage->resetDefinitions();
413  }
414
415}
416