Какие события ядра (Kernel Events) существуют в Symfony?

Ответ

Ядро Symfony (HttpKernel) генерирует ряд событий на протяжении жизненного цикла обработки HTTP-запроса. Эти события позволяют внедрять кастомную логику на разных этапах. Основные события определены в классе SymfonyComponentHttpKernelKernelEvents.

Полный список основных событий ядра:

  1. KernelEvents::REQUEST (kernel.request):

    • Когда: Сразу после получения Request от фронт-контроллера (index.php), но до определения роута и контроллера.
    • Зачем: Для глобальной инициализации, локализации, ранней проверки прав доступа (например, брандмауэр безопасности слушает это событие).
  2. KernelEvents::CONTROLLER (kernel.controller):

    • Когда: После определения контроллера и аргументов, но непосредственно перед его вызовом.
    • Зачем: Для модификации контроллера (например, подмена на другой) или его аргументов (например, десериализация тела запроса в объект).
  3. KernelEvents::CONTROLLER_ARGUMENTS (kernel.controller_arguments):

    • Когда: После разрешения всех аргументов контроллера, но перед вызовом.
    • Зачем: Для последней проверки или модификации массива аргументов.
  4. KernelEvents::VIEW (kernel.view):

    • Когда: Если контроллер вернул значение, которое не является объектом Response.
    • Зачем: Чтобы преобразовать результат контроллера (массив, объект) в Response. За это отвечает, например, SymfonyBundleFrameworkBundleControllerAbstractController.
  5. KernelEvents::RESPONSE (kernel.response):

    • Когда: После того как у ядра есть объект Response (полученный от контроллера или сформированный в VIEW), но перед его отправкой.
    • Зачем: Для модификации ответа — добавление заголовков, сжатие GZIP, инъекция контента.
  6. KernelEvents::FINISH_REQUEST (kernel.finish_request):

    • Когда: Сразу после RESPONSE, когда основной запрос завершен. Используется для очистки контекстозависимых данных (например, локали в RequestContext).
  7. KernelEvents::TERMINATE (kernel.terminate):

    • Когда: После отправки ответа клиенту (соединение закрыто).
    • Зачем: Для выполнения задач, которые не должны задерживать отправку ответа: тяжелое логирование, отправка email, обработка очередей.
  8. KernelEvents::EXCEPTION (kernel.exception):

    • Когда: При возникновении любого необработанного исключения в процессе работы ядра.
    • Зачем: Для кастомной обработки ошибок — преобразование исключения в пользовательскую страницу ошибки, логирование, отправку уведомлений.

Пример слушателя для логирования исключений:

# config/services.yaml
services:
    AppEventListenerExceptionLoggerListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception, priority: 100 }
// src/EventListener/ExceptionLoggerListener.php
use PsrLogLoggerInterface;
use SymfonyComponentHttpKernelEventExceptionEvent;

class ExceptionLoggerListener
{
    public function __construct(private LoggerInterface $logger) {}

    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        $this->logger->error('Unhandled exception', [
            'exception' => $exception,
            'url' => $event->getRequest()->getUri(),
        ]);
        // Можно НЕ останавливать распространение события,
        // чтобы стандартный обработчик ошибок Symfony тоже сработал.
    }
}

Ответ 18+ 🔞

А, ну это ж классика, ёпта! Сидишь такой, пишешь на Symfony, и тут бац — надо вклиниться в самую жопу процесса, пока запрос летит от пользователя до ответа. Ну, ядро-то не дурак, оно тебе целую пачку событий накидывает, как будто говорит: «На, мудила, хуярь свою логику куда хочешь, только не сломай». Главное — знать, где и что происходит, а то впендюришь слушатель не туда и будешь потом охуевать, почему ничего не работает.

