Event Dispatcher

based on Symfony 2.8

Sommaire

  1. Problem
  2. Design Pattern
  3. How to use it
    1. Events
    2. Dispatcher
    3. Listeners
  4. Dig into Symfony

Problem

In order to have a good object oriented design we have to create lots of classes interacting one with each other. If certain principles are not applied the final framework will end in a total mess where each object relies on many other objects in order to run. In order to avoid tight coupled frameworks, we need a mechanism to facilitate the interaction between objects in a manner in that objects are not aware of the existence of other objects.

Source: oodesign.com/mediator-pattern.html

Design Pattern

Mediator Pattern

 

Communication between objects is encapsulated with a mediator object. Objects no longer communicate directly with each other, but instead communicate through the mediator. This reduces the dependencies between communicating objects, thereby lowering the coupling.

Source: en.wikipedia.org/wiki/Mediator_pattern

Mediator

Without

With

Symfony EventDispatcher

{
    "require": {
        "symfony/event-dispatcher" : "^2.7"
    }
}
$ composer require symfony/event-dispatcher
{
    "require": {
        "php": ">=5.3.9"
    },
    "require-dev": {
        "symfony/phpunit-bridge":       "~2.7",
        "symfony/dependency-injection": "~2.6",
        "symfony/expression-language":  "~2.6",
        "symfony/config":               "~2.0,>=2.0.5",
        "symfony/stopwatch":            "~2.3",
        "psr/log":                      "~1.0"
    },
    "suggest": {
        "symfony/dependency-injection": "",
        "symfony/http-kernel": ""
    }
}

Install

1. Events

2. Dispatcher

3. Listeners

Event Names

have a unique name

kernel.request

kernel.response

form.bind

form.post_set_data

console.command

console.exception

Event Names

<?php

namespace Symfony\Component\HttpKernel;

final class KernelEvents
{
    /**
     * The REQUEST event occurs at the very beginning of request
     * dispatching.
     *
     * This event allows you to create a response for a request before any
     * other code in the framework is executed. The event listener method
     * receives a Symfony\Component\HttpKernel\Event\GetResponseEvent
     * instance.
     */
    const REQUEST = 'kernel.request';

    /**
     * The EXCEPTION event occurs when an uncaught exception appears.
     *
     * This event allows you to create a response for a thrown exception or
     * to modify the thrown exception. The event listener method receives
     * a Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent
     * instance.
     */
    const EXCEPTION = 'kernel.exception';

    // ...
}

Event Objects

Event
- propagationStopped: bool = false
+ isPropagationStopped(): bool
+ stopPropagation()

Instances of

Symfony\Component\EventDispatcher\Event

Event Objects

Symfony\Component\EventDispatcher\GenericEvent

GenericEvent
- subject : mixed
- arguments : array
+ getSubject(): mixed
+ getArgument(string): mixed
+ setArgument(string, mixed): mixed
+ getArguments(): mixed
+ setArguments(array): mixed
+ hasArgument(string): bool
Event
\ArrayAccess
\IteratorAggregate

Dispatcher

Use a Single Instance

$dispatcher = $this->getContainer()->get('event_dispatcher');

Dispatcher

Symfony\Component\EventDispatcher\EventDispatcherInterface
+ dispatch(string, Event $event = null): Event
+ addListeners(string, callable, int = 0)
+ addSubscriber(EventSubscriberInterface)
+ removeListener(string, callable)
+ removeSubscriber(EventSubscriberInterface)
+ getListeners(string = null)
+ hasListeners(string = null)

Dispatcher

$dispatcher = $this->getContainer()->get('event_dispatcher');

// event name only
$dispatcher->dispatch('store.order');

// with event object
$event = new StoreOrderEvent($order);
$dispatcher->dispatch(StoreEvents::STORE_ORDER, $event);

Dispatcher Extras

Symfony\Component\EventDispatcher\

ImmutableEventDispatcher

Symfony\Component\EventDispatcher\

ContainerAwareEventDispatcher

Symfony\Component\EventDispatcher\

Debug\TracableEventDispatcher

Listeners

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();

$listener = function (Event $event) {
    echo 'A';
};

$dispatcher->addListener(
    'example',     // Event name to listen
    $listener,     // A PHP callable (aka callback)
    $priority = 0  // Priority 
);

Reminder: PHP Callable

function my_callback_function() {
    echo 'hello world!';
}

class MyClass {
    static function myCallbackMethod() {
        echo 'Hello World!';
    }
}

// Type 1: Simple callback
call_user_func('my_callback_function'); 

// Type 2: Static class method call
call_user_func(array('MyClass', 'myCallbackMethod')); 

// Type 3: Object method call
$obj = new MyClass();
call_user_func(array($obj, 'myCallbackMethod'));

// Type 4: Static class method call (As of PHP 5.2.3)
call_user_func('MyClass::myCallbackMethod');

