Events system in Drupal

Drupal 10
Drupal 9

Starting with v.8 Drupal is based on Symfony and includes the event system which is built on the Symfony event dispatcher component. 

The EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them.

How it works

During the process of responding to a request, Drupal will dispatch (trigger) various events, notifying subscribers that now is the time to do their thing. Event subscribers respond to specific events being triggered and perform custom logic. Which could be anything from updating related settings when a configuration change occurs in another module, to redirecting a request after inspecting the request parameters. All event objects are extensions of \Symfony\Component\EventDispatcher\Event, which provides information that's specific to the event.

In order to subscribe to any event you have to  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 the name of a specific method on the class and its priority (optionally, it will be set up to 0 by default) that should be called whenever events of the specified type are triggered.

  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['onKernelRequestSessionTest', 100];
    $events[KernelEvents::RESPONSE][] = ['onKernelResponseSessionTest'];
    return $events;
  }

This example code will subscribe our listener to all KernelEvents::REQUEST and KernelEvents::RESPONSE events which are dispatched by the Symfony HTTP kernel.

  /**
   * This method is called whenever the KernelEvents::REQUEST event is
   * dispatched.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The event to process.
   */
  public function onKernelRequestSessionTest(RequestEvent $event) {
    // Set header for session testing.
    $session = $event->getRequest()->getSession();
    $this->emptySession = (int) !($session && $session->start());
  }
  
  /**
   * This method is called whenever the KernelEvents::RESPONSE event is
   * dispatched.
   *
   * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
   *   The Event to process.
   */
  public function onKernelResponseSessionTest(ResponseEvent $event) {
    // Set header for session testing.
    $response = $event->getResponse();
    $response->headers->set('X-Session-Empty', $this->emptySession);
  }

For such kind of tasks in Drupal 7 we likely would have handled in an implementation of different hooks, e.g. hook_init().

Creating 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. And it'd be better if event subscribers were lazy-loaded, as needed. That's why we use services container to register event subscribers in Drupal, where it can then cache the list of subscribers, and the events that they are interested in, and call them only as needed. 

All services can have specific tags which are re indicators to the compiler that this particular service should be registered, or used, in a special way. We should use event_subscriber tag for adding event subscriber service.

tags:
 
  - {name: event_subscriber}

In summary, when we add a new service in our {MYMODULE}.services.yml file and tag it with the event_subscriber tag, we can return a list of events we'd like to subscribe to, and the name of the method we'd like to call, by implementing the EventSubscriberInterface::getSubscribedEvents() method:

  • Define a service in the module and tag it with the event_subscriber tag.
  • Define a new class that implements \Symfony\Component\EventDispatcher\EventSubscriberInterface and declare the event(s) for subscribing to in ::getSubscribedEvents().
  • Write the methods on the new class that respond to the event(s) we subscribed to.

Practice examples

You might have noticed the modules "Search API" and "Search API Solr" stop using hook system and start using events system. Their {MODULE}.api.php includes notification about this, for example 

 * @deprecated in search_api_solr:4.2.0 and is removed from
 *   search_api_solr:4.3.0. Handle the PreSuggesterQueryEvent instead.
 *
 * @see https://www.drupal.org/project/search_api_solr/issues/3203375
 * @see \Drupal\search_api_solr_autocomplete\Event\PreSuggesterQueryEvent

As you can see this notification has name of the event which should to used into event subscriber. 
Let's let's suppose for example, we need to change the way the index's field names are mapped to Solr field names. Previously we would used hook_search_api_solr_field_mapping_alter for this. Let's find the description of this hook:

/**
 * Change the way the index's field names are mapped to Solr field names.
 *
 * @param \Drupal\search_api\IndexInterface $index
 *   The index whose field mappings are altered.
 * @param array $fields
 *   An associative array containing the index field names mapped to their Solr
 *   counterparts. The special fields 'search_api_id' and 'search_api_relevance'
 *   are also included.
 * @param string $language_id
 *   The language ID that applies for this field mapping.
 *
 * @deprecated in search_api_solr:4.2.0 and is removed from
 *   search_api_solr:4.3.0. Handle the PostFieldMappingEvent instead.
 *
 * @see https://www.drupal.org/project/search_api_solr/issues/3203375
 * @see \Drupal\search_api_solr\Event\PostFieldMappingEvent
 */
