Drupal 8: Hooks, Events, and Event Subscribers

Many modern complex systems are built with a robust event system. If you’re new to dealing with event based architectures know that an event system is made up of a few key components:

  • Event Subscribers – Sometimes called Listeners, are callable methods or functions that react to an event being propagated throughout the Event Registry.
  • Event Registry – Where event subscribers are collected and sorted.
  • Event Dispatcher – The mechanism in which and event is triggered, or “dispatched”, throughout the system.
  • Event Context – Many events require specific set of data that is important to the subscribers to an event. This can be as simple as a value passed to the Event Subscriber, or as complex as a specially created class that contains the relevant data.

For most of its existence Drupal has had a rudimentary events system by the way of “hooks“. Let’s look at how the concept of “hooks” breaks down into these 4 elements of an event system.

Drupal Hooks

  • Event Subscribers – Drupal hooks are registered in the system by defining a function with a specific name. For example, if you want to subscribe to the made up “hook_my_event_name” event, you must define a new function named myprefix_my_event_name(), where “myprefix” is the name of your module or theme.
  • Event Registry – Drupal hooks are stored in the “cache_boostrap” bin under the id “module_implements“. This is simply an array of modules that implement a hook, keyed by the name of the hook itself.
  • Event Dispatcher – Hooks are dispatched through different ways in Drupal 7 vs Drupal 8:
    • module_invoke_all() method in Drupal 7-
    • \Drupal::moduleHandler()->invokeAll() service method in Drupal 8.
  • Event Context – Context is passed into hooks by way of parameters to the subscriber. For example this dispatch would execute all “hook_my_event_name” implementations and pass in the parameter of $some_arbitrary_parameter:
    • Drupal 7:
      module_invoke_all('my_event_name', $some_arbitrary_parameter);
    • Drupal 8: 
      \Drupal::moduleHandler()->invokeAll('my_event_name', [$some_arbitrary_parameter]);

This simple system has gotten Drupal this far, but some obvious drawbacks to this approach are:

  • Only registers events during cache rebuilds.
    Generally speaking, Drupal only looks for new hooks when certain caches are built. This means that if you want to implement a new hook on your site, you will have to rebuild various caches depending on the hook you’re implementing.
  • Can only react to each event once per module.
    Since these events are implemented by defining very specific function names, there can only ever be one implementation of an event per module or theme. This is an arbitrary limitation when compared to other event systems.
  • Can not easily determine the order of events.
    Drupal determines the order of event subscribers by the order modules are weighted within the greater system. Drupal modules and themes all have a “weight” within the system. This “weight” determines the order modules are loaded, and therefore the order events are dispatched to their subscribers. A work around for this problem was added late into Drupal 7 by way of “hook_module_implements_alter“, a second event your module must subscribe to if you want to change the order of your hook execution without changing your module’s weight.

With the foundation of Symfony in Drupal 8, there is now another events system in play. A better events system in most ways. While there are not a lot of events dispatched in Drupal 8 core, plenty of modules have started making use of this system.

Drupal 8 Events

Drupal 8 events are very much Symfony events. Let’s take a look at how this breaks down into our list of event system components.

  • Event Subscribers – A class that implements the \Symfony\Component\EventDispatcher\EventSubscriberInterface.
  • Event Dispatcher – A class that implements \Symfony\Component\EventDispatcher\EventDispatcherInterface. Generally at least one instance of the Event Dispatcher is provided as a service to the system.
  • Event Registry – The registry for subscribers is stored within an Event Dispatcher object as an array keyed by the event name and the event priority (order). If you’re registering Events as a service, then that Event Subscriber will be registered within the globally provided services: \Drupal::service('event_dispatcher');
  • Event Context – A class that extends the \Symfony\Component\EventDispatcher\Event class. Generally each extension that dispatches its own event will create a new type of Event class that contains the relevant data event subscribers need.

