HardТеория6 min

Жизненный цикл запроса

Front Controller, Kernel, Router, Controller, Response — полный путь HTTP-запроса в Symfony

Понимание полного пути 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.exception
  • false — исключения пробрасываются наверх (полезно для тестирования)

Symfony 8.0: Параметр handle_all_throwables: true в framework.yaml стал обязательным. Symfony теперь перехватывает все \Throwable (включая \Error), а не только \Exception. Это обеспечивает единообразную обработку ошибок.


Проверь себя

Что обеспечивает корректную работу `kernel.terminate` ПОСЛЕ отправки ответа клиенту?

В каком порядке выполняются RouterListener и FirewallListener в kernel.request?

Что делает `autoload_runtime.php` в `public/index.php`?

Когда вызывается событие `kernel.view`?

Если listener на `kernel.request` вызывает `$event->setResponse()`, что произойдёт?