HardТеория4 min

События ядра

kernel.request, kernel.controller, kernel.view, kernel.response, kernel.exception — полный разбор

Система событий ядра (Kernel Events) — одна из сложнейших тем экзамена Symfony. Нужно знать каждое событие, его порядок, что доступно в каждом event, и какие стандартные listeners подписаны.

Все события ядра

Событие Когда Event class Можно установить Response
kernel.request До определения контроллера RequestEvent Да (пропускает контроллер)
kernel.controller Контроллер определён, до выполнения ControllerEvent Нет
kernel.controller_arguments Аргументы контроллера определены ControllerArgumentsEvent Нет
kernel.view Контроллер вернул не-Response ViewEvent Да (обязательно)
kernel.response Response готов, до отправки ResponseEvent Да (заменить)
kernel.finish_request После обработки sub-request FinishRequestEvent Нет
kernel.terminate После отправки Response TerminateEvent Нет
kernel.exception При исключении ExceptionEvent Да

kernel.request

Первое событие — вызывается для КАЖДОГО запроса. Используется для routing, security, locale.

<?php

declare(strict_types=1);

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

#[AsEventListener(event: KernelEvents::REQUEST, priority: 100)]
class MaintenanceModeListener
{
    public function __invoke(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        // Setting response HERE skips controller entirely
        if ($this->isMaintenanceMode()) {
            $event->setResponse(new JsonResponse(
                ['error' => 'Service under maintenance'],
                Response::HTTP_SERVICE_UNAVAILABLE
            ));
        }
    }
}

Стандартные listeners на kernel.request

Listener Priority Назначение
DebugHandlersListener 2048 Настройка error handler
ValidateRequestListener 256 Валидация запроса
SessionListener 128 Инициализация сессии
LocaleListener 16 Установка locale
RouterListener 32 Маршрутизация (определение контроллера)
FirewallListener 8 Security/Firewall
LocaleAwareListener 15 Уведомление locale-aware сервисов

Подвох экзамена: RouterListener имеет priority 32, а FirewallListener — 8. Это значит routing происходит ДО security. Если маршрут не найден — FirewallListener даже не вызывается. Порядок: Router (32) → LocaleAware (15) → Firewall (8).

kernel.controller

Вызывается когда контроллер определён, но ещё не выполнен. Можно подменить контроллер.

<?php

declare(strict_types=1);

use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;

#[AsEventListener(event: KernelEvents::CONTROLLER)]
class ControllerAuditListener
{
    public function __invoke(ControllerEvent $event): void
    {
        $controller = $event->getController();

        // Controller can be: [object, 'method'] or callable
        if (is_array($controller)) {
            $className = get_class($controller[0]);
            $methodName = $controller[1];
            $this->logger->info("Calling {$className}::{$methodName}");
        }

        // Can replace the controller entirely
        // $event->setController($newController);
    }
}

#[IsGranted] обрабатывается здесь

<?php

declare(strict_types=1);

// SecurityListener subscribes to kernel.controller
// and checks #[IsGranted] attribute on the controller
#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController
{
    // If user is not ROLE_ADMIN, AccessDeniedException is thrown
    // BEFORE controller executes (in kernel.controller event)
}

kernel.controller_arguments

Вызывается после определения аргументов контроллера. Можно модифицировать аргументы.

<?php

declare(strict_types=1);

use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;

#[AsEventListener(event: KernelEvents::CONTROLLER_ARGUMENTS)]
class ArgumentLoggingListener
{
    public function __invoke(ControllerArgumentsEvent $event): void
    {
        $arguments = $event->getArguments();
        $namedArguments = $event->getNamedArguments();

        // Can modify arguments before controller executes
        // $event->setArguments($modifiedArguments);
    }
}

kernel.view

Вызывается ТОЛЬКО если контроллер вернул НЕ Response. Listener должен создать Response.

<?php

declare(strict_types=1);

use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpFoundation\JsonResponse;

#[AsEventListener(event: KernelEvents::VIEW)]
class AutoJsonListener
{
    public function __invoke(ViewEvent $event): void
    {
        $result = $event->getControllerResult();

        // Controller returned array — convert to JSON
        if (is_array($result)) {
            $event->setResponse(new JsonResponse($result));
        }
    }
}
<?php

