SymfonyMiddleTechnical

Что такое 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: true subscriber не регистрируется автоматически: нужно явно добавить тег 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.

Sources

Related topics