Source: php.net/manual/en/language.types.callable.php

// Type 5: Relative static class method call (As of PHP 5.3.0)
class A {
    public static function who() {
        echo "A\n";
    }
}

class B extends A {
    public static function who() {
        echo "B\n";
    }
}

call_user_func(array('B', 'parent::who')); // A

// Type 6: Objects implementing __invoke can be used as callables (since PHP 5.3)
class C {
    public function __invoke($name) {
        echo 'Hello ', $name, "\n";
    }
}

$c = new C();
call_user_func($c, 'PHP!');
// Anonymous functions
$listener = function (Event $e) {
    echo "C\n";
}

$dispatcher->addListener('example', $listener);

Subscribers

Symfony\Component\EventDispatcher\EventSubscriberInterface
+ getSubscribedEvents(): array

Subscribers

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class EventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            'kernel.request' => 'onKernelRequest',
            'user.update' => ['onUserUpdate', 5],
            'blog_post.saved' => [
                ['updateRss', 10],
                ['sendEmails', 5],
            ]
        ];
    }

    public function onKernelRequest(GetResponseEvent $event) { /** ... */ }
    public function onUserUpdate(GenericEvent $event) { /** ... */ }
    public function updateRss(Event $event) { /** ... */ }
    public function sendEmails(Event $event) { /** ... */ }
}

$dispatcher = new EventDispatcher();

$subscriber = new EventSubscriber();
$dispatcher->addSubscriber($subscriber);

Stop propagation

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();

$dispatcher->addListener('example', function (Event $event) {
    echo 'A';
    $event->stopPropagation();
});
$dispatcher->addListener('example', function (Event $event) {
    echo 'B';
});

$dispatcher->dispatch('example'); // output : A

Dig in Symfony

  1. Workflow
  2. List of events
  3. Service definition
  4. Tools

Workflow

List of Events

Kernel

Console

Form

Security

Doctrine

Kernel

Name KernelEvents Constant Event Object
kernel.request KernelEvents::REQUEST GetResponseEvent
kernel.controller KernelEvents::CONTROLLER FilterControllerEvent
kernel.view KernelEvents::VIEW GetResponseForControllerResultEvent
kernel.response KernelEvents::RESPONSE FilterResponseEvent
kernel.finish_request KernelEvents::FINISH_REQUEST FinishRequestEvent
kernel.terminate KernelEvents::TERMINATE PostResponseEvent
kernel.exception KernelEvents::EXCEPTION GetResponseForExceptionEvent

Console

Name ConsoleEvents Constant Event Object
console.command ConsoleEvents::COMMAND ConsoleCommandEvent
console.terminate ConsoleEvents::TERMINATE ConsoleTerminateEvent
console.exception ConsoleEvents::EXCEPTION ConsoleExceptionEvent

Form

Name FormEvents Constant Event Object
form.pre_bind FormEvents::PRE_SUBMIT FormEvent
form.bind FormEvents::SUBMIT FormEvent
form.post_bind FormEvents::POST_SUBMIT FormEvent
form.pre_set_data FormEvents::PRE_SET_DATA FormEvent
form.post_set_data FormEvents::POST_SET_DATA FormEvent

Security

Name SecurityEvents Constant Event Object
security.interactive_login SecurityEvents::INTERACTIVE_LOGIN InteractiveLoginEvent
security.switch_user SecurityEvents::SWITCH_USER SwitchUserEvent
Name AuthenticationEvents Constant Event Object
security.authentication.success AuthenticationEvents::AUTHENTICATION_SUCCESS AuthenticationEvent
security.authentication.failure AuthenticationEvents::AUTHENTICATION_FAILURE AuthenticationFailureEvent

Doctrine

Name Events Constant Event Object
preRemove Events::preRemove ​LifecycleEventArgs
postRemove Events::postRemove ​LifecycleEventArgs
prePersist Events::prePersist ​LifecycleEventArgs
postPersist Events::postPersist ​LifecycleEventArgs
preUpdate Events::preUpdate PreUpdateEventArgs
postUpdate Events::postUpdate ​LifecycleEventArgs
postLoad Events::postLoad LifecycleEventArgs
loadClassMetadata Events::loadClassMetadata ​LifecycleEventArgs
preFlush Events::preFlush ​PreFlushEventArgs
onFlush Events::onFlush LifecycleEventArgs
postFlush Events::postFlush LifecycleEventArgs
onClear Events::onClear LifecycleEventArgs

Service definition

services:
    app.store_manager:
        class: AppBundle\Manager\Store
        arguments:
            - @event_dispatcher

    app.user_event_listener:
        class: AppBundle\EventListener\UserEventListener
        tags:
            - { name: kernel.event_listener,
                event: user.login,
                method: onLogin }
            - { name: kernel.event_listener,
                event: user.update,
                method: onUpdate,
                priority: -1 }

    app.store_event_listener:
        class: AppBundle\EventListener\StoreListener
        tags:
            - { name: kernel.event_subscriber }

