Profile picture for user admin
Daniel Sipos
05 Jun 2017

Drupal 8 has become much more flexible for doing pretty much everything. In this article I want to talk a bit about menu links and show you how powerful the new system is compared to Drupal 7.

In Drupal 7, menu links were a thing of their own with an API that you can use to create them programatically and put them in a menu. So if you wanted to deploy a menu link in code, you’d have to write an update hook and create the link programatically. All in a day’s…

We have much more control in Drupal 8. First, it has become significantly easier to do the same thing. Menu links are now plugins discovered from YAML files. So for example, to define a link in code, all you need is place the following inside a my_module.links.menu.yml file:

my_module.link_name:
  title: 'This is my link'
  description: 'See some stuff on this page.'
  route_name: my_module.route_it_points_to
  parent: my_module.optional_parent_link_name_it_belongs_under
  menu_name: the_menu_name_we_want_it_in
  weight: -1	

And that’s it. If you specify a parent link which is in a menu, you no longer even need to specify the menu name. So clearing the cache will get this menu link created and added to your menu. And even more, removing this code will remove your menu link from the menu. With D7 you need another update hook to clear that link.

Second, you can do far more powerful things than this. In the example above, we know the route name and have hardcoded it there. But what if we don’t yet and have to grab it from somewhere dynamically. That is where plugin derivatives come into play. For more information about what these are and how they work, do check out my previous article on the matter.

So let’s see an example of how we can define menu links dynamically. First, let’s head back to our *.links.menu.yml file and add our derivative declaration and then explain what we are doing:

my_module.product_link:
  class: Drupal\my_module\Plugin\Menu\ProductMenuLink
  deriver: Drupal\my_module\Plugin\Derivative\ProductMenuLink
  menu_name: product

First of all, we want to create dynamically a menu link inside the product menu for all the products on our site. Let’s say those are entities.

There are two main things we need to define for our dynamic menu links: the class they use and the deriver class responsible for creating a menu link derivative for each product. Additionally, we can add here in the YAML file all the static information that will be common for all these links. In this case, the menu name they’ll be in is the same for all we might as well just add it here.

Next, we need to write those two classes. The first would typically go in the Plugin/Menu namespace of our module and can look as simple as this:

namespace Drupal\my_module\Plugin\Menu;

use Drupal\Core\Menu\MenuLinkDefault;

/**
 * Represents a menu link for a single Product.
 */
class ProductMenuLink extends MenuLinkDefault {}

We don’t even need to have any specific functionality in our class if we don’t need it. We can extend the MenuLinkDefault class which will contain all that is needed for the default interaction with menu links — and more important, implement the MenuLinkInterface which is required. But if we need to work with these programatically a lot, we can add some helper methods to access plugin information.

Next, we can write our deriver class that goes in the Plugin/Derivative namespace of our module:

<?php

namespace Drupal\my_module\Plugin\Derivative;

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

/**
 * Derivative class that provides the menu links for the Products.
 */
class ProductMenuLink extends DeriverBase implements ContainerDeriverInterface {

   /**
   * @var EntityTypeManagerInterface $entityTypeManager.
   */
  protected $entityTypeManager;

  /**
   * Creates a ProductMenuLink instance.
   *
   * @param $base_plugin_id
   * @param EntityTypeManagerInterface $entity_type_manager
   */
  public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }

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

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

    // We assume we don't have too many...
    $products = $this->entityTypeManager->getStorage('product')->loadMultiple();
    foreach ($products as $id => $product) {
      $links[$id] = [
        'title' => $product->label(),
        'route_name' => $product->toUrl()->getRouteName(),
        'route_parameters' => ['product' => $product->id()]
      ] + $base_plugin_definition;
    }

    return $links;
  }
}

This is where most of the logic happens. First, we implement the ContainerDeriverInterface so that we can expose this class to the container and inject the Entity Type Manager. You can see the create() method signature is a bit different than you are used to. Second, we implement the getDerivativeDefinitions() method to return an array of plugin definitions based on the master definition (the one found in the YAML file). To this end, we load all our products and create the array of definitions.

Some things to note about this array of definitions. The keys of this array are the ID of the derivative, which in our case will match the Product IDs. However, the menu link IDs themselves will be made up of the following construct [my_module].product_link:[product-id]. That is the name of the link we set in the YAML file + the derivative ID, separated by a colon.

The route name we add to the derivative is the canonical route of the product entity. And because this route is dynamic (has arguments) we absolutely must also have the route_parameters key where we add the necessary parameters for building this route. Had the route been static, no route params would have been necessary.

Finally, each definition is made up of what we specify here + the base plugin definition for the link (which actually includes also all the things we added in the YAML file). If we need to interact programatically with these links and read some basic information about the products themselves, we can use the options key and store that data. This can then be read by helper methods in the Drupal\my_module\Plugin\Menu\ProductMenuLink class.

