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:
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.
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.
First thing we need is a module where we’re going to do our work. I’ve named mine
custom_events.
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
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:
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:
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.
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.
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 {
/**
* 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
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);
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' }
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');
}
UserLoginSubscriber.php view raw
Broken down:
Voila! We have both dispatched a new custom event, and subscribed to that event. We are
awesome at this!
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' }
another_config_events_subscriber:
class:
'\Drupal\custom_events\EventSubscriber\AnotherConfigEventsSubscriber'
tags:
- { name: 'event_subscriber' }
custom_events.services.yml view raw
<?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:
To this:
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.
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.
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!