Tags:

kernel.event_listener
kernel.event_subscriber

Service:

event_dispatcher

/!\ Doctrine /!\

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver: pdo_sqlite
                memory: true

services:
    my.listener:
        class: Acme\SearchBundle\EventListener\SearchIndexer
        tags:
            - { name: doctrine.event_listener, event: postPersist }
    my.listener2:
        class: Acme\SearchBundle\EventListener\SearchIndexer2
        tags:
            - { name: doctrine.event_listener, event: postPersist, connection: default }
    my.subscriber:
        class: Acme\SearchBundle\EventListener\SearchIndexerSubscriber
        tags:
            - { name: doctrine.event_subscriber, connection: default }

Tags:

doctrine.event_listener
doctrine.event_subscriber

Tools

Commands

$ app/console debug:event-dispatcher -h
Usage:
  debug:event-dispatcher [options] [--] [<event>]

Arguments:
  event                    An event name

Options:
      --format=FORMAT      The output format  (txt, xml, json, or md) [default: "txt"]
      --raw                To output raw description
      ...

Help:
 The debug:event-dispatcher command displays all configured listeners:

   php app/console debug:event-dispatcher

 To get specific listeners for an event, specify its name:

   php app/console debug:event-dispatcher kernel.request
$ app/console debug:event-dispatcher
[event_dispatcher] Registered listeners by event

[Event] console.command
+-------+-------------------------------------------------------------------------------+
| Order | Callable                                                                      |
+-------+-------------------------------------------------------------------------------+
| #1    | Symfony\Component\HttpKernel\EventListener\DebugHandlersListener::configure() |
| #2    | Symfony\Bridge\Monolog\Handler\ConsoleHandler::onCommand()                    |
| #3    | Symfony\Bridge\Monolog\Handler\ConsoleHandler::onCommand()                    |
+-------+-------------------------------------------------------------------------------+

[Event] console.terminate
+-------+-----------------------------------------------------------------------------------+
| Order | Callable                                                                          |
+-------+-----------------------------------------------------------------------------------+
| #1    | Symfony\Bundle\SwiftmailerBundle\EventListener\EmailSenderListener::onTerminate() |
| #2    | Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate()                      |
| #3    | Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate()                      |
+-------+-----------------------------------------------------------------------------------+

[Event] kernel.controller
+-------+-----------------------------------------------------------------------------------------------+
| Order | Callable                                                                                      |
+-------+-----------------------------------------------------------------------------------------------+
| #1    | Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector::onKernelController()        |
| #2    | Symfony\Component\HttpKernel\DataCollector\RequestDataCollector::onKernelController()         |
| #3    | Sensio\Bundle\FrameworkExtraBundle\EventListener\ControllerListener::onKernelController()     |
| #4    | Sensio\Bundle\FrameworkExtraBundle\EventListener\ParamConverterListener::onKernelController() |
| #5    | Sensio\Bundle\FrameworkExtraBundle\EventListener\HttpCacheListener::onKernelController()      |
| #6    | Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener::onKernelController()       |
| #7    | Sensio\Bundle\FrameworkExtraBundle\EventListener\TemplateListener::onKernelController()       |
+-------+-----------------------------------------------------------------------------------------------+

[Event] kernel.exception
+-------+-----------------------------------------------------------------------------------+
| Order | Callable                                                                          |
+-------+-----------------------------------------------------------------------------------+
| #1    | Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelException()  |
| #2    | Symfony\Component\HttpKernel\EventListener\ExceptionListener::onKernelException() |
+-------+-----------------------------------------------------------------------------------+

[Event] kernel.finish_request
+-------+----------------------------------------------------------------------------------------+
| Order | Callable                                                                               |
+-------+----------------------------------------------------------------------------------------+
| #1    | Symfony\Component\HttpKernel\EventListener\LocaleListener::onKernelFinishRequest()     |
| #2    | Symfony\Component\HttpKernel\EventListener\TranslatorListener::onKernelFinishRequest() |
| #3    | Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelFinishRequest()     |
| #4    | Symfony\Component\Security\Http\Firewall::onKernelFinishRequest()                      |
+-------+----------------------------------------------------------------------------------------+

Symfony Profiler

What next ?

https://github.com/symfony/symfony/pull/10823

Must read/watch

Source code https://github.com/symfony/EventDispatcher
Documentation http://symfony.com/doc/current/components/event_dispatcher
How Kris Builds Symfony Apps https://youtu.be/W8MOIOyPbmM
EventDispatcher in C https://github.com/symfony/symfony/pull/10823

Questions

?

Symfony Event Dispatcher

By MyKiwi

Symfony Event Dispatcher

  • 531