Drupal 9: Creating Custom Events

The events system in Drupal 9 allows for different parts of the system to communicate in an object-oriented manner. During the normal processing of a page, Drupal will trigger events that components will subscribe to in order to perform actions. The events system is built upon the Symfony event dispatcher component, which is an implementation of the mediator design pattern.

The mediator design pattern is a way for objects to communicate with each other through a mediator object (hence the name). The idea being that the different parts of the events system are never directly linked. This reduces the dependencies that objects have and allows parts of the application to be decoupled. Events are useful in a large and modular application like Drupal as it allows different parts of the application to work together without having to explicitly reference the objects involved.

The event system works by allowing different services to register their events with the event dispatcher. Then, when an event is triggered, the event dispatcher will invoke the events that are registered to it, passing information about the event to the method being called.

There are lots of different types of events in Drupal, but they all work on this same principle of subscribing to an event and being triggered at the appropriate time. To find out what sort of events are in use in Drupal you can search for the string "@Event" in the comments. By searching for this you will find the default Symfony event dispatcher events, Drupal core's additional events, and any other events defined by contributed modules.

You can also use Drush and the web profiler that comes with the Devel module to find out about the events that exist or were triggered during the page load.

A good example of an event that can be subscribed to is 'kernel.request'. This event is triggered first, right at the start of the page load, and so is a good way of doing things before other components have interacted with the page. A good example of this is issuing a redirect, which is actually the event that the Redirect module subscribes to in order to perform this feature. It can do this before any other event has done anything and keeps the redirect quick.

Implementing an event subscriber

Creating an event subscriber to listen to an event is just a case of adding a service and tagging it with 'event_subscriber'. Let's take the example of redirecting a user if they try to access a certain page.

The first thing we need to do is register our service class as an event. This is done by creating a service in the usual way, but tagging it with the name of 'event_subscriber'. We add the following code to our my_module.services.yml file in the module called My Module.

services:
  my_module.message_exit:
    class: Drupal\my_module\EventSubscriber\PermissionRedirectSubscriber
    tags:
      - {name: event_subscriber}

It is good practice to add the class to a directory called EventSubscriber and append the word 'Subscriber' to the end of the class name. This gives a clear indication as to what the class is meant to be doing.

Using services means that we can inject any other dependencies into the service that we might need to get hold of. As we actually want to be checking the role of the current user it makes sense to add in the service that allows access to this. 

services:
  my_module.message_exit:
    class: Drupal\my_module\EventSubscriber\PermissionRedirectSubscriber
    arguments: ['@current_user']
    tags:
      - {name: event_subscriber}

We are now in a position to start making the event class. This class must implement EventSubscriberInterface and this interface requires the method getSubscribedEvents() to be created. This method must return an associative array of events that the event subscriber is subscribed to and what methods to call in the class for each event. An optional weight can also be applied to the array to order the callback in relation to other subscribers of the same event. The associated array of event callbacks means that we can create different responses to different events in the same class if we need to.

In the code below we are setting up the class with the getSubscribedEvents() method and a callback method called checkRedirect(), which does nothing at the moment.

<?php

namespace Drupal\my_module\EventSubscriber;

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class PermissionRedirectSubscriber implements EventSubscriberInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $account;

  /**
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user.
   */
  public function __construct(AccountInterface $account) {
    $this->account = $account;
  }

  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['checkRedirect'];
    return $events;
  }
  
  public function checkRedirect(GetResponseEvent $event) {
  }
}

The weight component of the registration only becomes important if we wanted to ensure that this event was run before (or after) another event subscriber in the system. The redirect module also has an event subscriber that performs redirects and that has a weight of 33. In which case, if we wanted to prevent our subscriber from interfering with the redirect module then we could set the weight to 34.

$events[KernelEvents::REQUEST][] = ['checkRedirect', 34];

If you are subscribing to events then it's a good idea to take a look at other subscribers to the same event. Doing this allows you to add more weight to your own subscriber in order to avoid any conflicts.

With the components of the module in place, we can now fill in the checkRedirect method. All we need to do here is look at the current path and if the user doesn't have the role 'administrator' then we redirect the response. We do this by creating a RedirectResponse() object and setting it to be the response of the event (and by extension the page).

<?php

namespace Drupal\my_module\EventSubscriber;

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;

class PermissionRedirectSubscriber implements EventSubscriberInterface {

  public function checkRedirect(GetResponseEvent $event) {
    $routeName = RouteMatch::createFromRequest($event->getRequest())->getRouteName();

    if ($route_name == 'block.admin_display') {
      $roles = $this->account->getRoles();

      if (!in_array('administrator', $roles)) {
        $url = Url::fromRoute('entity.block_content.collection');
        $url = $url->toString();
        $response = new RedirectResponse($url);
        $event->setResponse($response);
        $event->stopPropagation();
      }
    }
  }

  // Rest of the class removed for brevity.
}

Setting the response of the event to a new RedirectResponse object will communicate that the page should be redirected. We also call the stop propagation method of the event object in order to prevent any other events from being called. This is useful if you want to make sure that nothing else can interfere with your result.

The event object passed to the subscriber method always contains the current request and response status of the page. This allows you to easily pullout information like paths, parameters or even IP addresses from the object. The response part of the event object is the current request state. In the example above we overwrite it with a redirect.

Creating your own events

