Render a menu tree from custom code in Drupal 8
Having menus rendered on a site in Drupal 8 is pretty simple. Most of the time this can be accomplished with site building, in a few clicks. And if you need some more advanced features on top of what the Drupal 8 core has, you can also have a look at modules like Menu Block, or have a look at this (a bit outdated) contributed modules for menus page.
However, you may have some special requirements, for example, to display some small portion of a menu inside the template of a node. There is no simple 'site building' solution for that. You most probably need to code a bit.
Let's say you have these requirements: a node which can be part of the main menu, and which could also have menu items bellow it, should display the first level of those menu items somewhere inside its template (so just as you would display a regular field).
The image above shows a very simple visual representation of what we'd like to achieve.
Now let's break this down into smaller and independent pieces. You can also skip to the entire code. Basically, you have to:
- Load and render a menu (or parts of a menu).
- Have a custom field available on the Manage display page for a content type, so that you can place it on the node page.
Let's actually start with the second part, as it is simpler.
Custom (extra) fields
There is the hook_entity_extra_fields_info hook which can be used to expose custom or extra fields on the entities. We'll use this hook to provide a custom field on a node.
The visible flag represents the default visibility setting of the field. Usually, you want this to be false, so you decide when the element is displayed and not when it is NOT displayed.
Now clear the cache and go to Structure >> Content types >> YourContentType, click on the Manage display tab and you should see your new field available there.
The next thing is to implement the hook_ENTITY_TYPE_view and particularly, in this case, hook_node_view. Here it is:
Save it, clear the cache, make sure that the field is visible in the display setting for that content type and visit a node of that type. You should see the custom text displayed.
So, we're done with the second part. Let's go and actually load the menu for this node and display the first level of menu items.
Render the menu
For this, we'll have to load a menu using a menu tree service and a set of parameters which define the root to start with, how many levels to load, and so on, then to apply a set of manipulators on the loaded tree that would check the access, do the sorting, etc, and finally to build the menu tree.
The root menu item
One of the tricky things is to get the correct root menu item. We know that the node can be part of a menu. It would be great if we could load a menu link based on the route name and some route parameters. For that, you can use the plugin.manager.menu.link service.
Menu tree parameters
When loading a menu, we have to provide some parameters. For example, what the root of the menu you want to load (if you want to load a subtree, which is our case) is, or how many levels to load. These parameters are specified using a \Drupal\Core\Menu\MenuTreeParameters object.
More details about the MenuTreeParameters can be found here.
Load the menu
To load the menu, we will use the menu.link_tree service.
Menu tree manipulators
After we have the menu loaded, it is time to actually apply some manipulators on its menu items. These manipulators will deal with access checking, sorting according to the weights, or whatever other things you want to do with the menu tree. The code looks like that:
So, in this case, we want to check for the access and sort the items. There's a default tree manipulator service in Core which is Drupal\Core\Menu\DefaultMenuLinkTreeManipulators. That already provides a few basic manipulators you can apply to a menu tree, which is enough in our case. But of course, you can add any other callable to that array and use the implementation from Core as a guideline.
Build the menu
Finally, we have to build the menu. This is as simple as:
This would replace the
$build['my_custom_menu'] = [ '#markup' => 'Some custom menu', ]
that you used as a placeholder in the custom_node_view() hook.
And that's it. The first level of navigation, having the current node as root, will be displayed inside the template of your node.
Here is the entire code: