Drupal 8: Set menu active trail based on type of currently viewed node

One of the Drupal Contrib modules I used on almost every Drupal 7 site I built is Menu Position. My general use case was: I have a Views view, listing teasers of some content type, for instance "blog", and I want the menu link belonging to this view to be active when viewing a blog node (without having to create a menu item for all blog nodes). With the Menu Position module I could easily implement this with a few clicks.

There is, however, no Drupal 8 version of the Menu Position module (yet), just an issue on the Drupal 8 Contrib Porting Tracker. Since I couldn't wait for a D8 port to be available, I implemented the functionality I needed in a custom module.

My starting point was the menu.active_trail service. In order to override this service, I had to extend the MenuActiveTrail class and tell Drupal to use my implementation instead of the default one. From there, I could return the plugin ID of the menu item that I wanted to be active when my condition was met. The plugin ID can be found in the menu_tree table id column, or - much better - retrieved from the node type's menu settings (idea from Miguel Fonseca).

Extend the MenuActiveTrail class

src/CustomMenuActiveTrail.php

Override the getActiveLink method.

/**
 * @file
 * Contains Drupal\custom\CustomMenuActiveTrail.
 */

namespace Drupal\custom;

use Drupal\Core\Menu\MenuActiveTrail;
use Drupal\node\Entity\NodeType;
use Drupal\node\NodeInterface;

/**
 * Extend the MenuActiveTrail class.
 */
class CustomMenuActiveTrail extends MenuActiveTrail {

  /**
   * {@inheritdoc}
   */
  public function getActiveLink($menu_name = NULL) {
    // Call the parent method to implement the default behavior.
    $found = parent::getActiveLink($menu_name);

    // If a node is displayed, load the default parent menu item
    // from the node type's menu settings and return it instead 
    // of the default one.
    $node = $this->routeMatch->getParameter('node');

    if ($node instanceof NodeInterface) {
      $bundle = $node->bundle();
      $parent = NodeType::load($bundle)->getThirdPartySetting('menu_ui', 'parent');
      $plugin_id = substr($parent, strpos($parent, ':') + 1);

      if (!empty($plugin_id)) {
        try {
          $found = $this->menuLinkManager->createInstance($plugin_id);
        }
        catch (\Exception $e) {
          watchdog_exception('custom', $e);
        }
      }
    }

    return $found;
  }
}

Use my implementation instead of the default one

src/CustomServiceProvider.php

Replace the default class. This documentation page provides detailed information about altering existing services.

/**
 * @file
 * Contains Drupal\custom\CustomServiceProvider.
 */

namespace Drupal\custom;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

/**
 * Alter the service container to use a custom class.
 */
class CustomServiceProvider extends ServiceProviderBase {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    $definition = $container->getDefinition('menu.active_trail');

    // Use CustomMenuActiveTrail class instead of the
    // default MenuActiveTrail class.
    $definition->setClass('Drupal\custom\CustomMenuActiveTrail');
  }
}