Events: The Object Oriented Hook System

Nida Ismail Shah

Developer at Acquia

d.o & twitter: @nidaismailshah

nidashah.com/blog

Gulmarg, Kashmir

Overview

Kolahoi Peak, Kashmir

Overview

  • The Symfony Event Dispatcher component.

    • Installation and usage

    • Creating and dispatching an event

    • Subscribing/Listening to events

  • Events in Drupal 8

    • Creating Events.

    • Subscribing to Events

Intent/Purpose

Dal Lake, Kashmir

The idea is to be able to run random code at given places in the engine. This random code should then be able to do whatever needed to enhance the functionality. The places where code can be executed are called “hooks” and are defined by a fixed interface.

~ Dries Buytaert.

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

 an event is an action or an occurrence recognised by software that may be handled by software.

event?

Extensibility

Modularity

Maintainability
Developer Experience

Background

Tulian Lake,

Pahalgam, Kashmir

Extensibility

Hooks

Plugins

Mulitple instances

Admin forms

Configuration

Tagged Services

Simple Extensions

Events

Alter something?React to something?

Symfony Event Dispatcher Component

Installation and usage

Installing the symfony event dispatcher component


composer require symfony/event-dispatcher


Using version ^3.2 for symfony/event-dispatcher
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing symfony/event-dispatcher (v3.2.6)
    Loading from cache

symfony/event-dispatcher suggests installing symfony/dependency-injection ()
symfony/event-dispatcher suggests installing symfony/http-kernel ()
Writing lock file
Generating autoload files

Text

Text

Using the symfony event dispatcher component

  1. Event - representing the event or the state of the application.

  2. Dispatcher - to notify the subscribers or listeners about the occurrence of the event.

  3. Subscriber/Listener - to extend the application once the event has occurred.

Pub-Sub pattern

  • The pub sub exemplifies the proper decoupling of components of an application.

  • Publishers publish the messages into classes without knowledge of which subscribers would be interested in the message.

  • Subscribers express interest in one or more classes and only receive messages that are of interest, without knowledge of which publishers.

Mediator pattern

  • Define an object that encapsulates how a set of objects interact.

  • Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

  • Design an intermediary to decouple many peers.

Workflow

Workflow

  • A listener (PHP object) tells a central dispatcher object that it wants to listen to the 'xyz' event

  • At some point, Symfony tells the dispatcher object to dispatch the 'xyz' event, passing with it an Event object that has access to the Object defining the state of the application at that point.

  • The dispatcher notifies (i.e. calls a method on) all listeners of the 'xyz' event, allowing each of them to make modifications to the State object.

Components

  • The dispatcher object

  • The Event object

  • The subscriber/listener

the dispatcher


// create an EventDispatcher instance.

$dispatcher = new EventDispatcher();



// the order is somehow created or retrieved
// contains the state of our application
// or the information we want expose.

$order = new Order();


// ...



// create the OrderPlacedEvent and dispatch it

$event = new OrderPlacedEvent($order);



// dispatch the event.


$dispatcher->dispatch(OrderPlacedEvent::NAME, $event);


// or $dispatcher->dispatch('order.placed', $event);

the event




/**

 * The order.placed event is dispatched each time an order is created

 * in the system.
 
*/


class OrderPlacedEvent extends Event {


  
    const NAME = 'order.placed';



    protected $order;

 
 
    public function __construct(Order $order) {

        $this->order = $order;
  
    }

  

    public function getOrder() {
    
        return $this->order;
  
    }

}

the base event

/**
 * Event is the base class for classes containing event data.
 * This class contains no event data. It is used by events that do not pass
 * state information to an event handler when an event is raised.
 */
class Event
{
    /**
     * @var bool Whether no further event listeners should be triggered
     */
    private $propagationStopped = false;

    /**
     * Returns whether further event listeners should be triggered.
     */
    public function isPropagationStopped()
    {
        return $this->propagationStopped;
    }

    /**
     * Stops the propagation of the event to further event listeners.
     */
    public function stopPropagation()
    {
        $this->propagationStopped = true;
    }

}

The base Event class provided by the Event Dispatcher component is deliberately sparse to allow the creation of API specific event objects by inheritance using OOP. This allows for elegant and readable code in complex applications.

the subscriber

class StoreSubscriber implements EventSubscriberInterface
 {
  

    public static function getSubscribedEvents()
  {
 
       return array(
      
            KernelEvents::RESPONSE => array(
 
               array('onKernelResponsePre', 10),
  
               array('onKernelResponsePost', -10),
     
             ),
     
             OrderPlacedEvent::NAME => 'onStoreOrder',
    
        );
  
    }

  

    public function onKernelResponsePre(FilterResponseEvent $event)
  {
    
    // do something.
  
  
    }

  
    public function onKernelResponsePost(FilterResponseEvent $event)
  {
    
    // do something.
  
  
    }

  
  

  public function onStoreOrder(OrderPlacedEvent $event)
 {
    
    // do something.
  
  
    }

  

}

