Что такое Symfony events и чем event listeners отличаются от subscribers?
Event Listener — отдельный класс с ручной регистрацией через тег в services.yaml; Event Subscriber реализует EventSubscriberInterface и объявляет все обрабатываемые события внутри себя, регистрируясь автоматически.
Система событий в Symfony
Symfony реализует паттерн Observer через компонент EventDispatcher. Приложение генерирует события (объекты, наследующие Symfony\Contracts\EventDispatcher\Event), а слушатели реагируют на них. Это позволяет расширять поведение без изменения исходного кода.
Event Listener
Listener — обычный PHP-класс с произвольным методом-обработчиком. Он регистрируется через конфигурацию сервиса с тегом kernel.event_listener.
<?php
// src/EventListener/ExceptionListener.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
class ExceptionListener
{
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$response = new Response('Error: ' . $exception->getMessage(), 500);
$event->setResponse($response);
}
}
# config/services.yaml
services:
App\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: 10 }
Event Subscriber
Subscriber реализует интерфейс EventSubscriberInterface и объявляет все обрабатываемые события внутри себя через статический метод getSubscribedEvents(). Он автоматически регистрируется, если включён autoconfigure.
<?php
// src/EventSubscriber/AuthSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
class AuthSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [['onKernelRequest', 20]],
LoginSuccessEvent::class => 'onLoginSuccess',
];
}
public function onKernelRequest(RequestEvent $event): void
{
// логика на каждый запрос
}
public function onLoginSuccess(LoginSuccessEvent $event): void
{
$user = $event->getUser();
// логика после входа
}
}
Создание собственного события
<?php
// src/Event/OrderCreatedEvent.php
namespace App\Event;
use App\Entity\Order;
use Symfony\Contracts\EventDispatcher\Event;
class OrderCreatedEvent extends Event
{
public const NAME = 'order.created';
public function __construct(private readonly Order $order) {}
public function getOrder(): Order
{
return $this->order;
}
}
<?php
// В сервисе, где создаётся заказ
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class OrderService
{
public function __construct(
private readonly EventDispatcherInterface $dispatcher
) {}
public function create(array $data): Order
{
$order = new Order($data);
// ... сохранение
$this->dispatcher->dispatch(new OrderCreatedEvent($order));
return $order;
}
}
Ключевые отличия
- Listener: регистрируется в конфигурации; один класс — один или несколько методов; удобен для изолированных обработчиков.
- Subscriber: самодостаточен, несёт конфигурацию внутри себя; подходит для группировки связанных обработчиков; автоматически подхватывается через autoconfigure.
- Priority (приоритет) задаётся в обоих: в теге для listener или в массиве из
getSubscribedEvents(); выше число — раньше выполнение. - Метод
$event->stopPropagation()прерывает цепочку слушателей.
Подводные камни
- Если не указать
methodв теге kernel.event_listener, Symfony ищет метод по имени события — легко ошибиться при нестандартных именах. - При отключённом
autoconfigure: truesubscriber не регистрируется автоматически: нужно явно добавить тегkernel.event_subscriber. - Приоритеты между listener и subscriber не нормализованы в одном месте — трудно предсказать порядок выполнения без явного аудита конфигурации.
- Тяжёлая логика внутри listener на
kernel.requestзамедляет каждый запрос, включая статику и healthcheck-эндпоинты. - Событие
kernel.exceptionвызывается только при необработанных исключениях; ошибки внутри try/catch не попадут к слушателям. - Вызов
$event->setResponse()в listener наkernel.viewилиkernel.exceptionполностью заменяет ответ — случайно перезаписать ответ другого listener легко. - Circular dependency: listener, зависящий от сервиса, который сам тригерит то же событие, вызывает бесконечный цикл или исключение Symfony DI.
Common mistakes
- Сводить events listeners subscribers к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
- Не отделять validation, authorization, transaction boundary и business logic.
- Менять похожие API местами без учёта семантики ошибок и ownership.
What the interviewer is testing
- Объясняет events listeners subscribers через конкретную точку lifecycle в Symfony.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.