Learning to use Drupal 8 events will help you understand more about developing with custom modules, and will prepare you for a future where events will (hopefully) replace hooks. So let’s create a custom module that shows how to leverage each of these event components in Drupal 8.

My First Drupal 8 Event Subscriber

Let’s create our first event subscriber in Drupal 8 using some core provided events. I personally like to do something very simple to start, so we’re going to create an event subscriber that shows the user a message when a Config object is saved or deleted.

First thing we need is a module where we’re going to do our work. I’ve named mine custom_events.

name: Custom Events
type: module
description: Custom/Example event work.
core_version_requirement: ^8.8 || ^9
package: Custom

Next step, we want to register a new event subscriber with Drupal. To do this we need to create custom_events.services.yml. If you’re coming from Drupal7- and are more familiar with the hooks system, then you can think of this step as the same as writing a “hook_my_event_name” function in your module or theme.

services:
  # Subscriber to the config events, with dependencies injected.
  # Name of this service.
  my_config_events_subscriber_with_di:
    # Event subscriber class that will listen for the events.
    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriberWithDI'
    # Inject services as "arguments"
    arguments:

That’s pretty simple, but let’s break it down a little bit.

  1. We define a new service named “my_config_events_subscriber
  2. We set its “class” property to the the global name of a new PHP class that we will create.
  3. We define the “tags” property, and provide a tag named “event_subscriber”. This is how the service is registered as an event subscriber with the globally available dispatcher.

Now we need to write the event subscriber class. There are a few requirements for this class we want to make sure we do:

  1. Should implement the EventSubscriberInterface interface.
  2. Must have a getSubscribedEvents() method that returns an array. The keys of the array will be the event names you want to subscribe to, and the values of those keys are a method name on this event subscriber object.

Here is our event subscriber class. It subscribes to events on the ConfigEvents class, and executes a local method for each event.

That’s it! Let’s walk through this and hit the important notes:

  • Our new class implements the EventSubscriberInterface interface.
  • We implement the getSubscribedEvents() method. That method returns an array of event name => method name key/value pairs. The method names “configSave” and “configDelete” are completely made up. These could be anything you want to call the method.
  • In the both the configSave() and configDelete() we expect an object of the ConfigCrudEvent type. That object has a the method getConfig() which returns the Config object for this event.

And a few questions that might come up to the astute observer:

  • What is ConfigEvents::SAVE and where did it come from?
    It’s a common practice when defining new events that you create a globally available constant whose value is the name of the event. In this case, \Drupal\Core\Config\ConfigEvents has a constant SAVE and its value is 'config.save'.
  • Why did we expect a ConfigCrudEvent object, and how did we know that?
    It is also common practice when defining new events that you create a new type of object that is special to your event, contains the data needed, and has a simple api for that data. At the moment, we’re best able to determine the event object expected by exploring the code base and the public api documentation.

I think we’re ready to enable the module and test this event. What we expect to happen is that whenever a config object is saved or delete by Drupal, we should see a message that contains the config object’s name.

Since config objects are so prevalent in Drupal 8, this is a pretty easy thing to try. Most modules manage their settings with config objects, so we should be able to just install and uninstall a module and see what config objects they save during installation and delete during uninstallation.

  1. Install “custom_events” module on its own.
  2. Install “statistics” module.
    Message after install of statistics module.
    Message after install of statistics module.

    Looks like two config objects were saved! The first is the core.extension config object, which manages installed modules and themes. Next is the statistics.settings config object.

  3. Uninstall “statistics” module.
    Message after uninstall of statistics module.
    Message after uninstall of statistics module.

    This time we see both the SAVE and DELETE events fired. We can see that the statistics.settings config object has been deleted, and the core.extension config object was saved.

I’d call that a success! We have successfully subscribed to two Drupal core events.

Now let’s look at how to create your own events and dispatch them for other modules to use.

My First Drupal 8 Event and Event Dispatch