Whilst listening to events is useful, it is also possible to define your own events in order to allow subscribers to respond to your events. What events you trigger is entirely up to what your module is trying to do. You can trigger events from actions that are performed in Drupal itself, but you can also add your own actions. For example, if a user submits a form that performs an action then that action could trigger an event to allow modules to plug into the action as well.

Rather than set up some custom events as an example I decided to show how a popular contributed module does things as a demonstration. The Flag module is a perfect example of this as it doesn't have a lot of events and it's easy to understand what the events are triggered by (ie, flagging or unflagging content).

To start off with we need to have a class that will act as a way to name the events. This is a class that consists of some string constants that act as event names. In the Flag module, two events are defined. One for an entity that is being flagged, and one when the entity is being unflagged. 

<?php

namespace Drupal\flag\Event;

/**
 * Contains all events thrown in the Flag module.
 */
final class FlagEvents {

  /**
   * Event ID for when an entity is flagged.
   *
   * @Event
   *
   * @var string
   */
  const ENTITY_FLAGGED = 'flag.entity_flagged';

  /**
   * Event ID for when a previously flagged entity is unflagged.
   *
   * @Event
   *
   * @var string
   */
  const ENTITY_UNFLAGGED = 'flag.entity_unflagged';

}

Using this class, if we want to name one of the events we can just use the code FlagEvents::ENTITY_FLAGGED for an event that is triggered from an entity being flagged.

In order to pass useful information to the event (in the spirit of the mediator pattern), we need to create an object we can pass to the event. All events are passed an Event object that contains the state of the request and response, so the Flag module extends this class in order to provide extra context for the functionality of the flag module.

<?php

namespace Drupal\flag\Event;

use Drupal\flag\FlaggingInterface;
use Symfony\Component\EventDispatcher\Event;

/**
 * Event for when a flagging is created.
 */
class FlaggingEvent extends Event {

  /**
   * The flagging in question.
   *
   * @var \Drupal\flag\FlaggingInterface
   */
  protected $flagging;

  /**
   * Builds a new FlaggingEvent.
   *
   * @param \Drupal\flag\FlaggingInterface $flagging
   *   The flaging.
   */
  public function __construct(FlaggingInterface $flagging) {
    $this->flagging = $flagging;
  }

  /**
   * Returns the flagging associated with the Event.
   *
   * @return \Drupal\flag\FlaggingInterface
   *   The flagging.
   */
  public function getFlagging() {
    return $this->flagging;
  }

}

The FlagEvents and FlaggingEvent classes are all we need to start triggering events for the Flag module. This can be seen in the Flagging entity when a new flag is saved to the system. An event is dispatched that informs the events system that a new entity has been flagged.

\Drupal::service('event_dispatcher')->dispatch(FlagEvents::ENTITY_FLAGGED, new FlaggingEvent($this));

The Flag module itself actually listens to its own events. This can be seen in the FlagCountManager class, which has a getSubscribedEvents() method (and is also tagged as an event_subscriber in the modules services file).

  public static function getSubscribedEvents() {
    $events = [];
    $events[FlagEvents::ENTITY_FLAGGED][] = ['incrementFlagCounts', -100];
    $events[FlagEvents::ENTITY_UNFLAGGED][] = [
      'decrementFlagCounts',
      -100,
    ];
    return $events;
  }

The incrementFlagCounts() callback above is responsible for writing the flag to the database.

What about hooks?

For a long time, Drupal has had a system called "hooks". This is where you name functions in a certain way and Drupal will find them and trigger them when certain events happen. For example, if you implement the hook hook_page_attachments() then as Drupal renders the HTML of a page it will call your hook and add any libraries you have added to the page load.

If you are getting into Drupal then the hook system can be a bit strange, especially if you are coming from an object-oriented world. They are very powerful though and should be part of your Drupal education. Hooks still exist in Drupal 9 and are still used in many different parts of the system.

The only real difference between a hook and an event is how you inform Drupal of their existence. With hooks, you simply name your function in a certain way. For events, you use a class and some configuration.

Eventually, the event and even subscriber system will replace the hook system within Drupal. There will be a time when instead of reacting to the user login event using hook_user_login() you would create an event subscriber that listens to the login event being triggered.

I will admit that right now having the two systems built into the system is a little confusing. There isn't much overlap between the two systems though as I haven't been able to find a hook that also has an event triggered. You should be using the event system for any new functionality in order to future proof yourself to this change as much as possible.

When to use the events system in Drupal 9

As you have seen above creating events and subscribing to them is quite simple. There are a few components involved, but once you understand what to look for then you can quickly start subscribing to events into your custom code. I would keep the number of events that your modules have to the minimum in order to keep things simple, but you should trigger events in your code whenever you think other modules might benefit from listening to them. In the Flag module, I looked at above the events used were minimal and easy to understand and any developer could write an event to subscribe to that event and perform custom features.

As the events system in Drupal is object-oriented, it also means you can easily unit test the functionality of events than you can with hooks. You can even mock the Event object that gets passed to the subscriber, allowing you to test the subscriber in different situations.

With the hook system eventually being replaced by the events system, you should look at creating events and subscribers as a default. A good way to allow for any future changes is to use services in all of your hooks by default. This means that when you need to migrate the code to a new event you just need to inject your service and run the same code.

Originally published here https://www.codeenigma.com/blog/events-system-in-drupal-9

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
4 + 12 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.