Before we get our hands dirty with menus and menu links, let's talk a bit about the general architecture behind the menu system. To this end, I want to see its main components, what some of its key players are and what classes you should be looking at. As always, no great developer has ever relied solely on a book or documentation to figure out complex systems.
Menus
Menus are configuration entities represented by the following class: Drupal\system\Entity\Menu. I mentioned in Chapter 1, Developing for Drupal 9, that we have something called configuration entities in Drupal, which we will explore in detail later in this book. However, for now, it's enough to understand that menus can be created through the UI and become an exportable configuration. Additionally, this exported configuration can also be included inside a module so that it gets imported when the module is first installed. This way, a module can ship with its own menus. We will see how this latter aspect works when we talk about the different kinds of storage in Drupal. For now, we will work with the menus that come with Drupal core.
Each menu can have multiple menu links, structured hierarchically in a tree with a maximum depth of 9 links. The ordering of the menu links can be done easily through the UI or via the weighting of the menu links, if defined in code.
Menu links
At their most basic level, menu links are YAML-based plugins (like the Layout plugins we saw in the previous chapter). To this end, regular menu links are defined inside a module_name.links.menu.yml file and can be altered by other modules by implementing hook_menu_links_discovered_alter(). When I say regular, I mean those links that go into menus. We will see shortly that there are also a few other types.
There are a number of important classes you should check out in this architecture though: MenuLinkManager (the plugin manager) and MenuLinkBase (the menu link plugins base class that implements MenuLinkInterface).
Menu links can, however, also be content entities. The links created via the UI are stored as entities because they are considered content. The way this works is that for each created MenuLinkContent entity, a plugin derivative is created. We are getting dangerously close to advanced topics that are too early to cover. But in a nutshell, via these derivatives, it's as if a new menu link plugin is created for each MenuLinkContent entity, making the latter behave as any other menu link plugin. This is a very powerful system in Drupal.
Menu links have a number of properties, among which is a path or route. When created via the UI, the path can be external or internal or can reference an existing resource (such as a user or piece of content). When created programmatically, you'll typically use a route.
Multiple types of menu links
The menu links we've been talking about so far are the links that show up in menus. There are also a few different kinds of links that show up elsewhere but are still considered menu links and work similarly.
Local tasks
Local tasks, otherwise known as tabs, are grouped links that usually show up above the main content of a page (depending on the region where the tabs block is placed). They are usually used to group together related links that have to deal with the current page. For example, on an entity page, such as the node detail page, you can have two tabs—one for viewing the node and one for editing it (and maybe one for deleting it); in other words, local tasks:
Figure 5.1: Local tasks
Local tasks take access rules into account, so if the current user does not have access to the route of a given tab, the link is not rendered. Moreover, if that means only one link in the set remains accessible, that link doesn't get rendered as there is no point. So, for tabs, a minimum of two links are needed for them to show up.
Modules can define local task links inside a module_name.links.task.yml file, whereas other modules can alter them by implementing hook_menu_local_tasks_alter().
Local actions
Local actions are links that relate to a given route and are typically used for operations. For example, on a list page, you might have a local action link to create a new list item, which will take you to the relevant form page.
In the following screenshot, we can see a local action link used to create a new user on the main user management page:
Figure 5.2: Local actions
Modules can define local action links inside a module_name.links.action.yml file, whereas other modules can alter them by implementing hook_menu_local_actions_alter().
Contextual links
Contextual links are used by the Contextual module to provide handy links next to a given component (a render array). You probably encountered this when hovering over a block, for example, and getting that little icon with a dropdown that has the Configure block link:
Figure 5.3: Contextual links
Contextual links are tied to render arrays. In fact, any render array can show a group of contextual links that have previously been defined.
Modules can define contextual links inside a module_name.links.contextual.yml file, whereas other modules can alter them by implementing hook_contextual_links_alter().
MenuLink trees
As I mentioned in the section about menus, menu links are stored hierarchically inside a menu. This hierarchy is represented via a menu link tree. There are a number of key players here we should go over.
We have the MenuLinkTree service, which is the interface used to load and prepare the tree of a certain menu. The loading is deferred to the MenuTreeStorage service, which does so on the basis of a MenuTreeParameters object that contains metadata on certain restrictions to be applied on the menu links that are loaded. We will see some examples of this a bit later.
What comes out of the MenuLinkTree service is an array of MenuLinkTreeElement objects. These are essentially value objects that wrap the MenuLinkInterface plugins and that provide some extra data about their placement in the tree they are loaded in. One such important piece of information is the subtree (the array of MenuLinkTreeElement objects that are below it).
Menu link tree manipulators
When loading a menu link tree, you get the entire tree that fits the specified parameters. However, when using that tree, you probably want to perform some checks and remove certain items. A common example is to remove the menu links to which the user doesn't have access. This is where manipulators come into place.
The MenuLinkTree service has a transform() method that alters a tree based on an array of manipulators. The latter take the form of callables, typically service names with specific methods. So, the actual manipulators are services that traverse the tree and make alterations to the tree items, their order, and so on.
Menu active trail
A menu trail is a list (array) of menu link plugins that are parents of a menu link. For the active trail, that specific menu link represents the current route (if there is a menu link for that route).
The Drupal menu system also has a service that can be used to determine the active trail of the current route if used by a menu link. By passing a menu name to look inside of, the MenuActiveTrail service returns an array of plugin IDs of the parents all the way up to the menu root, if the current route is in fact an active link. There is also a method that can be used to check that: getActiveLink().
Now that we have covered some theory about the menu system, it's time to get our hands dirty with some code.