First thing we need to decide is what type of event we’re going to dispatch and when we’re going to dispatch it. We’re going to create an event for a Drupal hook that does not yet have an event in core “hook_user_login“.

Let’s start by creating a new class that extends Event, we’ll call the new class UserLoginEvent. Let’s also make sure we provide a globally available event name for subscribers.

<?php

namespace Drupal\custom_events\Event;

use Drupal\user\UserInterface;
use Symfony\Component\EventDispatcher\Event;

/**
 * Event that is fired when a user logs in.
 */
class UserLoginEvent extends Event {

  const EVENT_NAME = 'custom_events_user_login';

  /**
   * The user account.
   *
   * @var \Drupal\user\UserInterface
   */
  public $account;

  /**
   * Constructs the object.
   *
   * @param \Drupal\user\UserInterface $account
   *   The account of the user logged in.
   */
  public function __construct(UserInterface $account) {
    $this->account = $account;
  }

}
  • UserLoginEvent::EVENT_NAME is a constant with the value of 'custom_events_user_login'. This is the name of our new custom event.
  • The constructor for this event expects a UserInterface object and stores it as a property on the event. This will make the $account object available to subscribers of this event.

And that’s it!

Now we just need to dispatch our new event. We’re going to do this during “hook_user_login“. Start by creating custom_events.module.

<?php

/**
 * @file
 * Contains custom_events.module.
 */

use Drupal\custom_events\Event\UserLoginEvent;

/**
 * Implements hook_user_login().
 */
function custom_events_user_login($account) {
  // Instantiate our event.
  $event = new UserLoginEvent($account);

  // Get the event_dispatcher server and dispatch the event.
  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher->dispatch($event, UserLoginEvent::EVENT_NAME);
}

Inside of our “hook_user_login” implementation, we only need to do a few things to dispatch our new event:

  1. Instantiate a new custom object named UserLoginEvent and provide its constructor the $account object available within the hook.
  2. Get the event_dispatcher service.
  3. Execute the dispatch() method on the event_dispatcher service. Provide the name of the event we’re dispatching (UserLoginEvent::EVENT_NAME), and the event object we just created ($event).

There we have it! We are now dispatching our custom event when a user is logged into Drupal.

Next up, let’s complete our example by creating an event subscriber for our new event. First we need to update our services.yml file to include the event subscriber we will write.

services:
  # Subscriber to the config events, with dependencies injected.
  # Name of this service.
  my_config_events_subscriber_with_di:
    # Event subscriber class that will listen for the events.
    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriberWithDI'
    # Inject services as "arguments"
    arguments:
      - '@messenger'
    # Tagged as an event_subscriber to register this subscriber with the event_dispatch service.
    tags:
      - { name: 'event_subscriber' }

  # Some other event subscriber with dependency injection.

Same as before. We define a new service and tag it as an event_subscriber. Now we need to write that EventSubscriber class.

Broken down:

  1. We subscribe to the event named UserLoginEvent::EVENT_NAME with the method onUserLogin() (a method name we made up).
  2. During onUserLogin, we access the $account property (the user that just logged in) of the $event object, and do some stuff with it.
  3. When a user logs in, they should see a message telling them the date and time for when they joined the site.

    Message after logging in.
    Message after logging in.

Voila! We have both dispatched a new custom event, and subscribed to that event. We are awesome at this!

Event Subscriber Priorities

Another great feature of the Events system is the subscriber’s ability to set its own priority within the subscriber itself, rather than having to change the entire module’s execution weight or leverage another hook to change the priority (as with hooks).

Doing this is very simple, but to best show it off we need to write another subscriber to an event where we already have a subscriber. Let’s write “AnotherConfigEventSubscriber” and set the priorities for its listeners.

First, we’ll register our new event subscriber in our services.yml file:

