MenuLinkManager.php 12 KB

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