declare(strict_types=1);

// Controller that returns non-Response (triggers kernel.view)
class ApiController
{
    #[Route('/api/products')]
    public function list(): array   // Returns array, not Response!
    {
        return ['products' => []];
    }
}

Подвох экзамена: Если контроллер возвращает Response — kernel.view НЕ вызывается. Если контроллер возвращает что-то другое (array, string, object) и НИКАКОЙ listener не установит Response в kernel.view — будет выброшено исключение. В Symfony 7+/8.0 #[Template()] атрибут обрабатывается через kernel.view.

kernel.response

Вызывается для КАЖДОГО запроса, после получения Response. Здесь можно модифицировать Response перед отправкой клиенту.

<?php

declare(strict_types=1);

use Symfony\Component\HttpKernel\Event\ResponseEvent;

#[AsEventListener(event: KernelEvents::RESPONSE)]
class SecurityHeadersListener
{
    public function __invoke(ResponseEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $response = $event->getResponse();

        // Add security headers
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains'
        );
    }
}

kernel.finish_request

Вызывается после обработки sub-request. Используется для восстановления состояния.

<?php

declare(strict_types=1);

// Rare — mainly used internally by Symfony
// Restores locale, firewall context after sub-request processing

kernel.terminate

Вызывается ПОСЛЕ отправки Response клиенту. Используется для тяжёлых операций.

<?php

declare(strict_types=1);

use Symfony\Component\HttpKernel\Event\TerminateEvent;

#[AsEventListener(event: KernelEvents::TERMINATE)]
class AnalyticsListener
{
    public function __invoke(TerminateEvent $event): void
    {
        // Response already sent to client
        // Client is NOT waiting for this code
        $request = $event->getRequest();
        $response = $event->getResponse();

        // Heavy operations: analytics, logging, cleanup
        $this->analytics->track(
            path: $request->getPathInfo(),
            statusCode: $response->getStatusCode(),
            duration: microtime(true) - $request->server->get('REQUEST_TIME_FLOAT'),
        );
    }
}

Подвох экзамена: kernel.terminate выполняется ПОСЛЕ отправки ответа клиенту. Клиент уже получил ответ и не ждёт. Но это работает ТОЛЬКО с php-fpm (fastcgi_finish_request) или Swoole. С php -S (built-in server) terminate выполняется ДО отправки ответа.

kernel.exception

Вызывается при любом исключении. Listener должен создать Response для ошибки.

<?php

declare(strict_types=1);

use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

#[AsEventListener(event: KernelEvents::EXCEPTION, priority: -10)]
class ApiExceptionListener
{
    public function __invoke(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();

        $statusCode = $exception instanceof HttpExceptionInterface
            ? $exception->getStatusCode()
            : 500;

        $event->setResponse(new JsonResponse([
            'error' => $exception->getMessage(),
            'code' => $statusCode,
        ], $statusCode));

        // Can also modify the exception
        // $event->setThrowable(new \RuntimeException('Wrapped'));

        // Allow other listeners to still process
        // $event->allowCustomResponseCode();
    }
}

Подвох экзамена: Symfony имеет встроенный ErrorListener (priority -128), который преобразует исключения в error pages. Если ваш listener имеет более высокий priority и установит Response — ErrorListener не вызовется. Если вы НЕ установите Response — обработка продолжится до ErrorListener.

Полный порядок событий

Нормальный запрос:
  kernel.request (32: Router, 8: Firewall)
  → kernel.controller
  → kernel.controller_arguments
  → [Controller executes]
  → kernel.response (если вернул Response)
  ИЛИ
  → kernel.view (если вернул не-Response) → kernel.response
  → [Response отправлен]
  → kernel.terminate

При исключении:
  kernel.exception
  → kernel.response (для Response из exception handler)
  → [Response отправлен]
  → kernel.terminate

Sub-request:
  ...те же события...
  → kernel.finish_request (восстановление контекста)

Symfony 8.0: В Symfony 8.0 порядок событий не изменился, но добавлены новые listeners с атрибутами. #[AsEventListener] стал единственным рекомендуемым способом регистрации listeners (вместо EventSubscriberInterface, который deprecated).