the listener

class AcmeListener
 {

 
    // ...

  
    public function onFooAction(Event $event)
  {


        // ... do something
  
    }

}


// This is very similar to a subscriber class, 
// except that the class itself cant tell the dispatcher which events it should listen to.

register listener/subscriber


// create an EventDispatcher instance.

$dispatcher = new EventDispatcher();



$subscriber = new StoreSubscriber();

// Register subscriber

$dispatcher->addSubscriber($subscriber);



// add a listener

$listener = new AcmeListener();

$dispatcher->addListener('acme.foo.action', array($listener, 'onFooAction'));



// create the OrderPlacedEvent and dispatch it

$event = new OrderPlacedEvent($order);



// dispatch the event.

$dispatcher->dispatch(OrderPlacedEvent::NAME, $event);

// or $dispatcher->dispatch(order.placed, $event);

other ways to register

# app/config/services.yml
services:
    kernel.listener.your_listener_name:
        class: AppBundle\EventListener\AcmeExceptionListener
        tags:
            - { name: kernel.event_listener, event: kernel.exception, method:
onKernelException }


With the use of ContainerAwareEventDispatcher and dependency injection:

  • Use the RegisterListenersPass to tag services as event listeners/subscribers.

  • Define event subscriber/listener as a service.

  • Tag them as kernel.event_listener or kernel.event_subscriber. 

subscriber vs listener

  • Event listeners and Subscribers serve the same purpose and can be used in an application indistinctly.

  • Event listeners can be added via service definition and also with addListener()method.

  • Event subscribers are added via service definition and by implementing the getSubscribedEvents() method and also with addSubscriber() method.

  • Event subscribers are easier to use and reuse.

  • Event listener is registered specifying the events on which it listens. The subscriber has a method telling the dispatcher what events it is listening to.

  • More here: http://nidashah.com/drupal/events-and-listeners.html

more dispatchers

  • ContainerAwareEventDispatcher

    • Use services within your events, and subscribers as services

  • TraceableEventDispatcher

    • wraps any other event dispatcher and can then be used to determine which event listeners have been called by the dispatcher

  • ImmutableEventDispatcher

    • is a locked or frozen event dispatcher. The dispatcher cannot register new listeners or subscribers.

ContainerAwareEventDispatcher

 

  • The ContainerAwareEventDispatcher is a special Event Dispatcher implementation which is coupled to the service container that is part of the DependencyInjection component.

  • It allows services to be specified as event listeners making the EventDispatcher extremely powerful.

  • Services are lazy loaded meaning the services attached as listeners will only be created if an event is dispatched that requires those listeners.

 

ContainerAwareEventDispatcher

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;

$container = new ContainerBuilder();
$dispatcher = new ContainerAwareEventDispatcher($container);

// Add the listener and subscriber services
$dispatcher->addListenerService($eventName, array('foo', 'logListener'));

$dispatcher->addSubscriberService(
    'kernel.store_subscriber',
    'StoreSubscriber'
);

TraceableEventDispatcher

The TraceableEventDispatcher is an event dispatcher that wraps any other event dispatcher and can then be used to determine which event listeners have been called by the dispatcher.

// the event dispatcher to debug
$eventDispatcher = ...;

$traceableEventDispatcher = new TraceableEventDispatcher( $eventDispatcher, new Stopwatch()
);

$traceableEventDispatcher->addListener(
    'event.the_name',
    $eventListener,
    $priority
);

// dispatch an event
$traceableEventDispatcher->dispatch('event.the_name', $event);


$calledListeners = $traceableEventDispatcher->getCalledListeners();
$notCalledListeners = $traceableEventDispatcher->getNotCalledListeners();

ImmutableEventDispatcher

 

  • The ImmutableEventDispatcher is a locked or frozen event dispatcher. The dispatcher cannot register new listeners or subscribers.

  • The ImmutableEventDispatcher takes another event dispatcher with all the listeners and subscribers. The immutable dispatcher is just a proxy of this original dispatcher.

  • Using it

    • first create a normal dispatcher (EventDispatcher or ContainerAwareEventDispatcher) and register some listeners or subscribers

    • Now, inject that into an ImmutableEventDispatcher

 

ImmutableEventDispatcher

use Symfony\Component\EventDispatcher\EventDispatcher;

use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;

$dispatcher = new EventDispatcher();
$dispatcher->addListener('foo.action', function ($event) {
    // ...
});

// ...

// ...

$immutableDispatcher = new ImmutableEventDispatcher($dispatcher);

Events in Drupal 8

  • Events are part of the Symfony framework: they allow for different components of the system to interact and communicate with each other.

  • Object oriented way of interaction with core and other modules.

  • Mediator Pattern

  • Container Aware dispatcher

  • Will probably replace hooks in future drupal versions.

  • Since Drupal is using ContainerAwareEventDispatcher, we always have the dispatcher object available as a service.

  • Consequently, Drupal supports the service definition way of adding event subscribers.

  • Service definition way of adding event listeners is not supported.

