Symfony Events

An Introduction To Symfony Events

Ole Rößner <o.roessner@neusta.de>

Symfony Events

In Theory

Symfony Events

In Theory

Observer Pattern

Symfony Events

In Theory

Mediator Pattern

Symfony Events

In Theory

SOLID

Open-Closed Principle

Single Responsibility Principle

Symfony Events

In Theory

Loose Coupling

Pro

  • Extremely flexible
  • Modular
  • Single responsibilities
  • Easy testable

Contra

  • Higher complexity
  • Scattered architecture
  • (Vendor lock-in)

Symfony Events

In Practice

Symfony Events

In Practice

<?php // public/index.php

use App\Kernel;
use Symfony\Component\HttpFoundation\Request;

require dirname(__DIR__).'/vendor/autoload.php';

// [...]

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Symfony Events

In Practice

Symfony Events

In Practice

Name KernelEvents Constant Argument passed to the listener
kernel.request KernelEvents::REQUEST RequestEvent
kernel.controller KernelEvents::CONTROLLER ControllerEvent
kernel.controller_arguments KernelEvents::CONTROLLER_ARGUMENTS ControllerArgumentsEvent
kernel.view KernelEvents::VIEW ViewEvent
kernel.response KernelEvents::RESPONSE ResponseEvent
kernel.finish_request KernelEvents::FINISH_REQUEST FinishRequestEvent
kernel.terminate KernelEvents::TERMINATE TerminateEvent
kernel.exception KernelEvents::EXCEPTION ExceptionEvent

Symfony Events

In Practice

Event
kernel.request
kernel.controller
kernel.controller_arguments
kernel.view
kernel.response
kernel.finish_request
kernel.terminate
kernel.exception

Symfony Events

In Practice

EventDispatcher

The Mediator

Symfony Events

In Practice

EventDispatcherInterface

<?php

namespace Symfony\Component\EventDispatcher;

use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;

interface EventDispatcherInterface extends ContractsEventDispatcherInterface
{
    public function addListener(string $eventName, callable $listener, int $priority = 0);

    public function addSubscriber(EventSubscriberInterface $subscriber);

    public function removeListener(string $eventName, callable $listener);

    public function removeSubscriber(EventSubscriberInterface $subscriber);

    public function getListeners(string $eventName = null);

    public function getListenerPriority(string $eventName, callable $listener);

    public function hasListeners(string $eventName = null);
}

\Symfony\Component\EventDispatcher\EventDispatcherInterface

Symfony Events

In Practice

EventDispatcherInterface

<?php

namespace Symfony\Contracts\EventDispatcher;

use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;

interface EventDispatcherInterface extends PsrEventDispatcherInterface
{
    public function dispatch(object $event, string $eventName = null): object;
}
<?php

namespace Psr\EventDispatcher;

interface EventDispatcherInterface
{
    public function dispatch(object $event);
}

\Psr\EventDispatcher\EventDispatcherInterface (PSR-14)

\Symfony\Contracts\EventDispatcher\EventDispatcherInterface

Symfony Events

In Practice

Event?

<?php declare(strict_types=1);

namespace Psr\EventDispatcher;

/**
 * Defines a dispatcher for events.
 */
interface EventDispatcherInterface
{
    /**
     * Provide all relevant listeners with an event to process.
     *
     * @param object $event
     *   The object to process.
     *
     * @return object
     *   The Event that was passed, now modified by listeners.
     */
    public function dispatch(object $event);
}

Symfony Events

In Practice

\Symfony\Contracts\EventDispatcher\Event?

<?php

namespace Symfony\Contracts\EventDispatcher;

use Psr\EventDispatcher\StoppableEventInterface;

class Event implements StoppableEventInterface
{
    private bool $propagationStopped = false;

    public function isPropagationStopped(): bool
    {
        return $this->propagationStopped;
    }

    public function stopPropagation(): void
    {
        $this->propagationStopped = true;
    }
}

Symfony Events

In Practice

Event Propagation

  • dispatch(RequestEvent $event)
    • listenerA
    • listenerB
    • listenerC ($event->stopPropagation())
    • listenerD (not called)

Symfony Events

In Practice

Listeners Order?

  • first registered, first called
  • specific priority
  • default priority: 0
  • execution order: high to low prio

Symfony Events

Listeners and Subscribers

Symfony Events

Listeners and Subscribers

  • any callable
    • invocable class
    • class method
    • anonymous function
  • receives following parameters
    • object $event
    • string $eventName
    • EventDispatcherInterface $eventDispatcher

Listeners

Symfony Events

Listeners and Subscribers

  • class implementing EventSubscriberInterface
    • public static function getSubscribedEvents(): iterable
  • collection of listeners in one class

Subscribers

Symfony Events

Listeners and Subscribers

<?php

namespace Symfony\Component\EventDispatcher;

interface EventSubscriberInterface
{
    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * The array keys are event names and the value can be:
     *
     *  * The method name to call (priority defaults to 0)
     *  * An array composed of the method name to call and the priority
     *  * An array of arrays composed of the method names to call and respective
     *    priorities, or 0 if unset
     *
     * For instance:
     *
     *  * ['eventName' => 'methodName']
     *  * ['eventName' => ['methodName', $priority]]
     *  * ['eventName' => [['methodName1', $priority], ['methodName2']]]
     *
     * The code must not depend on runtime state as it will only be called at compile time.
     * All logic depending on runtime state must be put into the individual methods handling the events.
     *
     * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
     */
    public static function getSubscribedEvents();
}

Symfony Events

Listeners and Subscribers