function hook_search_api_solr_field_mapping_alter(IndexInterface $index, array &$fields, string $language_id) {
  $fields['fieldname'] = 'ss_fieldname';
}

We can find out about the name of the required event from it:  \Drupal\search_api_solr\Event\PostFieldMappingEvent

  1. First of all we need to define a new event subscriber service in our custom module:
services:
  bbd_example.search_api_subscriber:
    class: Drupal\bbd_example\EventSubscriber\SearchApiSubscriber
    tags:
      - {name: event_subscriber}

Please pay attention this service has the tag "event_subscriber".

  1. Next step is to define a new class that implements \Symfony\Component\EventDispatcher\EventSubscriberInterface and declare the event for subscribing to in ::getSubscribedEvents():
namespace Drupal\bbd_example\EventSubscriber;
use Drupal\search_api_solr\Event\PostFieldMappingEvent;
use Drupal\search_api_solr\Event\SearchApiSolrEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
 * Search API event subscriber.
 */
class SearchSubscriber implements EventSubscriberInterface {
  
  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[SearchApiSolrEvents::POST_FIELD_MAPPING][] = ['onPostFieldMapping'];
    return $events;
  }
}
  1. And the final step is to write the methods on this new class that respond to the event:
...
use Drupal\search_api\IndexInterface;
...
...
  /**
   * Change the way the index's field names are mapped to Solr field names.
   *
   * @param \Drupal\search_api_solr\Event\PostFieldMappingEvent $event
   *   Event after the Search API to Solr fields mapping is generated. 
   */
  public function onPostFieldMapping(PostFieldMappingEvent $event) {
    $index = $event->getIndex();
    $fields = $event->getFieldMapping();
    
    { override field mapping }
    $event->setFieldMapping($fields);
  }

All available properties and methods of the event PostFieldMappingEvent you will find in the class Drupal\search_api_solr\Event\PostFieldMappingEvent. 

Example how to create own event

Pretend we need to defines some custom event, e.g Drupal entities still use hooks for various entity operations, like hook_entity_load(), hook_entity_create(), hook_entity_presave(), etc. Let's handle Drupal hooks in the event subscriber provided. I believe Drupal entity lifecycle hooks will be removed and EventDispatcher will be used instead in the near future. And an idea of moving code from hooks to methods into the EventSubscriberInterface class doesn't look like an insane.

  1. Let's start and define a new event class that implements \Symfony\Contracts\EventDispatcher\Event in the custom module:
namespace Drupal\bbd_example\Event;
use Symfony\Contracts\EventDispatcher\Event;
use Drupal\Core\Entity\EntityInterface;
/**
 * Event after the node is removed.
 */
class NodeDeleteEvent extends Event {
  /**
   * Node entity.
   *
   * @var \Drupal\Core\Entity\EntityInterface
   */
  protected $entity;
  /**
   * Constructs a new class instance.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   */
  public function __construct(EntityInterface $entity) {
    $this->entity = $entity;
  }
  /**
   * Retrieves the node entity.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   */
  public function getEntity() {
    return $this->entity;
  }
}
  1. Define a new class with list of the custom events:
namespace Drupal\bbd_example\Event;
/**
 * Defines custom events.
 */
final class BbdExampleEvents {
  /**
   * Event after the node is removed.
   *
   * @Event
   *
   * @see \Drupal\bbd_example\Event\NodeDeleteEvent
   */
  const NODE_DELETE = NodeDeleteEvent::class;
}
  1. Dispatch the event NodeDeleteEvent::class in the hook_node_delete() in the bbd_example.module:
/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function bbd_example_node_delete(EntityInterface $entity) {
  $event = new NodeDeleteEvent($entity);
  \Drupal::service('event_dispatcher')->dispatch($event, BbdExampleEvents::NODE_DELETE);
}

All other steps to define an event subscriber service, new class that implements \Symfony\Component\EventDispatcher\EventSubscriberInterface, to declare the event for subscribing to in ::getSubscribedEvents() and to write the methods on the new class that respond to the event were described above. The name of event is BbdExampleEvents::NODE_DELETE.