HttpKernel перетворює Request на Response, дорогою розсилаючи події: kernel.request (маршрутизація), kernel.controller і kernel.controller_arguments (резолвінг контролера й аргументів), kernel.view (якщо контролер повернув не Response), kernel.response, kernel.terminate після відправлення відповіді.
Питання на співбесіду Symfony
Питання для Symfony-розробників: dependency injection і компіляція контейнера, Doctrine ORM, Messenger, події ядра, конфігурація та DDD-підхід.
Doctrine це Data Mapper: сутність - звичайний PHP-клас без save() і find(), а стан у базу переносить EntityManager (persist реєструє новий об'єкт, flush виконує SQL), тоді як репозиторій відповідає лише за запити. В Active Record модель сама знає про з'єднання, тому домен і таблиця злиті в один клас.
Команда - це сервіс з атрибутом #[AsCommand]: у Symfony 7.3+ достатньо класу з методом __invoke() і параметрами під #[Argument] та #[Option], вивід іде через SymfonyStyle, тест пишеться на CommandTester, а від паралельних запусків з cron захищає Lock.
Autowiring підставляє залежності за типом параметра конструктора, autoconfigure навішує теги за реалізованим інтерфейсом чи атрибутом; обидва мовчать там, де тип нічого не каже: скаляри й env, класи з vendor, дві реалізації одного інтерфейсу та теги з атрибутами вимагають явного запису в services.yaml або атрибута #[Autowire].
Messenger розділяє три речі. Повідомлення тримає лише дані, логіка живе в обробнику з #[AsMessageHandler] і методом __invoke(), а транспорт описується DSN у конфігу. Де саме обробник виконається, вирішує секція routing: без маршруту він спрацює синхронно в тому ж запиті, з маршрутом на doctrine/amqp/redis його підхопить воркер messenger:consume, а в тестах той самий код ганяють через sync:// або in-memory://.
Будь-яка перевірка проходить через `isGranted($attribute, $subject)`: AccessDecisionManager опитує всі voter'и й зводить їхні голоси стратегією (за замовчуванням affirmative). Ролі перевіряє RoleHierarchyVoter, який розгортає `role_hierarchy`, а права на конкретний обʼєкт — власний Voter, підключений атрибутом `#[IsGranted('EDIT', subject: 'post')]`.
Контейнер компілюється в PHP-клас на етапі cache warmup: autowiring читає типи в конструкторах, compiler passes змінюють визначення сервісів, а результат — статичний код без рефлексії в рантаймі.
Twig за замовчуванням екранує кожен вивід `{{ }}`, але стратегію обирає за розширенням файлу шаблону (`.html.twig` - html, `.js.twig` - js, `.txt.twig` - без екранування), а не за місцем виразу в розмітці; тому в атрибутах, URL-параметрах і в `<script>` потрібні явні `|e('html_attr')`, `|e('url')`, `|e('js')`, а `|raw` лишають тільки для HTML, який згенерував сам застосунок або пропустив через санітайзер.
Слухач і підписник - це один і той самий механізм із різним місцем конфігурації: підписник оголошує події й пріоритети всередині класу, слухач отримує їх ззовні (атрибут, YAML), і це зручніше для чужих подій та для налаштувань, що змінюються без правки коду. У `kernel.exception` головне - пріоритет (нижче нуля, щоб не з'їсти логування), `setThrowable()` для перетворення доменного винятку на HTTP-виняток і памʼять про те, що `setResponse()` зупиняє поширення події.
Doctrine відслідковує всі керовані обʼєкти й накопичує зміни в памʼяті; на flush() вона обчислює change set і виконує запити в правильному порядку.
Форма — це двонаправлений маппер HTTP-даних на обʼєкт: submit прогонить дані через трансформери, покладе їх у модель і лише потім викличе Validator, який валідує обʼєкт, а не поля; в JSON-API цей шар зайвий, бо `handleRequest` читає `$request->request`, а не тіло запиту, і краще брати `#[MapRequestPayload]`.
Lazy-проксі та PersistentCollection довантажують дані окремим запитом на кожну сутність; лікується fetch join (`addSelect` приєднаної асоціації), а для пагінації — Doctrine Paginator, який ріже сутності, а не рядки.
API Platform виправданий, коли потрібен машиночитний контракт (OpenAPI/JSON-LD), фільтри, пагінація й HTTP-кеш «з коробки»; головна архітектурна зміна — контролер зникає, а його місце займають state provider і processor, тому ресурс має бути окремим DTO, а не Doctrine-сутністю.
diff дає єдине джерело істини для схеми, але його результат треба перечитувати й перегенеровувати після rebase; розходження ловиться в CI прогоном міграцій на порожній базі плюс doctrine:schema:validate, а перенесення даних живе в окремих міграціях на сирому SQL, не в тих, що змінюють схему.
Транспорт налаштовується з retry_strategy: кількість спроб, початкова затримка й множник; вичерпані повідомлення йдуть у failed transport, а логічні помилки відкидаються одразу через UnrecoverableMessageHandlingException.
Symfony-співбесіди технічніші за Laravel: більше про контейнер, Doctrine та межі шарів. Готуйтесь пояснювати Unit of Work, а не тільки писати запити.