Responding to Events in Drupal 8

Drupal 8

Note: You can get up-to-date, detailed tutorials about working with Drupal 8 events as part of our Drupal 8 Module Development Guide.

I recently went through the process of outlining and recording a short tutorial that covers the various methods, in addition to hooks, that module developers will be able to use in order to extend and enhance Drupal 8. In doing so I spent some time reading about, and subsequently helping to document, the new event dispatcher in Drupal and I wanted to share my findings.

Events in Drupal 8 allow various system components to interact and communicate with one another while remaining independent, or decoupled. The event system is built on the Symfony event dispatcher component, and is an implementation of the Mediator design pattern.

The general idea is this: As an application grows in complexity, and additional components are added, it becomes necessary for those components to communicate with one another. Rather than having objects refer to one another explicitly, which can quickly turn into a maintenance nightmare, communication is instead facilitated by a mediator object. This reduces the dependencies between communicating objects, thereby lowering the coupling, and creating a code base that's easier to maintain.

Sometimes these abstract patterns are a little bit easier to grasp if you can ground them in something you know. So let's see if we can't use our imagination and break this down.

Coordinating Superheroes

Pretend for a moment that you've just been hired by the city of Gotham to work in the hero coordination department. The office you work for helps to coordinate the activities of the various superheroes who have chosen to make Gotham their home. Your job: when the local bodega owner calls the Gotham Police Department to report a robbery you listen in and then notify any heroes who happen to be in the area that something is going down and they might want to respond. In doing so, you relay the pertinent details provided by the shop owner; What happened? How many suspects where involved? Was it Norman Osborn again?

Your job requires you to act as the mediator object. Receive a message from one object, and pass it on to another. In Drupal's case this is handled by \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher, an event dispatcher that builds on the Symfony dispatcher, with a few differences we'll cover later.

Now, any Gotham citizen can of course call the Police Department in order to report suspicious activity. You ask them to fill out an incident report. In your desk you've got different forms for different incident types. Robbery, cats stuck in trees, and other common ailments. In order to maintain consistency each of these forms has some common fields like caller name, date, and location, and they also have an incident-specific field as well. You help each caller fill out the appropriate form.

In Drupal terms, Gotham citizens get a hold of you, the dispatcher, by calling ContainerAwareEventDispatcher::dispatch($eventName, Event $event = null);. The form they fill out is an Event object. All event objects are extensions of \Symfony\Component\EventDispatcher\Event, which provides the aforementioned common fields like name of report, and location of incident. The Event class is extended in order to provide information that's specific to the event in question.

You've now got a message to pass along to notify the heroes in town that something is going down. Only a subset of the city's citizens are superheroes, and sometimes those heroes are taking the day off, or maybe even moved to another city for a period. So you maintain a list of all the heroes on active duty at any given time and when a report comes in you send it to just the heroes on your list. In order to keep this list up-to-date, heroes are required to call you every morning and register their intent to listen to any notifications you're dispatching that day. We could take this even further and allow heroes to declare their preference to only be notified of incidents of a specific type. Batman for example might only want to respond to night-time incidents.

In our code the hero is represented as an implementation of the \Symfony\Component\EventDispatcher\EventSubscriberInterface, in which you provide a ::getSubscribedEvents() method that declares your intent to listen for events of a given type. This is done by returning an associative array where the key is the unique name of the event (generally a PHP constant), and the value is an array of PHP callables (generally just the name of a specific method on the class) that should be called whenever events of the specified type are triggered.

Example: modules/tommy/src/EventSubscriber/TommySubscriber.php


<?php
/**
 * Example event subscriber.
 */

// Declare the namespace that our event subscriber is in. This should follow the
// PSR-4 standard, and use the EventSubscriber sub-namespace.
namespace Drupal\tommy\EventSubscriber;

// This is the interface we are going to implement.
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
// This class contains the event we want to subscribe to.
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Subscribe to KernelEvents::REQUEST events and redirect if site is currently
 * in maintenance mode.
 */