My First EventListener

<?php declare(strict_types=1);

namespace WebBundle\Ads\AdzoneLeft;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use WebBundle\Ads\AdManagerInterface;

final readonly class AdzoneLeftExceptionListener
{
    public function __construct(private AdManagerInterface $adManager)
    {
    }

    public function __invoke(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        if ($exception instanceof HttpExceptionInterface && Response::HTTP_NOT_FOUND === $exception->getStatusCode()) {
            $this->adManager->setVerticalAdZoneSize(AdzoneSize::S);
        }
    }
}

Symfony Events

Listeners and Subscribers

My First EventListener

# config/services.yaml
services:
    WebBundle\Ads\AdzoneLeft\AdzoneLeftExceptionListener:
        tags: [kernel.event_listener]

Symfony follows this logic to decide which method to call inside the event listener class:

  1. If the kernel.event_listener tag defines the method attribute, that's the name of the method to be called;
  2. If no method attribute is defined, try to call the __invoke() magic method (which makes event listeners invokable);
  3. If the __invoke() method is not defined either, throw an exception.

Symfony Events

Listeners and Subscribers

My First EventListener

# config/services.yaml
services:
    WebBundle\Ads\AdzoneLeft\AdzoneLeftExceptionListener:
        tags:
        - name: 'kernel.event_listener'
          method: 'onKernelException', 
          priority: 42
          # event: 'kernel.exception' # (prior to Symfony 5.4)

Symfony Events

Listeners and Subscribers

AsEventListener

<?php declare(strict_types=1);

namespace WebBundle\Ads\AdzoneLeft;

# [...]

#[AsEventListener]
final readonly class AdzoneLeftExceptionListener
{

    # [...]

    public function __invoke(ExceptionEvent $event): void
    {
        # [...]
    }
}

Symfony Events

Listeners and Subscribers

"EventSubscriber" with AsEventListener

<?php 

namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: CustomEvent::class, method: 'onCustomEvent')]
#[AsEventListener(event: 'foo', priority: 42)]
#[AsEventListener(event: 'bar', method: 'onBarEvent')]
final readonly class MyMultiListener
{
    public function onCustomEvent(CustomEvent $event): void
    {
        // ...
    }

    public function onFoo(): void
    {
        // ...
    }

    public function onBarEvent(): void
    {
        // ...
    }
}

Symfony Events

Listeners and Subscribers

"EventSubscriber" with AsEventListener

<?php 

namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

final readonly class MyMultiListener
{
    #[AsEventListener()]
    public function onCustomEvent(CustomEvent $event): void
    {
        // ...
    }

    #[AsEventListener(event: 'foo', priority: 42)]
    public function onFoo(): void
    {
        // ...
    }

    #[AsEventListener(event: 'bar')]
    public function onBarEvent(): void
    {
        // ...
    }
}

Symfony Events

Listeners and Subscribers

Listeners or Subscribers?

Listeners and subscribers can be used in the same application indistinctly. The decision to use either of them is usually a matter of personal taste. However, there are some minor advantages for each of them:

  • Subscribers are easier to reuse because the knowledge of the events is kept in the class rather than in the service definition. This is the reason why Symfony uses subscribers internally.
  • Listeners are more flexible because bundles can enable or disable each of them conditionally depending on some configuration value.
  • Listeners follow PSR, Subscribers are a Symfony feature.

Symfony Events

Custom Events

Symfony Events

Custom Events

final readonly class CustomMailer
{
    public function __construct(EventDispatcherInterface $dispatcher) {}

    public function send(string $subject, string $message): mixed
    {
        // dispatch an event before the method
        $preEvent = new BeforeSendMailEvent($subject, $message);
        $this->dispatcher->dispatch($preEvent);

        // get $subject and $message from the event, they may have been modified
        $subject = $preEvent->getSubject();
        $message = $preEvent->getMessage();

        // the real method implementation is here
        $returnValue = ...;

        // do something after the method
        $postEvent = new AfterSendMailEvent($returnValue);
        $this->dispatcher->dispatch($postEvent);

        return $postEvent->getReturnValue();
    }
}

Open-Closed Principle!

Single Responsibility Principle!

Symfony Events

Custom Events

<?php declare(strict_types=1);

namespace WebBundle\Event\Subscriber\FontSubmission;

use Symfony\Component\EventDispatcher\EventDispatcherInterface

#[AsEventListener(FileUploadedEvent::class, method: 'moveUploadedFile')]
final readonly class UploadedFileSubscriber
{
    public function __construct(private UploadedFileMover $fileMover) {}

    public function moveUploadedFile(FileUploadedEvent $uploadedEvent, string $_, EventDispatcherInterface $dispatcher): void
    {
        $event = $this->getUploadEvent($uploadedEvent);
        $uploadedFile = $event->getUploadedFile();
        $event->setTemporaryZip(
            $this->fileMover->moveUploadedFile(
                $uploadedFile,
                sprintf('%s.%s', $uploadedFile->getFilename(), $uploadedFile->getClientOriginalExtension())
            )
        );
        $dispatcher->dispatch(UploadedFileMovedEvent::decorate($event));
    }
}

Event Chaining

(<Event> $event, string $eventName, EventDispatcherInterface $dispatcher)
$eventDispatcher->dispatch(new PresentationFinishedEvent());
#[AsEventListener]
public function anyQuestions(PresentationFinishedEvent $event): void
{
    $event->pleaseAsk();
}

Symfony Events and Event Listeners

By Ole Rößner

Symfony Events and Event Listeners

An Introduction To Symfony Events

  • 180