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