something to note

  1. Get the dispatcher object from the service container.

  2. Create the event.

  3. Dispatch the event.

  4. Define a service tagged with event_subscriber in services.yml.

  5. Implement the EventSubscriberInterface to write getSubscribedEvents() method to return what events you want to subscribe to.

Workflow in Drupal

event subscriber class

class ConfigFactory implements ConfigFactoryInterface, EventSubscriberInterface {

    
static function getSubscribedEvents() {
  
        $events[ConfigEvents::SAVE][] = array('onConfigSave', 255);
  
        $events[ConfigEvents::DELETE][] = array('onConfigDelete', 255);
  
        return $events;

    }

}

// services.yml
config.factory:
  
    class: Drupal\Core\Config\ConfigFactory
  
    tags:
    
        - { name: event_subscriber }
    
        - { name: service_collector, tag: 'config.factory.override', call: addOverride }
  
    arguments: ['@config.storage', '@event_dispatcher', ‘@config.typed']

services:
  
    event_demo.alter_response:
    
        class: Drupal\event_demo\EventSubscriber\AlterResponse
    
        arguments: [ '@logger.factory' ]
    
        tags:
      
            - { name: event_subscriber }

dispatching the event

$dispatcher = \Drupal::service('event_dispatcher');


// or inject as a dependency

$event = new EventDemo($config);



$event = $dispatcher->dispatch(EVENT_NAME, $event);

core registering event subscribers

namespace Drupal\Core\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

/**
 * Registers all event subscribers to the event dispatcher.
 */
class RegisterEventSubscribersPass implements CompilerPassInterface {

  /**
   * {@inheritdoc}
   */
  public function process(ContainerBuilder $container) {
    if (!$container->hasDefinition('event_dispatcher')) {
      return;
    }

    $definition = $container->getDefinition('event_dispatcher');

    $event_subscriber_info = [];
    foreach ($container->findTaggedServiceIds('event_subscriber') as $id => $attributes) {

      // We must assume that the class value has been correctly filled, even if
      // the service is created by a factory.
      $class = $container->getDefinition($id)->getClass();

      $refClass = new \ReflectionClass($class);
      $interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface';
      if (!$refClass->implementsInterface($interface)) {
        throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
      }

      // Get all subscribed events.
      foreach ($class::getSubscribedEvents() as $event_name => $params) {
        if (is_string($params)) {
          $priority = 0;
          $event_subscriber_info[$event_name][$priority][] = ['service' => [$id, $params]];
        }
        elseif (is_string($params[0])) {
          $priority = isset($params[1]) ? $params[1] : 0;
          $event_subscriber_info[$event_name][$priority][] = ['service' => [$id, $params[0]]];
        }
        else {
          foreach ($params as $listener) {
            $priority = isset($listener[1]) ? $listener[1] : 0;
            $event_subscriber_info[$event_name][$priority][] = ['service' => [$id, $listener[0]]];
          }
        }
      }
    }

    foreach (array_keys($event_subscriber_info) as $event_name) {
      krsort($event_subscriber_info[$event_name]);
    }

    $definition->addArgument($event_subscriber_info);
  }

}
  • KernelEvents::CONTROLLER, EXCEPTION, REQUEST, RESPONSE, TERMINATE, VIEW

  • ConfigEvents::DELETE, IMPORT, SAVE, RENAME ...

  • EntityTypeEvents::CREATE, UPDATE, DELETE

  • FieldStorageDefinitionEvents::CREATE, UPDATE, DELETE

  • ConsoleEvents::COMMAND, EXCEPTION, TERMINATE

  • MigrateEvents:: MAP_DELETE, MAP_SAVE, POST_IMPORT, POST_ROLLBACK, POST_ROW_DELETE, POST_ROW_SAVE,

  • RoutingEvents::ALTER, DYNAMIC, FINISHED

Events in Drupal 8 core

path forward

  • Writing your own module?

    • trigger an Event for everything.

  • Interacting with or alter core?

    • subscribe to an event (if one is fired).

    • Hooks … you don't have too many options.

  • Configuration, Admin forms?

    • Plugins

  • Simple Extensions

    • Tagged services

summary

demo

Questions?

Hazratbal, Kashmir

Join Us for Contribution Sprints

Friday, April 28, 2017

First-Time Sprinter Workshop
9:00am-12:00pm
Room: 307-308

 

Mentored Core Sprint
9:00am-12:00pm
Room:301-303

 

General Sprints
9:00am-6:00pm
Room:309-310

 

#drupalsprints

WHAT DID

YOU THINK?

Locate this session at the DrupalCon Baltimore website:

http://baltimore2017.drupal.org/schedule

Take the survey!

https://www.surveymonkey.com/r/drupalconbaltimore

THANK YOU!

email: nida@nidashah.com
twitter: @nidaismailshah

Events: The Object Oriented Hook System

By Nida Ismail Shah

Events: The Object Oriented Hook System

  • 890