Drupal 9: Creating A Category Menu Using Derivers

Drupal 9: Creating A Category Menu Using Derivers

14th August 2022 - 16 minutes read time

Derivers in Drupal are one of the ways in which you can inform Drupal about the presence of plugin types. This allows you to generate multiple custom types of a plugin so that it can be represented as multiple different plugins within the system.

Perhaps the most useful deriver example I have seen is the menu deriver. This allows us to use the Drupal plugin architecture to generate custom menu links.

If you want to create a menu link for your module then you would normally add them one at a time to a *.links.menu.yml file. This is an example of using the menu links plugin system to inform the menu system about the links you want to add.

For example, the core config module has a single option in the config.links.menu.yml file that adds the "Configuration synchronization" menu item to the administration menu. Here is the contents of that file.

config.sync:
  title: 'Configuration synchronization'
  description: 'Import and export your configuration.'
  route_name: config.sync
  parent: system.admin_config_development

Instead of doing this, we can use a menu deriver to tell the menu system to look at a class that will then inject menu links into the menu. This saves us from adding them one by one and also means we can dynamically create menu links without hard coding them into the *.links.menu.yml file.

In this article, I will look at setting up a menu deriver and then using that deriver to inject custom elements into the menu.

Setting Up A Menu Deriver

The first thing to do is setup a menu deriver class. This should implement a method called getDerivativeDefinitions() which will return the plugin derivatives. As we are calling this from the menu system we need to return an array of menu links from this method so that they are understood by the menu system.

As a simple example, create a module called my_module and add a class called SingleMenuLinkDeriver at the location src\Plugin\Derivative\SingleMenuLinkDeriver.php within that method.

The getDerivativeDefinitions() method in the following example will return a single link to a node on the site.

<?php

namespace Drupal\my_module\Plugin\Derivative;

use Drupal\Component\Plugin\Derivative\DeriverBase;

class SingleMenuLinkDeriver extends DeriverBase {

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    $links = [];

    $links[] = [
      'title' => 'Some title',
      'url' => 'internal:/node/666',
      'menu_name' => 'main',
    ] + $base_plugin_definition;

    return $links;
  }

}

The $base_plugin_definition that comes from the parameters of the method has a number of defaults for the menu link. We merge this with the link array definition to inject defaults into the menu link.

Since we are also defining the "menu_name" parameter as "main" we are telling the menu deriver to add this menu link to the "main" menu.

To get this noticed by the menu system we need to create a *.links.menu.yml file and add our deriver class to that definition.  Create the file in the my_module directory called my_module.links.menu.yml and add the following YAML definition to it.

my_module.single.links:
    deriver: '\Drupal\my_module\Plugin\Derivative\SingleMenuLinkDeriver'
    class: '\Drupal\Core\Menu\MenuLinkDefault'

In this file, we are defining the deriver class and the individual class used for each menu item. The deriver class just points to the SingleMenuLinkDeriver class that we created above.

The class, in this case, is just a reference to the original menu class at \Drupal\Core\Menu\MenuLinkDefault. We could have created a custom class for this, but since we aren't doing anything custom with the link we can just use the default class and that will handle the menu item for us.

When we enable the module and clear the caches the main menu will contain a new link.

Creating A Category Menu

Let's do something more useful with this menu deriver rather than just add a single link. Using this deriver technique it is possible to load in entities and display them automatically as links. This means we can create a menu system based on the list of terms from a vocabulary.

First we need to change the deriver property in the my_module.links.menu.yml file to look at a different deriver class.

my_module.category.links:
    deriver: '\Drupal\my_module\Plugin\Derivative\CategoryMenuLinkDeriver'
    class: '\Drupal\Core\Menu\MenuLinkDefault'

The CategoryMenuLinkDeriver here is somewhat more complex than the one above, but what we are essentially doing is injecting the entity_type.manager service into the class and using this to create a list of taxonomy terms links that belong to the "category" vocabulary.

<?php

namespace Drupal\my_module\Plugin\Derivative;

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;

