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