<? phpukraine СПІВБЕСІДИ
Пошук по платформі

Symfony для Junior: питання на співбесіду

1 питання рівня Junior з теми Symfony з розгорнутими відповідями, порадами та перевіркою.

Тема
Рівень
1 питання
SF
Symfony·Junior ·HttpKernel ·EventDispatcher ·lifecycle

HttpKernel перетворює Request на Response, дорогою розсилаючи події: kernel.request (маршрутизація), kernel.controller і kernel.controller_arguments (резолвінг контролера й аргументів), kernel.view (якщо контролер повернув не Response), kernel.response, kernel.terminate після відправлення відповіді.

Що відбувається між public/index.php і моїм контролером?
У якому порядку йдуть kernel.request, kernel.controller, kernel.view, kernel.response, kernel.terminate?
Мій listener не бачить назви маршруту в атрибутах запиту — чому?
Як повернути відповідь ще до того, як викличеться контролер?

Уся Symfony зводиться до одного контракту: HttpKernelInterface::handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response. Фронт-контролер public/index.php (з Symfony 5.3 це кілька рядків, які повертають замикання, а решту робить компонент Runtime через vendor/autoload_runtime.php) створює Request::createFromGlobals(), віддає його ядру, відправляє отриманий Response і викликає terminate(). Клас Kernel із src/Kernel.php спершу завантажується — реєструє бандли й піднімає скомпільований контейнер, — а потім передає запит сервісу http_kernel. Саме HttpKernel виконує прохід від запиту до відповіді й у ключових точках розсилає події через EventDispatcher. Фреймворк тут нічого не «знає» наперед: маршрутизацію, безпеку, профайлер додають звичайні слухачі.

Порядок такий. kernel.request — найбагатша подія: тут RouterListener (пріоритет 32) зіставляє URL із маршрутом і кладе результат у $request->attributes (_route, _controller, параметри шляху), тут же працює firewall. Якщо будь-який слухач викличе $event->setResponse(), поширення події зупиняється й HttpKernel одразу переходить до kernel.response — контролер не викликається взагалі. Далі ControllerResolver перетворює _controller на callable, диспатчиться kernel.controller (контролер ще можна підмінити), ArgumentResolver збирає аргументи, диспатчиться kernel.controller_arguments — і лише тоді викликається ваш метод.

Що станеться з результатом, залежить від його типу. Повернули Response — Symfony одразу диспатчить kernel.response, де слухачі правлять заголовки, кладуть cookie, вставляють toolbar профайлера. Повернули щось інше — масив, DTO, сутність — диспатчиться kernel.view, і хтось має перетворити це на Response: атрибут #[Template] віддасть дані у Twig, у API-проєктах це робить слухач, що серіалізує обʼєкт у JSON. Якщо після kernel.view відповіді досі немає, ви отримаєте LogicException про те, що контролер має повертати Response. Виняток у будь-якому місці циклу (за $catch = true) веде до kernel.exception: слухачі можуть підставити свою відповідь, інакше ErrorListener рендерить сторінку помилки, а код береться з HttpExceptionInterface, якщо виняток його реалізує.

Останні два кроки часто плутають. kernel.finish_request закриває обробку конкретного запиту (важливо для під-запитів: він відновлює контекст батьківського запиту, наприклад локаль), а kernel.terminate спрацьовує вже після $response->send() — відповідь у користувача, а процес доробляє «хвіст». Тому змінювати відповідь там безглуздо, зате доречно відправити лист чи дописати статистику; щось справді довге має йти в чергу. І весь цей цикл повторюється для кожного під-запиту з render(controller(...)) та ESI, тому будь-який слухач, дія якого має бути одноразовою, починається з if (!$event->isMainRequest()) { return; }.

Практичний спосіб побачити все це самому: php bin/console debug:event-dispatcher kernel.request покаже реальний список слухачів із пріоритетами, а вкладка Events у веб-профайлері — які з них відпрацювали на конкретному запиті. Це швидше за читання схеми й одразу пояснює, чому ваш listener бачить або не бачить _route.

// src/EventSubscriber/MaintenanceSubscriber.php
final class MaintenanceSubscriber implements EventSubscriberInterface
{
    public function __construct(private MaintenanceMode $maintenance) {}

    public static function getSubscribedEvents(): array
    {
        return [
            // 33 > 32 (пріоритет RouterListener): маршрут ще не визначено, _route буде null
            KernelEvents::REQUEST => ['onRequest', 33],
            KernelEvents::RESPONSE => 'onResponse',
        ];
    }

    public function onRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return; // під-запити з render()/ESI не чіпаємо
        }

        if ($this->maintenance->isOn()) {
            // setResponse зупиняє поширення події: контролер не викличеться взагалі,
            // далі одразу піде kernel.response
            $event->setResponse(new Response('Технічні роботи', 503));
        }
    }

    public function onResponse(ResponseEvent $event): void
    {
        // сюди потрапляє і звичайна відповідь, і сторінка помилки з kernel.exception
        $event->getResponse()->headers->set('X-App', 'phpukraine');
    }
}

// src/Controller/ArticleController.php
#[Route('/articles/{slug}', name: 'article_show')]
public function show(Article $article): Response // обʼєкт підставив EntityValueResolver
{
    return $this->render('article/show.html.twig', ['article' => $article]);
}
Що вся робота фреймворку — це один контракт `HttpKernelInterface::handle(Request): Response`, а події лише вбудовуються в цей прохід.
Що маршрут визначає `RouterListener` на `kernel.request`, тому до цієї події атрибута `_route` в запиті ще немає.
Що `$event->setResponse()` на `kernel.request` зупиняє поширення події й контролер не викликається взагалі — так працюють firewall, maintenance mode, кеш.
Що `kernel.view` спрацьовує лише тоді, коли контролер повернув не `Response` (масив, обʼєкт), і саме там працює `#[Template]` чи серіалізація в API.
Що `kernel.terminate` виконується після відправлення відповіді користувачу і придатний для повільної роботи, а `kernel.exception` перетворює виняток на `Response`.
Що є головний запит і під-запити (`MAIN_REQUEST` / `SUB_REQUEST`), і listener зобовʼязаний перевіряти `$event->isMainRequest()`.
Казати, що контролер викликається одразу після роутингу: між ними ще `kernel.controller`, резолвінг аргументів і `kernel.controller_arguments`.
Читати `$request->attributes->get('_route')` у listener з високим пріоритетом на `kernel.request` і дивуватися `null`: `RouterListener` має пріоритет 32 і ще не відпрацював.
Очікувати `kernel.view`, коли контролер уже повернув `Response` — подія в цьому разі не диспатчиться зовсім.
Модифікувати відповідь у `kernel.terminate`: її вже відправлено, зміни нікуди не потраплять.
Забути `isMainRequest()` і виконати логіку авторизації чи логування вдруге на кожному `render()`-фрагменті Twig.
Плутати `Kernel` (бандли, конфіг, контейнер, бутстрап) з `HttpKernel` (сервіс, що реально ганяє події й викликає контролер).
ПОРАДА

Проговоріть ланцюжок уголос у порядку подій і назвіть для кожної реального слухача: `kernel.request` → `RouterListener` і `FirewallListener`, `kernel.controller_arguments` → резолвери аргументів, `kernel.view` → `#[Template]`, `kernel.exception` → `ErrorListener`, `kernel.terminate` → відправлення листів. Це одразу показує, що ви бачили стек, а не читали схему.

Сторінка питання →
Прогрес карток і тестів зберігається у профілі. Створити профіль·Увійти