services:
  # Subscriber to the config events, with dependencies injected.
  # Name of this service.
  my_config_events_subscriber_with_di:
    # Event subscriber class that will listen for the events.
    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriberWithDI'
    # Inject services as "arguments"
    arguments:
      - '@messenger'
    # Tagged as an event_subscriber to register this subscriber with the event_dispatch service.
    tags:
      - { name: 'event_subscriber' }

  # Some other event subscriber with dependency injection.
  another_config_events_subscriber:
    class: '\Drupal\custom_events\EventSubscriber\AnotherConfigEventsSubscriber'
    arguments:
      - '@messenger'
    tags:

Then we’ll write the AnotherConfigEventSubscriber.php:

<?php

namespace Drupal\custom_events\EventSubscriber;

use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Messenger\MessengerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class EntityTypeSubscriber.
 *
 * @package Drupal\custom_events\EventSubscriber
 */
class AnotherConfigEventsSubscriber implements EventSubscriberInterface {

  /**
   * Messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * AnotherConfigEventsSubscriber constructor.
   *
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   Messenger service injected during the static create() method.
   */
  public function __construct(MessengerInterface $messenger) {
    $this->messenger = $messenger;
  }

  /**
   * {@inheritdoc}
   *
   * @return array
   *   The event names to listen for, and the methods that should be executed.
   */
  public static function getSubscribedEvents() {
    return [
      ConfigEvents::SAVE => ['configSave', 100],
      ConfigEvents::DELETE => ['configDelete', -100],
    ];
  }

  /**
   * React to a config object being saved.
   *
   * @param \Drupal\Core\Config\ConfigCrudEvent $event
   *   Config crud event.
   */
  public function configSave(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    $this->messenger->addStatus(__CLASS__ . ' - Saved config: ' . $config->getName());
  }

  /**
   * React to a config object being deleted.
   *
   * @param \Drupal\Core\Config\ConfigCrudEvent $event
   *   Config crud event.
   */
  public function configDelete(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    $this->messenger->addStatus(__CLASS__ . ' - Deleted config: ' . $config->getName());
  }

}

Pretty much the only important difference here is that we have changed the returned array in the getSubscribedEvents() method. Instead of the value for a given event being a string with the local method name, it is now an array where the first item in the array is the local method name and the second item is the priority of this listener.

So we changed this:

  public static function getSubscribedEvents() {
    return [
      ConfigEvents::SAVE => 'configSave',
      ConfigEvents::DELETE => 'configDelete',
    ];
  }

To this:

  public static function getSubscribedEvents() {
    return [
      ConfigEvents::SAVE => ['configSave', 100],
      ConfigEvents::DELETE => ['configDelete', -100],
    ];
  }

The results we’re expecting:

  • AnotherConfigEventSubscriber::configSave() has a very high priority, so it should be executed before ConfigEventSubscriber::configSave().
  • AnotherConfigEventSubscriber::configDelete() has a very low priority, so it should be executed after ConfigEventSubscriber::configDelete().

Let’s see the SAVE event in action by enabling the Statistics module again.

Installing the Statistics module and looking at messages.
Installing the Statistics module and looking at messages.

Great! Our new event listener on ConfigEvents::SAVE happened before the other one we wrote. Now let’s uninstall the Statistics module and see what happens on the DELETE event.

Uninstalling statistics module and looking at the messages.
Uninstalling statistics module and looking at the messages.

Also great! Our new event listener on ConfigEvents::DELETE was executed after the other one we wrote because it has a very low priority.

Note: When you register a subscriber to an event without specifying the priority, it defaults to 0.

Notable Progress on Events in Drupal 8

There is an issue in the queue working towards replacing the foundation of the hook system with the event system:

  • Add a HookEvent. This approach would provide a generic HookEvent that it expects custom events to extend, and a method for returning values from the event.
  • An older issue about Replacing Hooks with Events was postponed until Drupal 9. This discussion ended over 5 years ago.
  • An ongoing issue proposing the addition of Events for Matching Entity Hooks.
  • More ready-for-use, there is an excellent contributed module named hook_event_dispatcher that provides Events for the most commonly used Drupal hooks. If you’re ready to start using fewer hooks and more events, this module is a fine dependency for your custom code.

