4

Working on a headless Drupal site, and have come across an issue. We have a REST resource that returns arbitrary menus by menu-id, however they always have "active-trail" set to false on every menu item.

I'd like to be able to specify a URL or route when building the menu and have the active-trail be equivalent to what it would be if the menu was loaded on that route.

At the moment our solution is to loop through every menu item until we find one that corresponds to the provided URL, and then mark it and all its parents as "active-trail", however this seems unnecessarily cumbersome and I'm left wondering if there's a more sensible solution we've overlooked.

Currently we're loading menus like this:

$tree = $this->menuTree->load('menu_id', new MenuTreeParameters());

Where $this->menuTree is an instance of MenuLinkTree being instantiated via Dependency Injection:

/**
 * {@inheritdoc}
 */
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, MenuLinkTreeInterface $menu_tree, EntityTypeManagerInterface $entity_type_manager, Request $current_request) {
  parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);

  $this->menuTree = $menu_tree;
  $this->entityTypeManager = $entity_type_manager;
  $this->request = $current_request;
  $this->alias = $this->request->query->get('alias');
}

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $configuration,
    $plugin_id,
    $plugin_definition,
    $container->getParameter('serializer.formats'),
    $container->get('logger.factory')->get('rest'),
    $container->get('menu.link_tree'),
    $container->get('entity_type.manager'),
    $container->get('request_stack')->getCurrentRequest()
  );
}
  • 1
    What you have described is effectively what happens on a non-headless server, so it's not /that/ crazy. It's got to be done somewhere and to me it makes more sense for the front end to know the context than for the back-end to do it. But YMMV. – Alfred Armstrong May 3 '18 at 14:44
  • How are you loading the menu in your REST resource? – Beebee May 3 '18 at 15:32
  • @Beebee Sorry, I should have included that in the initial question. I've edited the question accordingly. – Drif.io May 3 '18 at 15:40
  • @Drif.io thanks, can you also post how you're initializing $this->menuTree please. – Beebee May 3 '18 at 15:50
  • @Beebee It's being initialized via dependency injection, added additional details to the question. – Drif.io May 3 '18 at 15:54
5

Inject the menu.active_trail dependency along with menu.link_tree:

private $activeTrail;

/**
 * {@inheritdoc}
 */
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $active_trail, EntityTypeManagerInterface $entity_type_manager, Request $current_request) {
  parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);

  $this->menuTree = $menu_tree;
  $this->activeTrail = $active_trail;
  $this->entityTypeManager = $entity_type_manager;
  $this->request = $current_request;
  $this->alias = $this->request->query->get('alias');
}

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $configuration,
    $plugin_id,
    $plugin_definition,
    $container->getParameter('serializer.formats'),
    $container->get('logger.factory')->get('rest'),
    $container->get('menu.link_tree'),
    $container->get('menu.active_trail'),
    $container->get('entity_type.manager'),
    $container->get('request_stack')->getCurrentRequest()
  );
}

Now instead of:

$tree = $this->menuTree->load('menu_id', new MenuTreeParameters());

get the active trail ID's just before building your menu and pass it in as part of the parameters:

// Change this to menu machine name.
$menu_id = 'menu_id';

// This should return an array. 
// Array contains menu link uuid's, keyed by the uuid's iirc. 
// For example something like:
//   'menu_link:abcdefg_123' => 'menu_link:abcdefg_123'
$activeTrailIds = $this->activeTrail->getActiveTrailIds($menu_id); 

$params = new MenuTreeParameters();
$params->setActiveTrail($activeTrailIds);
$tree = $this->menuTree->load($menu_id, $params);

This should return you a menu tree with correct active trails.

  • 1
    Didn't quite work ($this->activeTrail->getActiveTrailIds() returns active trails based on your CURRENT route) but it got me enough of the way to do what I wanted, thanks! I ended up overriding the core menu.active_trail service and added a helper method to allow passing of arbitrary routes & route parameters, and got those from a passed-in URL alias. Much appreciated :) – Drif.io May 4 '18 at 13:25
  • 1
    @Drif.io good stuff. I suspected something like that will happen if you're requesting the menu separately and injecting it in the frontend. Which is why I tried to be as descriptive as possible with comments :) Glad you got it working. – Beebee May 5 '18 at 9:33
1

I also had this case and, as Drif.io describes in his comment to Beebee's answer, I had to extend the core MenuActiveTrail class.

In my case, I solved it via Patch, but in other use cases it would probably be a better idea to extend the class and create an own service. We use the functionality via patch in a core module where we cannot create a dependency to a custom module.

Until now, the function getActiveTrailIds was looking like this:

public function getActiveTrailIds($menu_name) {
  return $this->get($menu_name);
}

I changed the function to accept an optional $route_match parameter. However because of the parameter I had to disable the caching of the active trail, because when a regular breadcrumb from the same (Main-) navigation is being shown without the route match parameter, there is another outcome.

public function getActiveTrailIds($menu_name,$route_match = null) {
  unset($this->storage[$menu_name]); //disable caching for this case.
  if ($route_match) {
    $this->routeMatch = $route_match;
  }
  return $this->get($menu_name);
}

Now I am able to call the service using

\Drupal::service('menu.active_trail')->getActiveTrailIds('main',$route_match);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.