Ole Rößner
Spreche fließend Nerddeutsch! #nerd #clean_coder #father #rampensau #speaker #symfony | Organizer of @phpughb, Developer at @teamneusta, #traefikambassador
Open-Closed Principle
Single Responsibility Principle
<?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);
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 |
Event |
---|
kernel.request |
kernel.controller |
kernel.controller_arguments |
kernel.view |
kernel.response |
kernel.finish_request |
kernel.terminate |
kernel.exception |
@event_dispatcher
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
<?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
<?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);
}
<?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;
}
}
object $event
string $eventName
EventDispatcherInterface $eventDispatcher
public static function getSubscribedEvents(): iterable
collection of listeners in one class
<?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();
}
<?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);
}
}
}
# 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:
kernel.event_listener
tag defines the method
attribute, that's the name of the method to be called;method
attribute is defined, try to call the __invoke()
magic method (which makes event listeners invokable);__invoke()
method is not defined either, throw an exception.# 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)
<?php declare(strict_types=1);
namespace WebBundle\Ads\AdzoneLeft;
# [...]
#[AsEventListener]
final readonly class AdzoneLeftExceptionListener
{
# [...]
public function __invoke(ExceptionEvent $event): void
{
# [...]
}
}
<?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
{
// ...
}
}
<?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
{
// ...
}
}
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:
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!
<?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> $event, string $eventName, EventDispatcherInterface $dispatcher)
$eventDispatcher->dispatch(new PresentationFinishedEvent());
#[AsEventListener]
public function anyQuestions(PresentationFinishedEvent $event): void
{
$event->pleaseAsk();
}
By Ole Rößner
An Introduction To Symfony Events
Spreche fließend Nerddeutsch! #nerd #clean_coder #father #rampensau #speaker #symfony | Organizer of @phpughb, Developer at @teamneusta, #traefikambassador