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]);
- Drupal 7:
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
.
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.
That’s pretty simple, but let’s break it down a little bit.
- We define a new service named “my_config_events_subscriber“
- We set its “class” property to the the global name of a new PHP class that we will create.
- 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:
- Should implement the
EventSubscriberInterface
interface. - 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()
andconfigDelete()
we expect an object of theConfigCrudEvent
type. That object has a the methodgetConfig()
which returns theConfig
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 constantSAVE
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.
- Install “custom_events” module on its own.
- Install “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 thestatistics.settings
config object. - Uninstall “statistics” module.
This time we see both the
SAVE
andDELETE
events fired. We can see that thestatistics.settings
config object has been deleted, and thecore.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.
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
.
Inside of our “hook_user_login” implementation, we only need to do a few things to dispatch our new event:
- Instantiate a new custom object named
UserLoginEvent
and provide its constructor the $account object available within the hook. - Get the
event_dispatcher
service. - Execute the
dispatch()
method on theevent_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.
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:
- We subscribe to the event named
UserLoginEvent::EVENT_NAME
with the methodonUserLogin()
(a method name we made up). - During onUserLogin, we access the
$account
property (the user that just logged in) of the$event
object, and do some stuff with it. - When a user logs in, they should see a message telling them the date and time for when they joined the site.
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:
Then we’ll write the AnotherConfigEventSubscriber.php
:
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:
The results we’re expecting:
AnotherConfigEventSubscriber::configSave()
has a very high priority, so it should be executed beforeConfigEventSubscriber::configSave()
.AnotherConfigEventSubscriber::configDelete()
has a very low priority, so it should be executed afterConfigEventSubscriber::configDelete()
.
Let’s see the SAVE event in action by enabling the Statistics module again.
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
.
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!
Discussion
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)
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.
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!
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
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!
Best article I’ve seen on the subject so far (3 hours of searching for hook_user_update info for D8.)
Thanks!
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!
Great catch! You are absolutely right and I’ve corrected the article. Thanks for the kind words and the sharp eye!
Great article Jonathan! Thanks for the kind words about the hook_event_dispatcher module, really appreciated.
It’s the best article that I saw about Event Subscribers.
Thank you for this great post!!