Drupal: Get the current entity

First things first, it’s not always possible to get the current entity, some routes will have more than 1 entity, some wont have any, drush wont give a route. But most of the time, what you want when talking about the current entity is the entity being viewed by a canonical (or other entity links) route.

Snippet

What you’re looking for is most likely this:

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatch;

/**
 * Get the route entity (defaults to current route).
 */
function currentEntity(?RouteMatch $route_match = NULL): ?EntityInterface {
  $route_match = $route_match ?? \Drupal::routeMatch();

  foreach ($route_match->getParameters() as $key => $value) {
    if (
      $value instanceof EntityInterface
      // Make sure the route path matches a link in the entity.
      && in_array($route_match->getRouteObject()->getPath(), $value->getEntityType()->getLinkTemplates())
    ) {
      return $value;
    }
  }
}

I’m not the first to wonder or write about this, check out this forum topic: Drupal 8: get entity object given system path?. And my solution is a lot like Get the entity for a route which also appears in the previous link.

The code is straight forward, it checks if the parameter is an EntityInterface, and if the entity has the route path as a link, a Node would have these /node/{node}, /node/{node}/edit, /node/{node}/delete and a bunch more depending on what modules you have.

One route two entities

The problem is some routes have more than 1 entity in the parameters, like the entity.node.revision route that takes 2 entities /node/{node}/revisions/{node_revision}/view, the first being the node, and the second being a specific revision of the node. A route could have many more parameter entities, but it seems rare to encounter more than 2.
Sometimes a route will have 1 entity, but the path is not in the entity links, this usually occurs when it’s a custom route (at least in my cases).

With multiple entities only the primary entity is needed. For this to work need some business logic, that will get us the correct entity.
Using the entity.node.revision as an example I would argue that the entity you wanted is the node_revision, but the currentEntity() function would simple return the first entity it encountered.
The same is true for an path without an entity link, we need some kind of project specific logic to handle this.

So custom business logic, that can change from project to project. I’d say there’re 3 obvious solutions for this, a plugin system, an event based system or hooks. Plugins seems overkill, and hooks are persona non grata, so at the moment let’s fix it using events (I would probably have used hooks if it was just me, it’s by far a smaller and easier implementation).

The fix

I don’t want this turning into a how to do events post, so I’ll leave a lot of the parts out, you should read the Subscribe to and dispatch events article for any background on this.
I’ve added proof of concept module in following repo https://github.com/BirkAndMe/poc-primary_entity, which I’ll link to, but I’m also going to include any code of interest in the post.

function primary_entity(?RouteMatchInterface $route_match = NULL): ?EntityInterface {
  $route_match = $route_match ?? \Drupal::routeMatch();

  /** @var EventDispatcherInterface*/
  $event_dispatcher = \Drupal::service('event_dispatcher');

  $event = new PrimaryEntityEvent($route_match);
  $event_dispatcher->dispatch($event, PrimaryEntityEvent::GET);

  return $event->hasPrimaryEntity() ? $event->getPrimaryEntity() : NULL;
}

The idea is you’ll call the primary_entity() function and it will return the current / primary EntityInterface for the given route. The primary_entity() function dispatches a PrimaryEntityEvent::GET event, and the PrimaryEntitySubscriber subscribes to the event:

  public static function getSubscribedEvents(): array {
    return [
      PrimaryEntityEvent::GET => [
        ['checkRevision', 110],
        ['checkLinkTemplates', 100]
      ],
    ];
  }

Note the PrimaryEntitySubscriber::checkRevision() has a higher priority, it needs to run first, or the PrimaryEntitySubscriber::checkLinkTemplates() function will return the entity and not the current revision when viewing revision routes.

The PrimaryEntitySubscriber::checkLinkTemplates() is doing almost the same as the currentEntity() function above, but lets take quick look at the PrimaryEntitySubscriber::checkRevision() that checks to see if the route is a revision route:

  public function checkRevision(PrimaryEntityEvent $event) {
    if (!$event->getRouteMatch()->getRouteObject()) {
      return;
    }

    $entities = $event->getEntities();
    $parameters = $event->getRouteMatch()->getRouteObject()->getOption('parameters') ?? [];

    foreach ($parameters as $key => $parameter) {
      if (
        !empty($parameter['type'])
        && strpos($parameter['type'] ?? '', 'entity_revision:') === 0
        && isset($entities[$key])
      ) {
        return $this->setPrimaryEntity($event, $entities[$key]);
      }
    }
  }

The PrimaryEntityEvent::getEntities() is a helper function that will look through the route parameters in the event, and return all route parameters of EntityInterface type.
We assume that if there’s a parameter type with entity_revision:* it’s the entity we want. I would guess this is the expected behavior in most use cases.

To override this behavior you would need to create another event subscriber, that subscribes to PrimaryEntityEvent::GET with a higher priority than 110 which is what the checkRevision() is set to. And simply add more event subscribers to add any custom business logic, you can use the following template (that will return a specific parameter depending on a specific route):

<?php

use Drupal\primary_entity\Event\PrimaryEntityEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 *
 */
class PrimaryEntityEventExample implements EventSubscriberInterface {

  /**
   *
   */
  public function checkCustomRoute(PrimaryEntityEvent $event): void {
    if ($event->getRouteMatch()->getRouteName() === 'some.routename') {
      $event
        ->setPrimaryEntity($event->getRouteMatch()->getParameter('entity_parameter_name'))
        ->stopPropagation();
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      PrimaryEntityEvent::GET => 'checkCustomRoute'
    ];
  }

}

Conclusion

Getting the current entity is a trivial problem 95% of the time, but the last 5% needs a dedicated module with custom per project logic to work.

I have a variant of the first currentEntity() function I copy from project to project. It is a bit hackish, but it made sure all my guess current entity logic was in one place, so I (and others) could quickly get an overview.

But after diving a bit deeper into it all I think I’ll include the primary entity module in our base tools, and use it as described here instead.

0
Subscribe to my newsletter

Read articles from Philip Birk-Jensen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Philip Birk-Jensen
Philip Birk-Jensen

PHP & JavaScript developer, working in Drupal.