And that’s it. Now if we clear the cache, all our products are in the menu. If we create another product, it’s getting added to the menu (once the caches are cleared).

Bonus

You know how you can define action links and local tasks (tabs) in the same way as menu link? In their respective YAML files? Well the same applies for the derivatives. So using this same technique, you can define local actions and tasks dynamically. The difference is that you will have a different class to extend for representing the links. For local tasks it is LocalTaskDefault and for local actions it is LocalActionDefault.

Summary

In this article we saw how we can dynamically create menu links in Drupal 8 using derivatives. In doing so, we also got a brief refresher on how derivatives work. This is a very powerful subsystem of the Plugin API which hides a lot of powerful functionality. You just gotta dig it out and use it.

Profile picture for user admin

Daniel Sipos

CEO @ Web Omelette

Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.

Contact us

Comments

Jon Stevens 21 Jun 2017 21:28

Can you nest menu items this way?

Great article, thank you! Would it be possible to nest menu items? Lets say your products had a category grouping, so you wanted to create a menu link for each product category and then each product underneath that. And if so, how would you specify the parent/children?

Anonymous 11 Jul 2017 20:00

In reply to by Jon Stevens (not verified)

It would be something like

It would be something like this :

        $links['parent_link'] = [
          'title' => 'Parent link',
          'menu_name' => 'main',
          'route_name' => 'xxxxxxxx,
        ] + $base_plugin_definition;

        $links['sub_link'] = [
          'title' => 'Sub Link',
          'parent' => 'my_module:parent_link',
          'menu_name' => 'main',
          'route_name' => 'xxxxxx',
        ] + $base_plugin_definition;
Gordon Heydon 02 Sep 2020 16:58

In reply to by Mattias Andersson (not verified)

Format of the parent

When creating menus from a derivative the id is altered, and not the id you set in the links.

So if the derivative sets the id to menu_example_1 this will get concatenated with the id from the yaml file. So if you create a menu item with a parent which is created by a derivative then it will not the id the code added, but concatenated value like the example above.

Munish Kumar 16 Nov 2017 08:00

foreach ($products as $id =>

foreach ($products as $id => $product) {
      $links[$id] = [
        'title' => $product->label(),
        'route_name' => $product->toUrl()->getRouteName(),
        'route_parameters' => ['product' => $product->id()]
      ] + $base_plugin_definition;
    }

In the above code how can we add external menu link?

Michael Parisi 21 Dec 2017 21:07

Paths

Can you give the full paths and filenames for the examples. It would help.

J Norton 12 Jan 2018 18:22

Cache contexts

Great tutorial! Is there a way of adding cache contexts or cache tags with this approach?

Dmitry 02 May 2018 21:56

The problem is that it seems

The problem is that it seems to be impossible to set the dynamic link as a parent.
Because the id of each dynamic links would be the same like:

my_module.product_link

So how the system would know which the parent exactly?
I did different combinations but nothing works.
If someone knows how to do it, please explain.

Jack 23 May 2018 13:02

Custom plugins menu doesn't work

I have tried the above code. Loaded my custom plugins. However, they are not listed in the menu item. Any thoughts?

anonym-developer 15 Aug 2018 09:45

add permission or role access

Any ideas how we could restrict access to some links? this doesnt work and I wonder why:

'access' => FALSE,
Christian Kipke 07 Dec 2018 02:54

Good

Thx for your post. Everything is good...

... there is one thing I don't understand. On local development I disabled all caching mechanisms... But I have to clear the cache everytime my foreach array changed ... Do you have an additional post to avoid this?

Best regards,

Christian

Anonymous 15 Mar 2019 18:54

In reply to by Christian Kipke (not verified)

It's that cache you can't

It's that cache you can't disable if you ask which where is that cache - I don't know

Michael 10 Oct 2019 18:33

Unnamed routes?

Thanks for posting this. I haven't investigated the internal structure of these classes yet, so I'm a bit perplexed why there are the two, especially when one doesn't any any custom code.
What I'm really curious about is whether this method can be used for routes that don't have names. E.g., I have used the above to generate a set of links to hit a custom endpoint, which has a name, varying a parameter based on internal logic. I now want to do something similar, but create a set of links to specific nodes based on internal logic. Unless there's some hidden name (I've found route names in the menu_tree table of the database.) to use I'm stuck.

yogendra 17 Aug 2020 15:24

Nice article

Thanks for the article, it helped me and saved my effort.

Salvador 12 Feb 2021 02:15

Get URL param

Why Cant get the route parameters from a url argument?

$route_match = \Drupal::service('current_route_match');
$year = $route_match->getParameter('year');

$links[] = [
      'title' => 'xxxx',
      'route_name' => 'xxxx',
      'route_parameters' => ['year' => $year]
    ] + $base_plugin_definition;

Thanks!

Add new comment