Though I’m hardly an expert on what we should expect from Drupal in the future regarding events, I hope we see many more of them and that they eventually replace hooks completely.

References:


Want to know something else specific about Drupal 8 Events, or have some more information about the future of events in Drupal? Let me know below!

7 Thoughts

Discussion

Svetoslav Dragoev
March 8, 2018

Really nice article, just one note, for using services inside a class you should use (it is preferred to use) Dependency injection instead of the global service containers.
I am talking about this part of the code in UserLoginSubscriber (other classes too)

public function onUserLogin(UserLoginEvent $event) {
 $database = \Drupal::database();
 $dateFormatter = \Drupal::service('date.formatter');

You shouldn’t be using `\Drupal::service(‘date.formatter’);`, but rather initialize a variable carrying this service object in the __constructor which would receive it by providing it from create method:
https://www.drupal.org/docs/8/api/services-and-dependency-injection/services-and-dependency-injection-in-drupal-8 (Accessing services in objects using dependency injection)
An example of how it could be done with form:
https://www.drupal.org/docs/8/api/services-and-dependency-injection/dependency-injection-for-a-form
It’s pretty much the same for event, just implement also ContainerInjectionInterface and use it of course: Symfony\Component\DependencyInjection\ContainerInterface.

Jonathan Daggerhart
March 8, 2018

Hi Svetoslav,

Thanks for reading! You’re absolutely right, it would be ideal to inject those services.

In the module on GitHub I have a couple more events written that use DI, but they didn’t make it into this example. I think I’m going to write a “Part 2” post that includes details about DI conceptually and shows these other Events that use DI.

I don’t think you need this, but in case a random passerby is interested in seeing an EventSubscriber that has its dependencies injected:

Thanks again!

James Williams
March 8, 2018

Just to drop it here for reference, for anyone else looking to learn more on this — there’s an old post from PreviousNext which, especially in the comments, has some discussion of what hooks or events may do better. It’s a little old, so I don’t know if it’s still true, but for example, one comment mentions that events may not be as performant (yet). There’s also some tricks & caveats mentioned, such as how to get a module reacting to a hook more than once, or about removing another module’s event registration.

The article is here: https://www.previousnext.com.au/blog/alter-or-dispatch-drupal-8-events-versus-alter-hooks

Jonathan Daggerhart
March 8, 2018

That’s a really great post and comments. If I’ve learned anything over the past week about Drupal vs Events, it’s that the conversations (and opinions) are fragmented all over the place in the issue queue and various blog posts. ¯\_(ツ)_/¯

Thanks for sharing!

RJ
March 20, 2019

Best article I’ve seen on the subject so far (3 hours of searching for hook_user_update info for D8.)

Thanks!

Heitor Althmann
April 1, 2019

Excellent article from end to end! Thank you for putting the time and effort on writing it, Jonathan. With the only intention to help you make it better, in the beginning of the article you’ve mentioned twice that we should extend the EventSubscriber class, but what you actually implements EventSubscriberInterface. Am I missing something or that was really a misspell? Anyway, congratulations and thank you once again! Great article!

Jonathan Daggerhart
April 1, 2019

Great catch! You are absolutely right and I’ve corrected the article. Thanks for the kind words and the sharp eye!

Piet de Nooijer
April 10, 2019

Great article Jonathan! Thanks for the kind words about the hook_event_dispatcher module, really appreciated.

Mykola Veryha
June 2, 2020

It’s the best article that I saw about Event Subscribers.

Virajee Amarasinghe
December 17, 2021

Thank you for this great post!!

Leave a Reply

Your email address will not be published. Required fields are marked *