MenuLinkTree.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <?php
  2. namespace Drupal\Core\Menu;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Core\Access\AccessResultInterface;
  5. use Drupal\Core\Cache\CacheableMetadata;
  6. use Drupal\Core\Controller\ControllerResolverInterface;
  7. use Drupal\Core\Routing\RouteProviderInterface;
  8. use Drupal\Core\Template\Attribute;
  9. /**
  10. * Implements the loading, transforming and rendering of menu link trees.
  11. */
  12. class MenuLinkTree implements MenuLinkTreeInterface {
  13. /**
  14. * The menu link tree storage.
  15. *
  16. * @var \Drupal\Core\Menu\MenuTreeStorageInterface
  17. */
  18. protected $treeStorage;
  19. /**
  20. * The menu link plugin manager.
  21. *
  22. * @var \Drupal\Core\Menu\MenuLinkManagerInterface
  23. */
  24. protected $menuLinkManager;
  25. /**
  26. * The route provider to load routes by name.
  27. *
  28. * @var \Drupal\Core\Routing\RouteProviderInterface
  29. */
  30. protected $routeProvider;
  31. /**
  32. * The active menu trail service.
  33. *
  34. * @var \Drupal\Core\Menu\MenuActiveTrailInterface
  35. */
  36. protected $menuActiveTrail;
  37. /**
  38. * The controller resolver.
  39. *
  40. * @var \Drupal\Core\Controller\ControllerResolverInterface
  41. */
  42. protected $controllerResolver;
  43. /**
  44. * Constructs a \Drupal\Core\Menu\MenuLinkTree object.
  45. *
  46. * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
  47. * The menu link tree storage.
  48. * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
  49. * The menu link plugin manager.
  50. * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
  51. * The route provider to load routes by name.
  52. * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail
  53. * The active menu trail service.
  54. * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
  55. * The controller resolver.
  56. */
  57. public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) {
  58. $this->treeStorage = $tree_storage;
  59. $this->menuLinkManager = $menu_link_manager;
  60. $this->routeProvider = $route_provider;
  61. $this->menuActiveTrail = $menu_active_trail;
  62. $this->controllerResolver = $controller_resolver;
  63. }
  64. /**
  65. * {@inheritdoc}
  66. */
  67. public function getCurrentRouteMenuTreeParameters($menu_name) {
  68. $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name);
  69. $parameters = new MenuTreeParameters();
  70. $parameters->setActiveTrail($active_trail)
  71. // We want links in the active trail to be expanded.
  72. ->addExpandedParents($active_trail)
  73. // We marked the links in the active trail to be expanded, but we also
  74. // want their descendants that have the "expanded" flag enabled to be
  75. // expanded.
  76. ->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail));
  77. return $parameters;
  78. }
  79. /**
  80. * {@inheritdoc}
  81. */
  82. public function load($menu_name, MenuTreeParameters $parameters) {
  83. $data = $this->treeStorage->loadTreeData($menu_name, $parameters);
  84. // Pre-load all the route objects in the tree for access checks.
  85. if ($data['route_names']) {
  86. $this->routeProvider->getRoutesByNames($data['route_names']);
  87. }
  88. return $this->createInstances($data['tree']);
  89. }
  90. /**
  91. * Returns a tree containing of MenuLinkTreeElement based upon tree data.
  92. *
  93. * This method converts the tree representation as array coming from the tree
  94. * storage to a tree containing a list of MenuLinkTreeElement[].
  95. *
  96. * @param array $data_tree
  97. * The tree data coming from the menu tree storage.
  98. *
  99. * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
  100. * An array containing the elements of a menu tree.
  101. */
  102. protected function createInstances(array $data_tree) {
  103. $tree = [];
  104. foreach ($data_tree as $key => $element) {
  105. $subtree = $this->createInstances($element['subtree']);
  106. // Build a MenuLinkTreeElement out of the menu tree link definition:
  107. // transform the tree link definition into a link definition and store
  108. // tree metadata.
  109. $tree[$key] = new MenuLinkTreeElement(
  110. $this->menuLinkManager->createInstance($element['definition']['id']),
  111. (bool) $element['has_children'],
  112. (int) $element['depth'],
  113. (bool) $element['in_active_trail'],
  114. $subtree
  115. );
  116. }
  117. return $tree;
  118. }
  119. /**
  120. * {@inheritdoc}
  121. */
  122. public function transform(array $tree, array $manipulators) {
  123. foreach ($manipulators as $manipulator) {
  124. $callable = $manipulator['callable'];
  125. $callable = $this->controllerResolver->getControllerFromDefinition($callable);
  126. // Prepare the arguments for the menu tree manipulator callable; the first
  127. // argument is always the menu link tree.
  128. if (isset($manipulator['args'])) {
  129. array_unshift($manipulator['args'], $tree);
  130. $tree = call_user_func_array($callable, $manipulator['args']);
  131. }
  132. else {
  133. $tree = call_user_func($callable, $tree);
  134. }
  135. }
  136. return $tree;
  137. }
  138. /**
  139. * {@inheritdoc}
  140. */
  141. public function build(array $tree) {
  142. $tree_access_cacheability = new CacheableMetadata();
  143. $tree_link_cacheability = new CacheableMetadata();
  144. $items = $this->buildItems($tree, $tree_access_cacheability, $tree_link_cacheability);
  145. $build = [];
  146. // Apply the tree-wide gathered access cacheability metadata and link
  147. // cacheability metadata to the render array. This ensures that the
  148. // rendered menu is varied by the cache contexts that the access results
  149. // and (dynamic) links depended upon, and invalidated by the cache tags
  150. // that may change the values of the access results and links.
  151. $tree_cacheability = $tree_access_cacheability->merge($tree_link_cacheability);
  152. $tree_cacheability->applyTo($build);
  153. if ($items) {
  154. // Make sure drupal_render() does not re-order the links.
  155. $build['#sorted'] = TRUE;
  156. // Get the menu name from the last link.
  157. $item = end($items);
  158. $link = $item['original_link'];
  159. $menu_name = $link->getMenuName();
  160. // Add the theme wrapper for outer markup.
  161. // Allow menu-specific theme overrides.
  162. $build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
  163. $build['#menu_name'] = $menu_name;
  164. $build['#items'] = $items;
  165. // Set cache tag.
  166. $build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
  167. }
  168. return $build;
  169. }
  170. /**
  171. * Builds the #items property for a menu tree's renderable array.
  172. *
  173. * Helper function for ::build().
  174. *
  175. * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
  176. * A data structure representing the tree, as returned from
  177. * MenuLinkTreeInterface::load().
  178. * @param \Drupal\Core\Cache\CacheableMetadata &$tree_access_cacheability
  179. * Internal use only. The aggregated cacheability metadata for the access
  180. * results across the entire tree. Used when rendering the root level.
  181. * @param \Drupal\Core\Cache\CacheableMetadata &$tree_link_cacheability
  182. * Internal use only. The aggregated cacheability metadata for the menu
  183. * links across the entire tree. Used when rendering the root level.
  184. *
  185. * @return array
  186. * The value to use for the #items property of a renderable menu.
  187. *
  188. * @throws \DomainException
  189. */
  190. protected function buildItems(array $tree, CacheableMetadata &$tree_access_cacheability, CacheableMetadata &$tree_link_cacheability) {
  191. $items = [];
  192. foreach ($tree as $data) {
  193. /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
  194. $link = $data->link;
  195. // Generally we only deal with visible links, but just in case.
  196. if (!$link->isEnabled()) {
  197. continue;
  198. }
  199. if ($data->access !== NULL && !$data->access instanceof AccessResultInterface) {
  200. throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.');
  201. }
  202. // Gather the access cacheability of every item in the menu link tree,
  203. // including inaccessible items. This allows us to render cache the menu
  204. // tree, yet still automatically vary the rendered menu by the same cache
  205. // contexts that the access results vary by.
  206. // However, if $data->access is not an AccessResultInterface object, this
  207. // will still render the menu link, because this method does not want to
  208. // require access checking to be able to render a menu tree.
  209. if ($data->access instanceof AccessResultInterface) {
  210. $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($data->access));
  211. }
  212. // Gather the cacheability of every item in the menu link tree. Some links
  213. // may be dynamic: they may have a dynamic text (e.g. a "Hi, <user>" link
  214. // text, which would vary by 'user' cache context), or a dynamic route
  215. // name or route parameters.
  216. $tree_link_cacheability = $tree_link_cacheability->merge(CacheableMetadata::createFromObject($data->link));
  217. // Only render accessible links.
  218. if ($data->access instanceof AccessResultInterface && !$data->access->isAllowed()) {
  219. continue;
  220. }
  221. $element = [];
  222. // Set a variable for the <li> tag. Only set 'expanded' to true if the
  223. // link also has visible children within the current tree.
  224. $element['is_expanded'] = FALSE;
  225. $element['is_collapsed'] = FALSE;
  226. if ($data->hasChildren && !empty($data->subtree)) {
  227. $element['is_expanded'] = TRUE;
  228. }
  229. elseif ($data->hasChildren) {
  230. $element['is_collapsed'] = TRUE;
  231. }
  232. // Set a helper variable to indicate whether the link is in the active
  233. // trail.
  234. $element['in_active_trail'] = FALSE;
  235. if ($data->inActiveTrail) {
  236. $element['in_active_trail'] = TRUE;
  237. }
  238. // Note: links are rendered in the menu.html.twig template; and they
  239. // automatically bubble their associated cacheability metadata.
  240. $element['attributes'] = new Attribute();
  241. $element['title'] = $link->getTitle();
  242. $element['url'] = $link->getUrlObject();
  243. $element['url']->setOption('set_active_class', TRUE);
  244. $element['below'] = $data->subtree ? $this->buildItems($data->subtree, $tree_access_cacheability, $tree_link_cacheability) : [];
  245. if (isset($data->options)) {
  246. $element['url']->setOptions(NestedArray::mergeDeep($element['url']->getOptions(), $data->options));
  247. }
  248. $element['original_link'] = $link;
  249. // Index using the link's unique ID.
  250. $items[$link->getPluginId()] = $element;
  251. }
  252. return $items;
  253. }
  254. /**
  255. * {@inheritdoc}
  256. */
  257. public function maxDepth() {
  258. return $this->treeStorage->maxDepth();
  259. }
  260. /**
  261. * {@inheritdoc}
  262. */
  263. public function getSubtreeHeight($id) {
  264. return $this->treeStorage->getSubtreeHeight($id);
  265. }
  266. /**
  267. * {@inheritdoc}
  268. */
  269. public function getExpanded($menu_name, array $parents) {
  270. return $this->treeStorage->getExpanded($menu_name, $parents);
  271. }
  272. }