SymfonySeniorTechnical
Что такое Symfony HttpKernel и как он обрабатывает запрос?
HttpKernel обрабатывает запрос через цепочку событий: kernel.request (роутинг/auth), kernel.controller, kernel.controller_arguments, вызов контроллера, kernel.view, kernel.response и kernel.terminate после отправки ответа.
Symfony HttpKernel: жизненный цикл запроса
HttpKernel — это сердце Symfony. Он принимает объект Request и возвращает Response, используя цепочку событий (Event Dispatcher). Весь Symfony-фреймворк — это лишь набор EventListener'ов, зарегистрированных на события ядра.
Схема обработки запроса
Request
└── kernel.request # аутентификация, locale, роутинг
└── kernel.controller # resolve controller callable
└── kernel.controller_arguments # ParamConverter, ValueResolver
└── Controller::action() # бизнес-логика
└── kernel.view # если вернули не Response
└── kernel.response # модификация заголовков, Set-Cookie
└── kernel.finish_request
└── kernel.terminate # после flush (logging, async)
Ключевые события
- kernel.request — приходит самым первым. RouterListener слушает это событие и добавляет атрибуты роутинга (_controller, _route) в Request. FirewallListener проверяет аутентификацию.
- kernel.controller — ControllerResolver уже определил контроллер. Можно подменить его через $event->setController().
- kernel.controller_arguments — ArgumentValueResolver'ы разрешают аргументы метода: #[MapRequestPayload], #[CurrentUser], Entity по ID и т.д.
- kernel.view — если контроллер вернул не Response, а массив или DTO, ViewHandler (например, из FOSRestBundle) конвертирует его в Response.
- kernel.response — финальная обработка ответа: добавление Cache-Control, CORS-заголовков, установка cookie.
- kernel.terminate — выполняется после отправки ответа клиенту (fastcgi_finish_request). Используется для отложенного логирования и очередей.
- kernel.exception — перехватывает любое непойманное исключение и конвертирует в Response через ExceptionListener.
Исходный код HttpKernel::handle()
<?php
// Упрощённый вариант Symfony\Component\HttpKernel\HttpKernel::handle()
public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response
{
try {
return $this->handleRaw($request, $type);
} catch (\Throwable $e) {
if (!$catch) {
throw $e;
}
return $this->handleThrowable($e, $request, $type);
}
}
private function handleRaw(Request $request, int $requestType): Response
{
// 1. kernel.request
$event = new RequestEvent($this, $request, $requestType);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);
if ($event->hasResponse()) {
return $this->filterResponse($event->getResponse(), $request, $requestType);
}
// 2. Resolve controller
$controller = $this->resolver->getController($request);
// 3. kernel.controller
$event = new ControllerEvent($this, $controller, $request, $requestType);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
$controller = $event->getController();
// 4. kernel.controller_arguments
$arguments = $this->argumentResolver->getArguments($request, $controller);
$event = new ControllerArgumentsEvent($this, $controller, $arguments, $request, $requestType);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
// 5. Call controller
$response = $controller(...$event->getArguments());
// 6. kernel.view (если не Response)
if (!$response instanceof Response) {
$event = new ViewEvent($this, $request, $requestType, $response);
$this->dispatcher->dispatch($event, KernelEvents::VIEW);
$response = $event->getResponse();
}
// 7. kernel.response
return $this->filterResponse($response, $request, $requestType);
}
Sub-requests
HttpKernel поддерживает вложенные запросы (sub-request) — это основа для ESI и функции {{ render(controller('App\Controller\WidgetController::index')) }} в Twig. Каждый sub-request проходит весь цикл событий заново.
Создание собственного EventListener
<?php
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
#[AsEventListener(event: KernelEvents::REQUEST, priority: 20)]
class LocaleListener
{
public function __invoke(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return; // Пропустить sub-requests
}
$request = $event->getRequest();
$locale = $request->cookies->get('locale', 'en');
$request->setLocale($locale);
}
}
Подводные камни
- kernel.terminate работает только при использовании FastCGI или специального терминируемого runner'а — в обычном PHP-FPM он выполняется синхронно и задерживает ответ.
- Приоритеты листенеров: RouterListener имеет приоритет 32, FirewallListener — 8; если ваш листенер обращается к _route-атрибутам, его приоритет должен быть ниже 32.
- Проверка
$event->isMainRequest()обязательна в листенерах, которые не должны срабатывать для sub-requests (ESI, forward). - ExceptionListener вызывает handle() рекурсивно с
$catch = false— бесконечная рекурсия возможна, если обработчик исключений сам бросает исключение. - kernel.view диспатчится только если контроллер вернул не Response; если вернул null — будет TypeError, а не ViewEvent.
- #[AsEventListener] требует Symfony 6.0+; в более старых версиях нужна ручная регистрация через services.yaml с тегом kernel.event_listener.
Common mistakes
- Сводить httpkernel request lifecycle к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет httpkernel request lifecycle через конкретную точку lifecycle в Symfony.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.