class TommySubscriber implements EventSubscriberInterface {
  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = array('checkForRedirection');
    return $events;
  }
}

Event Response

Heroes, aren't required to respond to an incident, though of course it's preferable if at least one of them does. After passing along the report, your job is done, and you go back to the switchboards waiting for another call. When the hero receives an incident report from you however, their job has just started. They examine the details and determine whether or not this is something that requires action on their part. They assess things like: How busy am I right now? Is the location near me? If it's another Norman Osborn incident Batman might decide to pass, but Spiderman will be sure to jump at the opportunity.

In our code, we implement a response by adding the method matching that which we declared in the return value from getSubscribedEvents(). The superhero dispatcher was nice enough to pass along a complete copy of the form that was filled out, so we've got an Event object we can examine for further details about what we're getting into. This makes our code look like the following:


<?php
/**
 * Example event subscriber.
 */

// Declare the namespace that our event subscriber is in. This should follow the
// PSR-4 standard, and use the EventSubscriber sub-namespace.
namespace Drupal\tommy\EventSubscriber;

// This is the interface we are going to implement.
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
// This class contains the event we want to subscribe to.
use Symfony\Component\HttpKernel\KernelEvents;
// Our event listener method will receive one of these.
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
// We'll use this to perform a redirect if necessary.
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Subscribe to KernelEvents::REQUEST events and redirect if site is currently
 * in maintenance mode.
 */
class TommySubscriber implements EventSubscriberInterface {
  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = array('checkForRedirection');
    return $events;
  }

  /**
   * This method is called whenever the KernelEvents::REQUEST event is
   * dispatched.
   *
   * @param GetResponseEvent $event
   */
  public function checkForRedirection(GetResponseEvent $event) {
    // If system maintenance mode is enabled, redirect to a different domain.
    $enabled = \Drupal::state()->get('system.maintenance_mode');
    if ($enabled === 1) {
      $event->setResponse(new RedirectResponse('http://example.com/'));
    }
  }
}

This example code will subscribe our listener to all KernelEvents::REQUEST events which are dispatched by the Symfony HTTP kernel early in the page request life cycle. When one of these events is triggered, our listener checks to see if the application is in maintenance mode and performs a redirect to a different URL if it is, or it does nothing if the site is not in maintenance mode. This is a task that in Drupal 7 we likely would have handled in an implementation of hook_init().

If all you're doing is implementing a basic mediator pattern, that's it, you're done. The thing is your boss, who goes by the name Drupal 8, has some additional requirements that you're going to need to meet when relaying messages from citizens of Gotham to the superheroes that protect them.

Optimizing for Speed

One of the things that really bugs your boss about this whole system is that she feels the office spends far to much time maintaining the list of heroes who intend to provide support on any given day. And you agree—it's a tedious task. You're waiting around every morning for each hero on the list to call in and declare their intent, and then collating their responses into a list for the day, only to throw the list away at the end of the day and start fresh the next day. So you come up with a better system. Rather than require heroes to call in every day and let you know what types of incidents they're interested in, you ask them to fill out a calendar that declares their intent for the whole month. It's more work for the hero up front but it saves everyone time in the long run since you can spend one long Monday morning creating a list that remains valid for the whole month! This vastly reduces the overhead of informing superheroes that something is going down by an order of magnitude. Your boss is thrilled, and you get a promotion.

Instantiating a copy of every event subscriber, and then calling the getSubscribedEvents() method on each one in order to create a list of listeners is a big performance drain, especially if you've instantiated an object which declares that it's listening to an event that's never dispatched. It would be better if event subscribers were lazy-loaded, as needed. Drupal accomplishes this by requiring that event subscribers are registered with the services container, where it can then cache the list of subscribers, and the events that they are interested in, and call them only as needed.