Вот смотри, есть у них там класс KernelEvents. В нём все эти события, как будто контрольные точки в какой-нибудь долбанной игре. Разберём по полочкам, а то волнение ебать, как всё сложно.

  1. KernelEvents::REQUEST (kernel.request)
    Срабатывает самым первым, прямо после того как фронт-контроллер (index.php) отдал ядру объект запроса. Контроллер ещё не найден, роутер даже не нюхал этот запрос. Идеальное место, чтобы навесить какую-нибудь глобальную хуйню: проверить IP, подсунуть локаль пользователю, или, как это делает компонент Security — поставить брандмауэр и спросить «А ты кто такой, сука?».

  2. KernelEvents::CONTROLLER (kernel.controller)
    А вот тут уже роутер отработал, нашёл контроллер и аргументы к нему. Но сам вызов контроллера ещё не произошёл! Можно, блядь, подсунуть свинью — заменить контроллер на свой, или поковыряться в аргументах. Типа последний шанс всё испортить перед стартом.

  3. KernelEvents::CONTROLLER_ARGUMENTS (kernel.controller_arguments)
    Это уже почти то же самое, что и предыдущее, но чуть позже — когда все аргументы для контроллера уже полностью готовы и упакованы в массив. Можно сделать финальный чек или подмену, если совсем припёрло.

  4. KernelEvents::VIEW (kernel.view)
    О, это интересно! Срабатывает, если твой контроллер вернул какую-то хуйню, но не объект Response. Ну, там, массив данных или свой какой-то объект. Задача этого события — превратить эту хрень в нормальный HTTP-ответ. AbstractController из FrameworkBundle как раз этим и занимается — ловит результат и через какой-нибудь сериализатор делает из него JSON или что ты там хочешь.

  5. KernelEvents::RESPONSE (kernel.response)
    Ответ уже есть! Неважно, откуда он взялся — напрямую из контроллера или родился в событии VIEW. Но он ещё не улетел к пользователю. Самое время его пошаманить: добавить хидеры, сжать gzip'ом, впихнуть какую-нибудь аналитику в тело. Главное — не сломать, а то пользователь получит пиздатый кракозябр вместо страницы.

  6. KernelEvents::FINISH_REQUEST (kernel.finish_request)
    Всё, основной запрос обработан, ответ готов к отправке. Событие нужно в основном для уборки — почистить за собой контекстные данные, которые привязаны к этому конкретному запросу (например, сбросить локаль в каком-нибудь сервисе). Чтобы следующий запрос не начался с грязного наследства.

  7. KernelEvents::TERMINATE (kernel.terminate)
    А вот это, блядь, мощная фича! Срабатывает уже ПОСЛЕ того, как ответ полностью отправился клиенту и соединение закрыто. Понимаешь? Пользователь уже получил свою страницу и доволен, а ты в это время можешь делать какую-то долгую, тяжёлую хуйню: логировать в медленное хранилище, кидать задачи в очередь, рассылать письма. Это не задержит ответ ни на миллисекунду. Красота, да? Терпения ноль ебать ждать, когда это всё выполнится в основном потоке.

  8. KernelEvents::EXCEPTION (kernel.exception)
    Ну и куда же без этого, ёпта! Если в любом месте, на любом из предыдущих этапов, вылетело необработанное исключение — летишь сюда. Твоя последняя надежда всё починить, прежде чем пользователю прилетит белая страница с ошибкой 500. Можно залогировать ошибку, отправить алерт в телегу, или сгенерировать красивую кастомную страничку «Извините, всё пошло по пизде».

Вот тебе пример, как бы я сделал слушателя для логирования исключений:

Сначала настраиваю сервис в YAML. Всё стандартно, блядь.

# config/services.yaml
services:
    AppEventListenerExceptionLoggerListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception, priority: 100 }

А вот сам слушатель. Беру логгер, и если что-то пошло не так — пишу всё в него, вместе с URL, где это случилось. Главное — не остановить событие, пусть стандартный обработчик Symfony тоже отработает и покажет пользователю вменяемую ошибку, а не голый exception.

// src/EventListener/ExceptionLoggerListener.php
use PsrLogLoggerInterface;
use SymfonyComponentHttpKernelEventExceptionEvent;

class ExceptionLoggerListener
{
    public function __construct(private LoggerInterface $logger) {}

    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        $this->logger->error('Unhandled exception', [
            'exception' => $exception,
            'url' => $event->getRequest()->getUri(),
        ]);
        // Можно НЕ останавливать распространение события,
        // чтобы стандартный обработчик ошибок Symfony тоже сработал.
    }
}

Вот и вся магия, чувак. Разложил по полочкам, как нахуй надо. Главное — не путать, где TERMINATE, а где FINISH_REQUEST, а то будешь потом сидеть и думать, почему твои письма отправляются по пять минут.