Подвох 1: Приоритет маршрутов -- порядок имеет значение
В Symfony первый совпавший маршрут побеждает. Порядок определяется порядком загрузки файлов и приоритетом (priority).
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ProductController
{
// Этот маршрут перехватит ВСЕ URL вида /products/{что-угодно}
#[Route('/products/{slug}', name: 'product_show')]
public function show(string $slug): Response
{
return new Response("Product: $slug");
}
// Этот маршрут НИКОГДА не сработает!
// /products/new совпадает с /products/{slug} где slug = "new"
#[Route('/products/new', name: 'product_new')]
public function new(): Response
{
return new Response('New product form');
}
}
Решение -- requirements или порядок:
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ProductController
{
// Вариант 1: явный приоритет
#[Route('/products/new', name: 'product_new', priority: 1)]
public function new(): Response
{
return new Response('New product form');
}
// Вариант 2: ограничение через requirements
#[Route('/products/{slug}', name: 'product_show', requirements: ['slug' => '[a-z0-9\-]+'])]
public function show(string $slug): Response
{
return new Response("Product: $slug");
}
}
На экзамене: Порядок маршрутов критичен. Статические маршруты (
/products/new) должны быть определены ПЕРЕД динамическими (/products/{slug}), либо используйтеpriorityилиrequirements.
Подвох 2: Специальные параметры маршрутов
Symfony имеет зарезервированные параметры маршрутов, которые имеют особое поведение.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class PageController
{
// _locale -- автоматически устанавливает locale запроса
#[Route('/{_locale}/page/{slug}', name: 'page_show')]
public function show(string $slug): Response
{
// Request::getLocale() автоматически вернёт значение из URL!
return new Response("Page: $slug");
}
// _format -- автоматически устанавливает формат ответа
#[Route('/api/data.{_format}', name: 'api_data', defaults: ['_format' => 'json'])]
public function data(): Response
{
// Request::getRequestFormat() вернёт 'json' или 'xml'
return new Response('data');
}
}
Зарезервированные параметры:
| Параметр | Назначение |
|---|---|
_controller |
Определяет контроллер (используется внутренне) |
_locale |
Устанавливает locale запроса |
_format |
Устанавливает формат запроса (json, xml, html) |
_fragment |
Не передаётся на сервер (только для генерации URL) |
_stateless |
Отмечает маршрут как stateless (без сессии) |
На экзамене:
_fragmentНИКОГДА не доходит до сервера -- это часть URL после#, которая обрабатывается только браузером. Symfony использует его только при генерации URL.
Подвох 3: Requirements regex -- без разделителей!
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class UserController
{
// ПРАВИЛЬНО: regex БЕЗ разделителей
#[Route('/users/{id}', name: 'user_show', requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
return new Response("User: $id");
}
// НЕПРАВИЛЬНО: с разделителями -- вызовет ошибку!
// requirements: ['id' => '/\d+/'] -- ОШИБКА!
}
На экзамене: В
requirementsпишется чистый regex без разделителей (/). Часто в ответах подсовывают варианты с разделителями -- это ошибка.
Подвох 4: Sub-requests и kernel.terminate
Sub-requests -- это внутренние запросы, которые обрабатываются тем же kernel.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\Attribute\Route;
final class DashboardController
{
public function __construct(
private readonly HttpKernelInterface $httpKernel,
) {
}
#[Route('/dashboard', name: 'dashboard')]
public function index(Request $request): Response
{
// Sub-request -- обрабатывается ТЕМ ЖЕ kernel
$subRequest = Request::create('/api/stats');
$subResponse = $this->httpKernel->handle(
$subRequest,
HttpKernelInterface::SUB_REQUEST, // <-- Важно!
);
// kernel.terminate НЕ вызывается для sub-request!
// Только для MAIN request.
return new Response(
'Dashboard: ' . $subResponse->getContent(),
);
}
}
Жизненный цикл запросов:
MAIN Request:
kernel.request -> kernel.controller -> kernel.response -> Response -> kernel.terminate
SUB Request:
kernel.request -> kernel.controller -> kernel.response -> Response
(БЕЗ kernel.terminate!)
На экзамене:
kernel.terminateвызывается ТОЛЬКО для основного запроса (MAIN_REQUEST). Sub-requests не триггерят это событие. Это важно для задач, которые выполняются после отправки ответа клиенту.
Подвох 5: StreamedResponse vs BinaryFileResponse
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
final class DownloadController
{
// StreamedResponse -- для ГЕНЕРАЦИИ данных на лету
#[Route('/export/csv', name: 'export_csv')]
public function exportCsv(): StreamedResponse
{
return new StreamedResponse(function (): void {
$handle = fopen('php://output', 'w');
// Данные отправляются клиенту по мере генерации
// Не загружает весь файл в память!
foreach ($this->getRows() as $row) {
fputcsv($handle, $row);
flush(); // Отправить данные немедленно
}
fclose($handle);
}, Response::HTTP_OK, [
'Content-Type' => 'text/csv',
]);
}
// BinaryFileResponse -- для СУЩЕСТВУЮЩИХ файлов
#[Route('/download/{filename}', name: 'download_file')]
public function downloadFile(string $filename): BinaryFileResponse
{
$path = '/var/uploads/' . $filename;
// Поддерживает X-Sendfile, Range requests, кеширование
$response = new BinaryFileResponse($path);
$response->setContentDisposition('attachment', $filename);
return $response;
}
/** @return iterable<array<string>> */
private function getRows(): iterable
{
yield ['id', 'name'];
yield ['1', 'Product A'];
}
}
На экзамене:
StreamedResponse-- для генерации данных на лету (CSV, большие данные).BinaryFileResponse-- для отдачи существующих файлов (поддерживает X-Sendfile, Range). Не путайте их!
Подвох 6: kernel.request -- Early Response
Listener на kernel.request может прервать обработку, установив Response.
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
#[AsEventListener(event: RequestEvent::class, priority: 256)]
final readonly class MaintenanceListener
{
public function __invoke(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return; // Не реагировать на sub-requests
}
// Установив Response, мы ПОЛНОСТЬЮ пропускаем контроллер!
// kernel.controller, kernel.view -- НЕ будут вызваны
$event->setResponse(new JsonResponse(
['error' => 'Maintenance mode'],
Response::HTTP_SERVICE_UNAVAILABLE,
));
// НО kernel.response БУДЕТ вызван!
}
}
На экзамене: Если listener на
kernel.requestустанавливает Response черезsetResponse(), контроллер НЕ вызывается. Ноkernel.responseвсё равно вызывается для этого Response.
Подвох 7: Методы HTTP и _method override
<?php
declare(strict_types=1);
// В HTML-формах нельзя отправить PUT, PATCH, DELETE.
// Symfony поддерживает _method override:
// config/packages/framework.yaml:
// framework:
// http_method_override: true # Включить поддержку _method
// Тогда в форме:
// <input type="hidden" name="_method" value="DELETE">
// НО! В Symfony 8.0 http_method_override: false по умолчанию!
// Нужно включать явно.
На экзамене: В Symfony 8.0
http_method_overrideвыключен по умолчанию (false). Если в ответе подразумевается использование_methodв формах -- нужно помнить об этом изменении.