You are on page 1of 13

Drupal 8: Hooks, Events, and Event

Subscribers
Introduction to events in Drupal 8. Comparison between Events and Hooks, how to use
events, and a little bit about what to expect in the future.

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 use of
the module_invoke_all() method in Drupal 7-, and
the \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:
o Drupal 7: module_invoke_all('my_event_name',
$some_arbitrary_parameter);
o 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: 8.x
package: Custom
custom_events.info.yml view raw

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:
# Name of this service.
my_config_events_subscriber:
# Event subscriber class that will listen for the events.
class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
# Tagged as an event_subscriber to register this subscriber with the
event_dispatch service.
tags:
- { name: 'event_subscriber' }
custom_events.services.yml view raw

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 only need to write the event subscriber class. There are a few requirements for this
class we want to make sure we do:

1. Should extend the EventSubscriber class.


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.

<?php

namespace Drupal\custom_events\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

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

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

/**
* React to a config object being saved.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* Config crud event.
*/
public function configSave(ConfigCrudEvent $event) {
$config = $event->getConfig();
drupal_set_message('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();
drupal_set_message('Deleted config: ' . $config->getName());
}

}
ConfigEventsSubscriber.php view raw

That’s it! It looks pretty simple, but let’s walk through this and hit the important notes:

 We extend the EventSubscriber class.


 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.

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.

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.php view raw

 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(UserLoginEvent::EVENT_NAME, $event);
}
custom_events.module view raw

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:
# Name of this service.
my_config_events_subscriber:
# Event subscriber class that will listen for the events.
class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
# Tagged as an event_subscriber to register this subscriber with the
event_dispatch service.
tags:
- { name: 'event_subscriber' }

# Subscriber to the event we dispatch in hook_user_login.


custom_events_user_login:
class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber'
tags:
- { name: 'event_subscriber' }
custom_events.services.yml view raw

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

<?php

namespace Drupal\custom_events\EventSubscriber;

use Drupal\custom_events\Event\UserLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

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

/**
* Database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;

/**
* Date formatter.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
// Static class constant => method on this class.
UserLoginEvent::EVENT_NAME => 'onUserLogin',
];
}

/**
* React to the user login event dispatched.
*
* @param \Drupal\custom_events\Event\UserLoginEvent $event
* Dat event object yo.
*/
public function onUserLogin(UserLoginEvent $event) {
$database = \Drupal::database();
$dateFormatter = \Drupal::service('date.formatter');

$account_created = $database->select('users_field_data', 'ud')


->fields('ud', ['created'])
->condition('ud.uid', $event->account->id())
->execute()
->fetchField();

drupal_set_message(t('Welcome, your account was created on


%created_date.', [
'%created_date' => $dateFormatter->format($account_created, 'short'),
]));
}

}
UserLoginSubscriber.php view raw

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.

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:
# Name of this service.
my_config_events_subscriber:
# Event subscriber class that will listen for the events.
class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
# Tagged as an event_subscriber to register this subscriber with the
event_dispatch service.
tags:
- { name: 'event_subscriber' }

# Subscriber to the event we dispatch in hook_user_login.


custom_events_user_login:
class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber'
tags:
- { name: 'event_subscriber' }

another_config_events_subscriber:
class:
'\Drupal\custom_events\EventSubscriber\AnotherConfigEventsSubscriber'
tags:
- { name: 'event_subscriber' }
custom_events.services.yml view raw

Then we’ll write the AnotherConfigEventSubscriber.php:

<?php

namespace Drupal\custom_events\EventSubscriber;

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

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

/**
* {@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();
drupal_set_message('(Another) 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();
drupal_set_message('(Another) Deleted config: ' . $config->getName());
}

}
AnotherConfigEventsSubscriber.php view raw

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.

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.

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:

 GitHub repo – Contains all the working code presented in this post.
 Symfony Documentation: Event Listeners & Subscribers – Note, Drupal 8 does not
use “Event Listeners” in the Symfony sense. Focus on Event Subscribers.
 Symfony Documentation: Event Dispatcher

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!