At a Glance
Language
PHP
Topic
Backend Drupal
Tags
Drupal
Summary

There are some circumstances that you want to get data from the node one level up a menu from your current node. However, doing that is not self-explanatory since there isn't actually any direct relationship between the nodes. I'll be referring to the nodes as "parent" and "child" for simplicity despite this.

Prerequisites
  • Functioning Drupal 8+ install
  • Some Drupal 8+ knowledge
  • Moderate PHP comfort.
  • Custom module or theme

Consider this snippet of a menu from /admin/structure/menu/manage/main

Image
Drupal 8+ edit menu view showign 5 menu items - Home and Goats on the top level and then 3 items as child items for Goats.

"Goats" would link to the "parent" node, while "Our Policies" would link to the "child" node.

My personal use case was getting a hero image from the parent node if the child node didn't have one. But it could be applied, to get any data from the parent. You could also, reverse the process and get data from the child although then you would have to filter it to which child node/menu item since menu items only have one parent but can have multiple children.

This basic technique can be done from either a module or a theme although depending on the hook you need it in, you may need to use a module - but most hooks you'd want to use it in work in themes. You could also use it in OOP based code in a module although then you'd want to inject some of the services. For the purpose of this tutorial, we'll presume you've created a new theme - if you don't know how to do that, see Creating Drupal a 8 sub-theme.

Setup

Since Drupal 8 uses namespaces and autoloading (because they make so much easier), you'll have to add a use statement to the top of the file:

use Drupal\node\Entity\Node; 

Then you have to decide what hook you need to use it in. This is mostly based on what you are doing with this data. In my case, I used template_preprocess_region() however, you can use any hook - or class. A frequently ideal choice, would probably be template_preprocess_node() - it also means the node is guaranteed to be available.

With whatever hook we're using, the first thing we need to do is get the current node's id. If using template_preprocess_node, that'd look like:

function MY_THEME_preprocess_node($var) {
  $node_id = $var['node']->id();
  ...

For most other hooks, you'll have to get the node via the ::request() service. There is one important hangup here that although easy to avoid you have to know: there are cases when the request includes a node ID instead of a loaded node object. So if you then try to do operations with this "object" you'll get a fatal error. So in that case, your code would look like:

function MY_THEME_preprocess_region($var) {
  // This will only be true if it is a node route.
  if ($node = \Drupal::request()->attributes->get('node')) {
    if (is_string($node)) {
      $node_id = $node;
    }
    else {
      $node_id = $node->id();
    }
    ...

Now we get to the part that is least straight forward. Nodes don't actually have a direct relationship with Menu Links. However, the menu link manager service will get links that match various filters. So next, we get the menu link manager service, get the relevant link, then get the parent menu item. Finally from the parent menu item, if it refers to a node, we get that node. Continuing that function, next we add:

 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
  $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
  $links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $node_id]);

  // Because loadLinksByRoute() returns an array keyed by a complex id
  // it is simplest to just get the first result by using array_pop().
  /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
  $link = array_pop($links);

  // Now check if the menu has a parent menu item and if so load it.
  /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */
  if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) {
  
    // Finally, we figure out if the parent menu item refers to another node
    // and if so, load it.
    $route = $parent->getUrlObject()->getRouteParameters();
    if (isset($route['node']) && $parent_node = Node::load($route['node'])) {
      // We now have a fully loaded node in the $parent_node variable and can
      // get whatever data we need from it.
    }
  }

Straight forward code

That code above is extra commented to explain what is going on and split into chunks that are hard to copy and past. So here is the completed function if you're using template_preprocess_region.

/**
 * Implements template_preprocess_region().
 */
function MY_THEME_preprocess_region($var) {
  if ($node = \Drupal::request()->attributes->get('node')) {
    if (is_string($node)) {
      $node_id = $node;
    }
    else {
      $node_id = $node->id();
    }
    
    /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
    $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
    $links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $node_id]);

    /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
    $link = array_pop($links);

    /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */
    if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) {
      $route = $parent->getUrlObject()->getRouteParameters();
      if (isset($route['node']) && $parent_node = Node::load($route['node'])) {
        // Do something with teh $parent_node here.
      }
    }
  }
}

Any questions? Something unclear? Something you disagree with? Leave a comment below.