Inside Drupal 8 Events

Inside Drupal 8 Events image

Earlier Drupal versions have a hook system, a way of letting other developers write code that interacts with core code in a clean and organized manner. System events are defined, and when they occur, the entire application is notified, and all the code associated with the event is run.

The hook system is based on the naming and definition of functions. When certain events occur, e.g. a form has been built, a node is being inserted into the database, a user has just logged in, etc., Drupal asks all modules a question like, "Anything else?" or "What would you like to do now?". Drupal does this by looking for functions matching a particular pattern. It then runs all matched functions. Core and custom code are written in uniquely-named modules, so that is the ideal path to follow in order to identify any special functions. They are named as MODULE_HOOK. For example, to invoke a user_login hook in login_example module, Drupal will look for a function called login_example_user_login().

The EventDispatcher component of Symfony takes an Object-Oriented Programming (OOP) approach to having components of an application communicate with each other by dispatching events and listening to them.

Kernel Events

The HttpKernel comes with the following key events, represented by the following constants:

// vendor/symfony/http-kernel/KernelEvents.php

// when a request is beginning to be dispatched 
KernelEvents::REQUEST
// when an uncaught exception appears
KernelEvents::EXCEPTION
// when a controller has returned anything that is not a Response
KernelEvents::VIEW 
// when a controller has been found for handling a request
KernelEvents::CONTROLLER
// when a response has been created for replying to a request 
KernelEvents::RESPONSE
// when a response has been sent
KernelEvents::TERMINATE
// when a response has been generated for a request
KernelEvents::FINISH_REQUEST

Core Events

Drupal core builds upon the EventDispatcher component by defining more events, and the constants representing them are as follows:


// core/lib/Drupal/Core/Config/ConfigEvents.php
// when information on all config collections is collected
ConfigEvents::COLLECTION_INFO	
// when deleting a configuration object
ConfigEvents::DELETE
// when importing configuration to target storage
ConfigEvents::IMPORT
// when validating imported configuration
ConfigEvents::IMPORT_VALIDATE	
// when renaming a configuration object
ConfigEvents::RENAME
// when saving a configuration object
ConfigEvents::SAVE	

// core/lib/Drupal/Core/Entity/EntityTypeEvents.php
// when a new entity type is created
EntityTypeEvents::CREATE
// when an existing entity type is deleted
EntityTypeEvents::DELETE	
// when an existing entity type is updated
EntityTypeEvents::UPDATE	

// core/lib/Drupal/Core/Field/FieldStorageDefinitionEvents.php
// when a field storage definition is created
FieldStorageDefinitionEvents::CREATE	 
// when a field storage definition is deleted
FieldStorageDefinitionEvents::DELETE	 
// when a field storage definition is updated
FieldStorageDefinitionEvents::UPDATE	 

// core/modules/language/src/Config/LanguageConfigOverrideEvents.php
// when deleting a configuration override
LanguageConfigOverrideEvents::DELETE_OVERRIDE
// when saving a configuration override 
LanguageConfigOverrideEvents::SAVE_OVERRIDE 

// core/modules/locale/src/LocaleEvents.php
// when saving a translated string
LocaleEvents::SAVE_TRANSLATION	

// core/modules/migrate/src/Event/MigrateEvents.php
// when saving a message to the idmap
MigrateEvents::IDMAP_MESSAGE	
// when removing an entry from a migration's map
MigrateEvents::MAP_DELETE
// when saving to a migration's map 
MigrateEvents::MAP_SAVE	
// when finishing a migration import operation
MigrateEvents::POST_IMPORT	
// when finishing a migration rollback operation
MigrateEvents::POST_ROLLBACK	
// when a single item has been deleted
MigrateEvents::POST_ROW_DELETE	
// when a single item has been imported
MigrateEvents::POST_ROW_SAVE	
// when beginning a migration import operation
MigrateEvents::PRE_IMPORT	
// when beginning a migration rollback operation
MigrateEvents::PRE_ROLLBACK	
// when about to delete a single item
MigrateEvents::PRE_ROW_DELETE	
// when about to import a single item
MigrateEvents::PRE_ROW_SAVE	

// core/lib/Drupal/Core/Render/RenderEvents.php
// when selecting a page display variant
RenderEvents::SELECT_PAGE_DISPLAY_VARIANT 

// core/lib/Drupal/Core/Routing/RoutingEvents.php
// when collecting routes to allow changes to them
RoutingEvents::ALTER	
// when collecting routes to allow to allow new ones
RoutingEvents::DYNAMIC
// when route building has finished
RoutingEvents::FINISHED	 

Other Events

There may be other events from contributed modules or vendor packages, such as ConsoleEvents in vendor/symfony/console/ConsoleEvents.php. Custom modules can also define new events.

Subscribing to Events

To hook into the events system of the EventDispatcher component, a subscriber class needs to:

  • implement the EventSubscriberInterface
  • implement getSubscribedEvents() method and return an array keyed by the event to listen to and one of the following as value:
    • the name of a method to call (the priority will default to 0)
    • an array of a method name to call and prioritize
    • an array of arrays comprising the method names and respective priorities (the default priority is 0, if left out)

To inform Drupal that we have subscribed to events, we need to add a services configuration file (MY_MODULE.services.yml) to our module.

Let us go through an example in core/modules/user/src/EventSubscriber/UserRequestSubscriber.php. If a user is logged in, every time a request has ended, the last access time is logged against the user's account. Also, we do not want to do this too frequently so we opt for not more than once every 180 seconds.

Step 1: We subscribe to the KernelEvents::TERMINATE event.

public static function getSubscribedEvents() {
    $events[KernelEvents::TERMINATE][] = ['onKernelTerminate', 300];
    return $events;
  }

The service id is KernelEvents::TERMINATE, the method we want to call is onKernelTerminate and the priority is 300.

Step 2: The onKernelTerminate() method needs to be implemented:

public function onKernelTerminate(PostResponseEvent $event) {
    if ($this->account->isAuthenticated() && REQUEST_TIME - $this->account->getLastAccessedTime() > Settings::get('session_write_interval', 180)) {
      $storage = $this->entityManager->getStorage('user');
      $storage->updateLastAccessTimestamp($this->account, REQUEST_TIME);
    }
  }

Here, the EntityManager allows us to save the data easily.

Step 3: The configuration is in user.services.yml:

services:
  user_last_access_subscriber:
    class: Drupal\user\EventSubscriber\UserRequestSubscriber
    arguments: ['@current_user', '@entity.manager']
    tags:
      - { name: event_subscriber }

@current_user and @entity.manager are services too.

Conclusion

This was an overview of the implementation of the EventDispatcher component in Drupal 8. There are lots of events to subscribe to, and if necessary, you can create your own. Let us save that for our next short article on creating and subscribing to events.

Recommended reading

  1. Events and Event Listeners
  2. Events

KEEP MOVING FORWARD

Deji Akala / code