Getting familair with the EventDispatcher
Drupal 8 introduced a few new concepts to Drupal and added the EventDispatcher to its list of components. I wanted to get familair with this new tool in our toolbox. Here is a walkthrough of my journey.
I started my journey by digging into the EventDispatcher component on symfony.com. The straight forward explenation of the EventDispatcher is: it implements the Observer pattern in Symfony and allows to subscribe for events. These event can be triggered by anyone that can acces the EventDispatcher object. In this article we will look into the concept of the Observer pattern, using the EventDispatcher and how its used inside Drupal core and how to use it your self in your modules.
The Observer pattern
In programming history a lot of standard patterns have been developed. These patterns help with common programming problems. The Observer pattern is a pattern where a Subject (e.g. Bank) tells Observers (e.g. StockMarket) about changes or events (e.g. MoneyTransfer). A Observer subscribes with the Subject (or a party in the middle) that it wants to receive updates about certain events.
Using EventDispatcher yourself
Lets take the example of the Bank I mentioned above. Here is the example of the class.
class Bank { <br> function transferMoney($amount) { <br> // code ... <br> } <br>}
The Bank wants to provide others with updates about the transfer of money. To do this the there is a middle party (the EventDispatcher) that will manage all the Observers that want to receive events from the Bank. Lets call this the bankDispatcher.
$bankDispatcher = new EventDispatcher();
We extend the contructor of Bank with a argument to pass the dispatcher into the object to use it.
use Symfony\Component\EventDispatcher\EventDispatcher;<br>use Symfony\Component\EventDispatcher\Event;<br><br>class Bank { <br> private $dispatcher = NULL; <br><br> function __contruct(EventDispatcher $dispatcher) {<br> $this->dispatcher = $dispatcher; <br> }<br><br> // other code ...<br>}
The StockMarket can register for a specific event. Lets look at a example of such a subscribtion.
class StockMarket {<br> private $dispatcher = NULL; <br><br> function __contruct(EventDispatcher $dispatcher) {<br> $this->dispatcher = $dispatcher; <br> $this->dispatcher->addListener(<br> 'bank.money_transfer',<br> array($this, 'onMoneyTransfer'));<br> }<br><br> function onMoneyTransfer(Event $event) {<br> // code...<br> }<br>}
The Bank object can now use the dispatcher to dispatch a event and the StockMarket object will be triggered.
class Bank { <br> static $dispatcher = NULL; <br><br> function __contruct(EventDispatcher $dispatcher) {<br> $this->dispatcher = $dispatcher; <br> }<br><br> function transferMoney($amount) { <br> $this->dispatcher->dispatch('bank.money_transfer', new Event()); <br> } <br>}
You can find a example module here on github that you can add to your Drupal 8 installation and play with it.
Events in Drupal core
One of the new main concepts in Drupal is the DependencyInjectionContainer or for short DIC. This container also contains the central event_dispatcher. You can retrieve the dispatcher like this:
Drupal::service('event_dispatcher');
After you retrieved the event_dispatcher you can dispatch event just like in the example before. All the listeners that are connected to this Dispatcher will receive notifications if they registered for it. Drupal core does not have a lot of event build in yet. But the components of Symfony sometimes have. The HttpKernel that is used to handle the page request does trigger several events.
Search for Subscriber in the core/modules folder to figure out what events Drupal core modules listen too. Or search for 'implements EventSubscriberInterface' then you will find several. A different way is to look for '->dispatch(' then you will find the places where a event is dispatched.
Now lets look at what a subscriber does.
Using Subscribers in your module
The next step, that is also introduced in Drupal 8, is the implementation of the EventSubscriber. By implementing the EventSubscriberInterface with its static getSubscribedEvents() we can register for a set of event and tell what functions to call. One of the main targets with Drupal 8 was to introduce lazy loading of objects. This way the load of the application goes down because only the objects are loaded that are really needed (at the time they are needed). With the registration of events in the ContainerAwareEventDispatcher events can be lazy loaded. Here is a example of a Subscriber class.
namespace Drupal\vdmi_eventdispatcher\EventSubscriber;<br><br>use Symfony\Component\EventDispatcher\Event;<br>use Symfony\Component\EventDispatcher\EventSubscriberInterface;<br><br>class StockMarketSubscriber implements EventSubscriberInterface {<br><br> /**<br> * Registers the methods in this class that should be listeners.<br> *<br> * @return array<br> * An array of event listener definitions.<br> */<br> static function getSubscribedEvents() {<br><br> // - Return a list of events names as key.<br> // - With elements containing a method name<br> // or a array with method name and priority.<br> //<br> // For exampl like this with multiple callbacks:<br> //<br> // $events['event.name'] = array();<br> // $events['event.name'][] = 'static callback function on this object';<br> // $events['event.name'][] = array(<br> // 'static callback function on this object',<br> // 10<br> // );<br> //<br> // or like this for a single callback:<br> //<br> // $events['event.name'] = array();<br> // $events['event.name'] = array(<br> // 'static callback function on this object',<br> // 10<br> // );<br> //<br> // or like this when the priority is not important.<br> //<br> // $events['event.name'] = 'static callback function on this object';<br><br> $events['bank.money_transfer'] = 'onMoneyTransfer';<br> return $events;<br> }<br><br> static function onMoneyTransfer(Event $event) {<br> drupal_set_message('Money was transferred. Event thrown by Subscriber.', 'status', TRUE);<br> }<br><br>}
This module implements the getSubscribedEvents() and tells to subscribe to the event with the name
bank.money_transfer
. This is the event triggered in our previous example.
Every module can implement the modulename.services.yml configuration file that automaticly registers a service with the Dependency Injection Container. This is also where the central EventDispatcher lives. You register it in the Event Subscriber by using TaggedService functionality.
services:<br> stockmarket.subscriber:<br> class: Drupal\vdmi_eventdispatcher\EventSubscriber\StockMarketSubscriber<br> tags:<br> - { name: event_subscriber }
Adding the tagname event_subscriber is important. This will be picked when building the DIC. You can see a working example in my git repo.
For those interested on where this tag is processed. The Subscriber classes are executed by the RegisterKernelListenersPass which implements the CompilerPassInterface. This is used inside the CoreServiceProvider.
Extra references:
- While writing this article the core library changed. Notice that this might happen in the future too. I wrote this first with alpha2. But the CoreServiceProvide I mentioned was introduced in commit b1c68.
- On the Drupal issue queue cweagans tried to introduce the EvenDispatcher as a replacement for the hook system. There was no agreement but the issue give a lot of information on the concept of hooks and of events.
- Allow modules to register services and subscriber services (events) on Drupal.org. You can learn a lot from reading on these topics.
- Blog image from Symfony2 blog