Chris@0: 'tools', Chris@0: // (required) The name of the route this links to, unless it's external. Chris@0: 'route_name' => '', Chris@0: // Parameters for route variables when generating a link. Chris@0: 'route_parameters' => [], Chris@0: // The external URL if this link has one (required if route_name is empty). Chris@0: 'url' => '', Chris@0: // The static title for the menu link. If this came from a YAML definition Chris@0: // or other safe source this may be a TranslatableMarkup object. Chris@0: 'title' => '', Chris@0: // The description. If this came from a YAML definition or other safe source Chris@0: // this may be be a TranslatableMarkup object. Chris@0: 'description' => '', Chris@0: // The plugin ID of the parent link (or NULL for a top-level link). Chris@0: 'parent' => '', Chris@0: // The weight of the link. Chris@0: 'weight' => 0, Chris@0: // The default link options. Chris@0: 'options' => [], Chris@0: 'expanded' => 0, Chris@0: 'enabled' => 1, Chris@0: // The name of the module providing this link. Chris@0: 'provider' => '', Chris@0: 'metadata' => [], Chris@0: // Default class for local task implementations. Chris@0: 'class' => 'Drupal\Core\Menu\MenuLinkDefault', Chris@0: 'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm', Chris@0: // The plugin ID. Set by the plugin system based on the top-level YAML key. Chris@0: 'id' => '', Chris@0: ]; Chris@0: Chris@0: /** Chris@0: * The object that discovers plugins managed by this manager. Chris@0: * Chris@0: * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface Chris@0: */ Chris@0: protected $discovery; Chris@0: Chris@0: /** Chris@0: * The object that instantiates plugins managed by this manager. Chris@0: * Chris@0: * @var \Drupal\Component\Plugin\Factory\FactoryInterface Chris@0: */ Chris@0: protected $factory; Chris@0: Chris@0: /** Chris@0: * The menu link tree storage. Chris@0: * Chris@0: * @var \Drupal\Core\Menu\MenuTreeStorageInterface Chris@0: */ Chris@0: protected $treeStorage; Chris@0: Chris@0: /** Chris@0: * Service providing overrides for static links. Chris@0: * Chris@0: * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface Chris@0: */ Chris@0: protected $overrides; Chris@0: Chris@0: /** Chris@0: * The module handler. Chris@0: * Chris@0: * @var \Drupal\Core\Extension\ModuleHandlerInterface Chris@0: */ Chris@0: protected $moduleHandler; Chris@0: Chris@0: /** Chris@0: * Constructs a \Drupal\Core\Menu\MenuLinkManager object. Chris@0: * Chris@0: * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage Chris@0: * The menu link tree storage. Chris@0: * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides Chris@0: * The service providing overrides for static links. Chris@0: * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler Chris@0: * The module handler. Chris@0: */ Chris@0: public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, ModuleHandlerInterface $module_handler) { Chris@0: $this->treeStorage = $tree_storage; Chris@0: $this->overrides = $overrides; Chris@0: $this->moduleHandler = $module_handler; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs extra processing on plugin definitions. Chris@0: * Chris@0: * By default we add defaults for the type to the definition. If a type has Chris@0: * additional processing logic, the logic can be added by replacing or Chris@0: * extending this method. Chris@0: * Chris@0: * @param array $definition Chris@0: * The definition to be processed and modified by reference. Chris@0: * @param $plugin_id Chris@0: * The ID of the plugin this definition is being used for. Chris@0: */ Chris@0: protected function processDefinition(array &$definition, $plugin_id) { Chris@0: $definition = NestedArray::mergeDeep($this->defaults, $definition); Chris@0: // Typecast so NULL, no parent, will be an empty string since the parent ID Chris@0: // should be a string. Chris@0: $definition['parent'] = (string) $definition['parent']; Chris@0: $definition['id'] = $plugin_id; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the plugin discovery. Chris@0: * Chris@0: * @return \Drupal\Component\Plugin\Discovery\DiscoveryInterface Chris@0: */ Chris@0: protected function getDiscovery() { Chris@0: if (!isset($this->discovery)) { Chris@0: $yaml_discovery = new YamlDiscovery('links.menu', $this->moduleHandler->getModuleDirectories()); Chris@0: $yaml_discovery->addTranslatableProperty('title', 'title_context'); Chris@0: $yaml_discovery->addTranslatableProperty('description', 'description_context'); Chris@0: $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery); Chris@0: } Chris@0: return $this->discovery; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the plugin factory. Chris@0: * Chris@0: * @return \Drupal\Component\Plugin\Factory\FactoryInterface Chris@0: */ Chris@0: protected function getFactory() { Chris@0: if (!isset($this->factory)) { Chris@0: $this->factory = new ContainerFactory($this); Chris@0: } Chris@0: return $this->factory; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getDefinitions() { Chris@0: // Since this function is called rarely, instantiate the discovery here. Chris@0: $definitions = $this->getDiscovery()->getDefinitions(); Chris@0: Chris@0: $this->moduleHandler->alter('menu_links_discovered', $definitions); Chris@0: Chris@0: foreach ($definitions as $plugin_id => &$definition) { Chris@0: $definition['id'] = $plugin_id; Chris@0: $this->processDefinition($definition, $plugin_id); Chris@0: } Chris@0: Chris@0: // If this plugin was provided by a module that does not exist, remove the Chris@0: // plugin definition. Chris@0: // @todo Address what to do with an invalid plugin. Chris@0: // https://www.drupal.org/node/2302623 Chris@0: foreach ($definitions as $plugin_id => $plugin_definition) { Chris@0: if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) { Chris@0: unset($definitions[$plugin_id]); Chris@0: } Chris@0: } Chris@0: return $definitions; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function rebuild() { Chris@0: $definitions = $this->getDefinitions(); Chris@0: // Apply overrides from config. Chris@0: $overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions)); Chris@0: foreach ($overrides as $id => $changes) { Chris@0: if (!empty($definitions[$id])) { Chris@0: $definitions[$id] = $changes + $definitions[$id]; Chris@0: } Chris@0: } Chris@0: $this->treeStorage->rebuild($definitions); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getDefinition($plugin_id, $exception_on_invalid = TRUE) { Chris@0: $definition = $this->treeStorage->load($plugin_id); Chris@0: if (empty($definition) && $exception_on_invalid) { Chris@0: throw new PluginNotFoundException($plugin_id); Chris@0: } Chris@0: return $definition; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function hasDefinition($plugin_id) { Chris@0: return (bool) $this->getDefinition($plugin_id, FALSE); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a pre-configured menu link plugin instance. Chris@0: * Chris@0: * @param string $plugin_id Chris@0: * The ID of the plugin being instantiated. Chris@0: * @param array $configuration Chris@0: * An array of configuration relevant to the plugin instance. Chris@0: * Chris@0: * @return \Drupal\Core\Menu\MenuLinkInterface Chris@0: * A menu link instance. Chris@0: * Chris@0: * @throws \Drupal\Component\Plugin\Exception\PluginException Chris@0: * If the instance cannot be created, such as if the ID is invalid. Chris@0: */ Chris@0: public function createInstance($plugin_id, array $configuration = []) { Chris@0: return $this->getFactory()->createInstance($plugin_id, $configuration); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getInstance(array $options) { Chris@0: if (isset($options['id'])) { Chris@0: return $this->createInstance($options['id']); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function deleteLinksInMenu($menu_name) { Chris@0: foreach ($this->treeStorage->loadByProperties(['menu_name' => $menu_name]) as $plugin_id => $definition) { Chris@0: $instance = $this->createInstance($plugin_id); Chris@0: if ($instance->isDeletable()) { Chris@0: $this->deleteInstance($instance, TRUE); Chris@0: } Chris@0: elseif ($instance->isResettable()) { Chris@0: $new_instance = $this->resetInstance($instance); Chris@0: $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName(); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Deletes a specific instance. Chris@0: * Chris@0: * @param \Drupal\Core\Menu\MenuLinkInterface $instance Chris@0: * The plugin instance to be deleted. Chris@0: * @param bool $persist Chris@0: * If TRUE, calls MenuLinkInterface::deleteLink() on the instance. Chris@0: * Chris@0: * @throws \Drupal\Component\Plugin\Exception\PluginException Chris@0: * If the plugin instance does not support deletion. Chris@0: */ Chris@0: protected function deleteInstance(MenuLinkInterface $instance, $persist) { Chris@0: $id = $instance->getPluginId(); Chris@0: if ($instance->isDeletable()) { Chris@0: if ($persist) { Chris@0: $instance->deleteLink(); Chris@0: } Chris@0: } Chris@0: else { Chris@0: throw new PluginException("Menu link plugin with ID '$id' does not support deletion"); Chris@0: } Chris@0: $this->treeStorage->delete($id); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function removeDefinition($id, $persist = TRUE) { Chris@0: $definition = $this->treeStorage->load($id); Chris@0: // It's possible the definition has already been deleted, or doesn't exist. Chris@0: if ($definition) { Chris@0: $instance = $this->createInstance($id); Chris@0: $this->deleteInstance($instance, $persist); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function menuNameInUse($menu_name) { Chris@0: $this->treeStorage->menuNameInUse($menu_name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function countMenuLinks($menu_name = NULL) { Chris@0: return $this->treeStorage->countMenuLinks($menu_name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getParentIds($id) { Chris@0: if ($this->getDefinition($id, FALSE)) { Chris@0: return $this->treeStorage->getRootPathIds($id); Chris@0: } Chris@0: return NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getChildIds($id) { Chris@0: if ($this->getDefinition($id, FALSE)) { Chris@0: return $this->treeStorage->getAllChildIds($id); Chris@0: } Chris@0: return NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function loadLinksByRoute($route_name, array $route_parameters = [], $menu_name = NULL) { Chris@0: $instances = []; Chris@0: $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $menu_name); Chris@0: foreach ($loaded as $plugin_id => $definition) { Chris@0: $instances[$plugin_id] = $this->createInstance($plugin_id); Chris@0: } Chris@0: return $instances; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function addDefinition($id, array $definition) { Chris@0: if ($this->treeStorage->load($id)) { Chris@0: throw new PluginException("The menu link ID $id already exists as a plugin definition"); Chris@0: } Chris@0: elseif ($id === '') { Chris@0: throw new PluginException("The menu link ID cannot be empty"); Chris@0: } Chris@0: // Add defaults, so there is no requirement to specify everything. Chris@0: $this->processDefinition($definition, $id); Chris@0: // Store the new link in the tree. Chris@0: $this->treeStorage->save($definition); Chris@0: return $this->createInstance($id); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function updateDefinition($id, array $new_definition_values, $persist = TRUE) { Chris@0: $instance = $this->createInstance($id); Chris@0: if ($instance) { Chris@0: $new_definition_values['id'] = $id; Chris@0: $changed_definition = $instance->updateLink($new_definition_values, $persist); Chris@0: $this->treeStorage->save($changed_definition); Chris@0: } Chris@0: return $instance; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function resetLink($id) { Chris@0: $instance = $this->createInstance($id); Chris@0: $new_instance = $this->resetInstance($instance); Chris@0: return $new_instance; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Resets the menu link to its default settings. Chris@0: * Chris@0: * @param \Drupal\Core\Menu\MenuLinkInterface $instance Chris@0: * The menu link which should be reset. Chris@0: * Chris@0: * @return \Drupal\Core\Menu\MenuLinkInterface Chris@0: * The reset menu link. Chris@0: * Chris@0: * @throws \Drupal\Component\Plugin\Exception\PluginException Chris@0: * Thrown when the menu link is not resettable. Chris@0: */ Chris@0: protected function resetInstance(MenuLinkInterface $instance) { Chris@0: $id = $instance->getPluginId(); Chris@0: Chris@0: if (!$instance->isResettable()) { Chris@0: throw new PluginException("Menu link $id is not resettable"); Chris@0: } Chris@0: // Get the original data from disk, reset the override and re-save the menu Chris@0: // tree for this link. Chris@0: $definition = $this->getDefinitions()[$id]; Chris@0: $this->overrides->deleteOverride($id); Chris@0: $this->treeStorage->save($definition); Chris@0: return $this->createInstance($id); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function resetDefinitions() { Chris@0: $this->treeStorage->resetDefinitions(); Chris@0: } Chris@0: Chris@0: }