Система событий ядра (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).