Getting the parent node of a Drupal 8 node

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. For example, consider this snippet of a menu from /admin/structure/menu/manage/main Screenshot of a menu admin page in Drupal 8

"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 what child node/menu item whereas menu items only have one parent.

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.

Pre-requisites

  • Functioning Drupal 8 install
  • Some Drupal 8 knowledge
  • Moderate PHP comfort.
  • Custom module or theme to add it to.

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.

Add new comment

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

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.