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