class CategoryMenuLinkDeriver extends DeriverBase implements ContainerDeriverInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;

  /**
   * CategoryMenuLinkDeriver constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManager $entityTypeManager) {
    $this->entityTypeManager = $entityTypeManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('entity_type.manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    $links = [];

    $terms = $this->entityTypeManager
      ->getStorage('taxonomy_term')
      ->loadTree('category', 0, 2, TRUE);

    foreach ($terms as $term) {
      // Get URL of object.
      $term_url = Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]);

      // Create menu ID.
      $menuId = 'my_module.category.links:' . $term->id();

      // Create link along with default settings.
      $link = [
        'id' => $menuId,
        'route_name' => $term_url->getRouteName(),
        'route_parameters' => $term_url->getRouteParameters(),
        'title' => $term->getName(),
        'weight' => $term->getWeight(),
        'menu_name' => 'main',
      ] + $base_plugin_definition;

      if ($term->parents[0] != 0) {
        // If the parent is not 0 then set the parent attribute of the link.
        $link['parent'] = 'my_module.category.links:my_module.category.links:' . $term->parents[0];
      }

      // Add link to the bank of links we will return.
      $links[$menuId] = $link;
    }

    return $links;
  }

}

Note the use of the "parent" attribute in the above code. By using this attribute we can avoid having to think about the hierarchical nature of the links. Instead, we inform the menu system of the menu hierarchy and the menu handling system handles everything else for us. This also means that the menu is then rendered as a hierarchy as well.

After clearing the Drupal caches, when we load the menu with the name "main" it will contain all of the links to the taxonomy terms in the category menu.

Clearing Caches?

You might find that if you change or add to the taxonomy list then the menu won't be updated. This is because menu links are heavily cached and that caching is preventing your changes from being picked up by the menu system. The cache being used here is not the taxonomy cache and so there is no connection between the menu cache and the taxonomy cache.

In fact, the only way around this (that I can see) is to add some hooks to your my_module.module file to invalidate the menu cache and rebuild the menu links every time a category term is updated.

The following code shows an implementation of the hook_ENTITY_TYPE_update() hook to trigger the rebuilding of the menu if the taxonomy term updated belongs to the category bundle.

<?php
use \Drupal\Core\Entity\EntityInterface;

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function my_module_taxonomy_term_update(EntityInterface $entity) {
  if ($entity->bundle() == 'category') {
    // Clear the menu cache.
    \Drupal::cache('menu')->invalidateAll();
    // Rebuild the menu links.
    \Drupal::service('plugin.manager.menu.link')->rebuild();
  }
}

This might seem like a bit of a drastic approach, but the information about the menu links created from the deriver class isn't very well cached. We therefore need to invalidate the entire menu system in order to ensure our derived link is regenerated.

For this reason, if you do go down this route, you should probably select a vocabulary that isn't updated very often as this might cause a performance problem if your site has to regenerate the menu system all the time.

Link Attributes

The array of items that are returned from the getDerivativeDefinitions() method can have a number of different properties. These properties can be found in the class MenuLinkManager and are reproduced here for reference.

  • menu_name - Required. The name of the menu for this link.
  • route_name - Required. The name of the route this links to. If the route is external then the "url" parameter should be used instead of this.
  • route_parameters - Parameters for route variables when generating a link.
  • url - The external URL if this link has one (required if route_name is empty). You can also link to internal paths by prefixing the link with "internal:/".
  • title - The static title for the menu link.
  • description - The description.
  • parent - The plugin ID of the parent link (or NULL for a top-level link).
  • weight - The weight of the link.
  • options - The link options to inject into the link and can be used to add HTML attributes. Defaults to an empty array.
  • expanded - If the link is expanded or not. Defaults to 0 (i.e. not expanded).
  • enabled - If the link is enabled or not. Defaults to 1 (i.e. enabled).
  • provider - The name of the module providing this link.
  • metadata - The metadata for the link. Defaults to an empty array.
  • class - Class for the menu link. Defaults to Drupal\Core\Menu\MenuLinkDefault, but can be set through the *.links.menu.yml file.
  • form_class - The configuration form used to configure the menu link. Defaults to Drupal\Core\Menu\Form\MenuLinkDefaultForm, but can be set through the *.links.menu.yml file.
  • id - The plugin ID. If the link is loaded from the YAML file then this is set to the top-level YAML key.

When creating the link in the deriver class any of the above parameters can be added to the menu links to configure the links in different ways.

Some Examples Of Menu Derivers

Here are some examples of menu derivers in use in core and in contributed modules.

Core Custom Menu Links

The menu system allows for users to enter their own links into the menu system though the menu_link_content module which is available in core. Any custom links that are added are stored as 'menu_link_content' entities so a custom deriver is defined in order to allow these entities to be injected into the menu system.

Here is the menu_link_content.links.menu.yml file that defines the deriver.

menu_link_content:
  class: \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent
  form_class: \Drupal\menu_link_content\Form\MenuLinkContentForm
  deriver: \Drupal\menu_link_content\Plugin\Deriver\MenuLinkContentDeriver

The MenuLinkContentDeriver class loads in all of the available menu_link_content entities and injects them as plugin definitions into the menu system.

Views Menu Links

During the rebuilding of the menu system the Views module will use a menu deriver to inject menu links into the menu system. This is how Views generates the different menu links that it needs.

The core if this is the views.links.menu.yml file where the Views module defines the deriver it users to add the menu links.

views_view:
  class: Drupal\views\Plugin\Menu\ViewsMenuLink
  form_class: Drupal\views\Plugin\Menu\Form\ViewsMenuLinkForm
  deriver: \Drupal\views\Plugin\Derivative\ViewsMenuLink

The ViewsMenuLink deriver class loads every View that has a menu link and finds the available menu links from any displays the View has.

The menu link class defined in the views.link.menu.yml file provides a link between the View display and the menu system. This allows the menu system to find the title, description, if the menu is expanded and a few other items of meta data. 

Taxonomy Menu

The category menu list functionality that I talk about above is actually provided by a module called Taxonomy Menu. This module works by creating an entity that stores the connection between the menu and the taxonomy term, which allows for finer controls over things like cache invalidation, ordering and hierarchy.

Just like the other examples here, the Taxonomy Menu module defines a taxonomy_menu.links.menu.yml file and adds a deriver to this file.

taxonomy_menu.menu_link:
  class: Drupal\taxonomy_menu\Plugin\Menu\TaxonomyMenuMenuLink
  deriver: Drupal\taxonomy_menu\Plugin\Derivative\TaxonomyMenuMenuLink

In this case, the TaxonomyMenuLink deriver loads in the instances of the taxonomy_menu entity and uses these entities to generate the links that need to be added to the menu system.

If you want to inject vocabularies into the menu system then I recommend using that module over copying my code from above. The code written above is for used to show how a custom deriver class could be used to inject taxonomy terms into a menu system and the Taxonomy Menu module handles quite a few edge cases much better than the example here.

Conclusion

Using menu derivers you can inject any dynamic menu links into any menu on your Drupal site. Whilst the examples above detail generating a menu filled with categories, there is no reason why this technique can't be applied to any other entity you want to show in a menu.

There are some challenges to get around with regards to entity caches, but they are easily worked around depending on what sort of entity you are adding into the menu.

Comments

Permalink

Thanks for writing. Your blog posts are always very informative.

Anonymous (Wed, 08/17/2022 - 17:22)

Add new comment

The content of this field is kept private and will not be shown publicly.