Understanding exactly how this works requires stepping back and understanding how the services container is compiled. Services in Drupal are collected, and compiled, into the services container during various compiler passes, which do things like read the {MYMODULE}.services.yml file in your module's directory and use the information contained within to add any services provided by your module to the container. This compiled container is ultimately written to disk and effectively cached so that during the next request Drupal doesn't have to go through this entire discovery process again. The details about how this all works are outside of the scope of this article, but here is what you need to know to understand the event system.

Comic describing the super hero event dispatcher process

When reading the data in your {MYMODULE}.services.yml file, one of the things Drupal looks for is service tags. These are indicators to the compiler that this particular service should be registered, or used, in a special way. When adding an event subscriber we tag our service(s) with the aptly named, event_subscriber tag.

Example:


tags:
 
  - {name: event_subscriber}

When this tag is encountered, the compiler instantiates a copy of the tagged service class, calls the ::getSubscribedEvents(), and retains a list of all their combined responses. This is like when you go through the list of heroes on the first Monday of each month, call each one, and formulate a plan for who to call on which days.

This list is then compiled into the service container. If you're curious you can see it in the getEventDispatcherService() method. Every morning when you come into work you sit down at your desk and pull up today's list, and you rejoice that you don't have to call each hero every day. You can just flip to the next page in your notebook which contains the list you created earlier in the month.

In summary, when you add a new service in your {MYMODULE}.services.yml file, if you tag it with the event_subscriber tag, you can return a list of events you would like to subscribe to, and the name of the method you would like to call, by implementing the EventSubscriberInterface::getSubscribedEvents() method. Whenever an event you are subscribed to is dispatched, your method will be called.

Recap

In order to make it easy for any new superhero in town to get notified whenever something is going down, all they need to do is:

Then you will happily relay reports to them from Gotham's citizens, and everyone will be just a little bit happier.

Now that you know how to subscribe to an event, here's the list of events in Drupal 8 that you can subscribe to. At the time that this article was written not all events have been documented, and there is an on-going effort to do so in this issue: [meta] Add @Event to all events defined by drupal core.

We'll leave it to a future article to get into the topic of dispatching events that another module can subscribe to, allowing your code to be extended and enhanced with the same flexibility of Drupal core.

Additional resources:

Related Topics: 

Comments

Great one, indeed!!! Story really helps!!!

Thanks for sharing information about drupal and I am drupal that why i m searching on this topic and found your website on google.

Renuka Singh

Yikes! Please don't tell me that this is the replacement for drupal_alter(). That would mean replacing a couple lines of code with dozens spread across several files.

In some cases, but not all, this is the replacement for drupal_alter(). For better or worse, I also suspect we'll see things moving in this direction more and more in the future. It is certainly more code, and I agree that it can be a bit more of a pain to write, but it also allows for some additional flexibility – you can have multiple listeners in a single module. It's more robust. And it's generally more in-line with what other people in the PHP space are doing.

There are some good tools already like Drupal console for generating some of the boilerplate code, and a good IDE like phpStorm will also help quite a bit with the more "copy/paste" parts of the implementation.

After research I think there's no event to react on submitted forms. Does anybody know more than me?

Thats' correct. There are no events triggered for "form submit" actions. Instead, you'll want to implement hook_form_alter() and add a #submit handler to your form definition if you want to react to form submission for a specific form. See https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21fo...

Alternately, if the thing you want to react to is the saving/updating of some entity, and not necessarily the form submission there are hooks you can implement for those reactions as well.

Thank you.

yml .. it's clear Now.

D7 used to do this in just a couple of lines, now D8 turn this in something awful, more organized YES ...but still long and painful.

This is a lot of stuff for people coming from d7 to d8. But no doubt more organized and very well explained. Helpful article.

Seriously poor.

Curious to know what you didn't like about this post? Happy to try and make it clearer if you've got suggestions.

Great read, helped clarify a lot, I really appreciate it

Add new comment