Понимание полного пути HTTP-запроса через Symfony — одна из ключевых тем сертификации. Нужно знать каждый шаг: от index.php до отправки Response, включая все события ядра и их порядок.
Обзор жизненного цикла
Browser → HTTP Request
→ Web Server (Nginx/Apache)
→ public/index.php (Front Controller)
→ Runtime::boot()
→ Kernel::handle(Request)
→ Event: kernel.request (routing, security, session)
→ Router matches route → determines controller
→ Event: kernel.controller
→ Event: kernel.controller_arguments
→ Controller executes → returns Response or data
→ Event: kernel.view (only if non-Response returned)
→ Event: kernel.response (modify response)
← Response::send() → sent to browser
→ Event: kernel.terminate (post-response tasks)
Шаг 1: Front Controller
Единая точка входа для ВСЕХ HTTP-запросов — public/index.php:
<?php
// public/index.php
use App\Kernel;
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
return function (array $context): Kernel {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
Symfony Runtime Component
Runtime Component обрабатывает bootstrap-логику:
<?php
declare(strict_types=1);
// What autoload_runtime.php does internally:
// 1. Load .env files
// 2. Set error/exception handlers
// 3. Call the closure from index.php to get Kernel
// 4. Create Request from globals
// 5. Call Kernel::handle($request)
// 6. Send Response
// 7. Call Kernel::terminate($request, $response)
Подвох экзамена:
autoload_runtime.php— это НЕ обычный autoloader. Он предоставляет Runtime, который автоматически создаётRequest, вызываетhandle(), отправляетResponseи вызываетterminate(). Front Controller просто возвращает closure, создающую Kernel.
Шаг 2: Kernel::handle()
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
// Kernel implements HttpKernelInterface
interface HttpKernelInterface
{
public const MAIN_REQUEST = 1;
public const SUB_REQUEST = 2;
public function handle(
Request $request,
int $type = self::MAIN_REQUEST,
bool $catch = true,
): Response;
}
Внутреннее устройство handleRaw()
<?php
declare(strict_types=1);
// Simplified HttpKernel::handleRaw() — the core processing pipeline
private function handleRaw(Request $request, int $type): Response
{
// 1. KERNEL.REQUEST — routing, firewall, session, locale
$event = new RequestEvent($this, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);
// If a listener set a Response — skip controller entirely
if ($event->hasResponse()) {
return $this->filterResponse($event->getResponse(), $request, $type);
}
// 2. RESOLVE CONTROLLER — from request attributes set by Router
$controller = $this->resolver->getController($request);
// Throws exception if no controller found
// 3. KERNEL.CONTROLLER — can replace controller
$event = new ControllerEvent($this, $controller, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
$controller = $event->getController();
// 4. RESOLVE ARGUMENTS — auto-inject parameters
$arguments = $this->argumentResolver->getArguments($request, $controller);
// 5. KERNEL.CONTROLLER_ARGUMENTS — can modify arguments
$event = new ControllerArgumentsEvent(
$this, $controller, $arguments, $request, $type
);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
// 6. EXECUTE CONTROLLER
$response = $controller(...$event->getArguments());
// 7. If controller returns non-Response — KERNEL.VIEW
if (!$response instanceof Response) {
$event = new ViewEvent($this, $request, $type, $response);
$this->dispatcher->dispatch($event, KernelEvents::VIEW);
if ($event->hasResponse()) {
$response = $event->getResponse();
} else {
// No listener created Response — error!
throw new \LogicException(sprintf(
'The controller must return a "Symfony\Component\HttpFoundation\Response" object'
. ' but it returned %s.',
get_debug_type($response)
));
}
}
// 8. KERNEL.RESPONSE — modify before sending
return $this->filterResponse($response, $request, $type);
}
Шаг 3: События ядра (Kernel Events)
kernel.request
Первое событие. Выполняется ДО определения контроллера (routing происходит здесь).
<?php
declare(strict_types=1);
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
// Standard listeners and their priorities:
// DebugHandlersListener: 2048 — error handler setup
// ValidateRequestListener: 256 — request validation
// SessionListener: 128 — session initialization
// RouterListener: 32 — routing (sets _controller attribute)
// LocaleListener: 16 — locale detection
// LocaleAwareListener: 15 — notify locale-aware services
// FirewallListener: 8 — security checks
#[AsEventListener(event: KernelEvents::REQUEST, priority: 20)]
final class ApiVersionListener
{
public function __invoke(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return; // Skip sub-requests
}
$request = $event->getRequest();
// Read API version from header
$version = $request->headers->get('X-API-Version', 'v1');
$request->attributes->set('_api_version', $version);
}
}
Подвох экзамена: Порядок критичен: RouterListener (priority 32) выполняется ДО FirewallListener (priority 8). Это значит, что маршрутизация происходит ДО проверки безопасности. Если маршрут не найден, firewall даже не вызывается — будет 404, а не 403.
kernel.controller
Контроллер определён, но ещё не вызван. Можно подменить контроллер или выполнить проверки.
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\Event\ControllerEvent;
#[AsEventListener(event: KernelEvents::CONTROLLER)]
final class ControllerTokenListener
{
public function __invoke(ControllerEvent $event): void
{
$controller = $event->getController();
// Controller is callable: [object, 'method'] or Closure
if (is_array($controller)) {
[$object, $method] = $controller;
// Check if controller has specific attribute/interface
}
// Can completely replace the controller:
// $event->setController(function () {
// return new JsonResponse(['error' => 'Forbidden'], 403);
// });
}
}
kernel.controller_arguments
Аргументы контроллера определены, можно их модифицировать.
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
#[AsEventListener(event: KernelEvents::CONTROLLER_ARGUMENTS)]
final class AuditArgumentsListener
{
public function __invoke(ControllerArgumentsEvent $event): void
{
// Get resolved arguments
$arguments = $event->getArguments();
// Get named arguments (parameter name => value)
$named = $event->getNamedArguments();
// Modify if needed
// $event->setArguments($modifiedArguments);
}
}
kernel.view
Вызывается ТОЛЬКО если контроллер вернул НЕ объект Response.
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ViewEvent;
#[AsEventListener(event: KernelEvents::VIEW)]
final class ArrayToJsonListener
{
public function __invoke(ViewEvent $event): void
{
$result = $event->getControllerResult();
if (is_array($result)) {
$event->setResponse(new JsonResponse($result));
}
}
}
<?php
declare(strict_types=1);
// Controller returning non-Response — triggers kernel.view
class ApiController extends AbstractController
{
#[Route('/api/data')]
public function getData(): array // Returns array, NOT Response
{
return ['status' => 'ok', 'data' => [1, 2, 3]];
}
}
Подвох экзамена: Если контроллер возвращает Response —
kernel.viewНЕ вызывается. Если возвращает не-Response и НИОДИН listener не установит Response вkernel.view— будет выброшен\LogicException. В Symfony 7+/8.0 атрибут#[Template()]обрабатывается черезkernel.view.
kernel.response
Вызывается ВСЕГДА (для любого запроса), после получения Response.
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\Event\ResponseEvent;
#[AsEventListener(event: KernelEvents::RESPONSE)]
final class CorsHeadersListener
{
public function __invoke(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$response = $event->getResponse();
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('X-Request-Id', bin2hex(random_bytes(8)));
}
}
kernel.terminate
Выполняется ПОСЛЕ отправки Response клиенту.
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\Event\TerminateEvent;
#[AsEventListener(event: KernelEvents::TERMINATE)]
final class SlowTaskListener
{
public function __invoke(TerminateEvent $event): void
{
// Client already received response
// Good for: analytics, email sending, cleanup
$this->sendAnalytics($event->getRequest(), $event->getResponse());
}
}
Подвох экзамена:
kernel.terminateработает ПОСЛЕ отправки ответа клиенту только сphp-fpm(черезfastcgi_finish_request()) или Swoole/RoadRunner. С встроенным PHP-сервером (php -S) кодterminateвыполняется ДО отправки ответа клиенту.
kernel.exception
Вызывается при любом исключении во время обработки запроса.
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
#[AsEventListener(event: KernelEvents::EXCEPTION, priority: -10)]
final 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));
}
}
Main Request vs Sub-Request
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\HttpKernelInterface;
// Main request — from the browser
$response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST); // type = 1
// Sub-request — internal (Twig render(), controller forward())
$subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); // type = 2
| Критерий | Main Request | Sub-Request |
|---|---|---|
| Источник | Браузер | Внутри приложения |
| Константа | MAIN_REQUEST (1) |
SUB_REQUEST (2) |
| Пример | GET /products |
{{ render(controller('...')) }} |
| Security | Полная проверка firewall | Наследует контекст |
| Profiler | Главный профиль | Вложенный профиль |
Подвох экзамена: В Symfony 6+ используется
isMainRequest()(методisMasterRequest()удалён в 7.0). Многие listeners должны обрабатывать ТОЛЬКО main request, чтобы не дублировать работу для sub-requests.
Полная диаграмма
Request from browser
│
▼
public/index.php
│
▼
Kernel::handle(Request)
│
├─── kernel.request ──► [Response set?] ──YES──► kernel.response ──► DONE
│ │
│ NO
│ │
│ ▼
├─── Resolve Controller
│ │
│ ▼
├─── kernel.controller
│ │
│ ▼
├─── kernel.controller_arguments
│ │
│ ▼
├─── Execute Controller
│ │
│ ┌────┴────┐
│ │ │
│ Response Other
│ │ │
│ │ kernel.view
│ │ │
│ └────┬────┘
│ │
│ ▼
├─── kernel.response
│ │
│ ▼
│ Response::send()
│ │
│ ▼
└─── kernel.terminate
Exception at any point:
kernel.exception → kernel.response → send → kernel.terminate
Обработка ошибок
<?php
declare(strict_types=1);
// HttpKernel::handle() wraps everything in try/catch
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; // Propagate in test environment
}
return $this->handleThrowable($e, $request, $type);
}
}
Параметр $catch:
true(default) — исключения перехватываются и обрабатываются черезkernel.exceptionfalse— исключения пробрасываются наверх (полезно для тестирования)
Symfony 8.0: Параметр
handle_all_throwables: trueвframework.yamlстал обязательным. Symfony теперь перехватывает все\Throwable(включая\Error), а не только\Exception. Это обеспечивает единообразную обработку ошибок.