Настройка маршрутов в Symfony от атрибутов до кастомных лоадеров
Маршруты (routes) в Symfony определяют соответствие между URL и контроллером. Начиная с Symfony 5.3, основным способом является использование PHP-атрибутов (attributes) прямо в классах контроллеров. Этот подход считается наиболее эффективным благодаря лаконичности и интеграции с IDE. Для проектов, где требуется централизованное управление или поддержка старых версий, доступны альтернативы: YAML, PHP-массивы, аннотации (устарели).
Основные способы определения маршрутов
Какой способ самый современный и рекомендуемый для Symfony 6+?
PHP-атрибуты (Attributes)
// src/Controller/ProductController.php
use Symfony\Component\Routing\Annotation\Route;
class ProductController extends AbstractController
{
#[Route('/products', name: 'product_list')]
public function list(): Response
{
// ...
}
#[Route('/products/{id}', name: 'product_show', requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
// ...
}
}
Php symfony routes (маршруты symfony)
Атрибут #[Route] принимает путь и опции. Параметры в фигурных скобках (например {id}) автоматически передаются в метод контроллера. Обязательно наличие PHP 8.0+.
Типичная ошибка: роут не найден из-за отсутствия импорта use Symfony\Component\Routing\Annotation\Route;. Решение: добавить use-выражение. Также возможно конфликт с аннотациями Doctrine: следует использовать именно Routing\Annotation\Route.
Как определить маршруты в отдельном файле YAML?
# config/routes/product.yaml
product_list:
path: /products
controller: App\Controller\ProductController::list
methods: GET
product_show:
path: /products/{id}
controller: App\Controller\ProductController::show
requirements:
id: '\d+'
В основном файле config/routes.yaml подключаются другие ресурсы. YAML удобен для централизованного управления, но не позволяет IDE подсказывать параметры контроллера.
Ошибка: название маршрута (например product_list) должно быть уникальным. При дублировании Symfony выдаст исключение RouteNotFoundException.
Как использовать PHP-массивы для описания маршрутов?
// config/routes.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
return function (RoutingConfigurator $routes) {
$routes->add('product_list', '/products')
->controller([ProductController::class, 'list'])
->methods(['GET']);
$routes->add('product_show', '/products/{id}')
->controller([ProductController::class, 'show'])
->requirements(['id' => '\d+']);
};
PHP-формат даёт максимум гибкости: можно динамически создавать маршруты, использовать условия и циклы.
Проблема: при ошибке синтаксиса (например лишняя запятая) файл не выполнится, и роутинг перестанет работать. Рекомендуется использовать статический анализ.
Есть ли способ с аннотациями (устаревший)?
/**
* @Route("/products", name="product_list")
*/
public function list() {}
Аннотации (DocBlock) работали до Symfony 6.0, но теперь заменены атрибутами. Их использование не рекомендуется в новых проектах.
Ошибка: при обновлении Symfony с версии 5.4 на 6.0 аннотации перестают обрабатываться, если не установлен пакет doctrine/annotations. Лучше сразу перейти на атрибуты.
Параметры, требования и значения по умолчанию
Для маршрута можно задать параметры с типами, требованиями (regex) и значениями по умолчанию. Атрибут позволяет всё в одном месте:
#[Route('/blog/{page}', name: 'blog_index', defaults: ['page' => 1], requirements: ['page' => '\d+'])]
public function index(int $page): Response { ... }
Параметр {page} опционален: если не указан в URL, используется значение 1. Требование \d+ гарантирует, что только цифры.
Ошибка: если параметр объявлен без типа int, он приходит строкой. Это может сломать логику. Используйте type hinting и конвертацию.
Именованные маршруты и генерация URL
Каждый маршрут имеет имя (параметр name). Имена используются для создания ссылок в шаблонах Twig или контроллерах.
// Twig: <a href="{{ path('product_show', {id: product.id}) }}">
// PHP: $this->generateUrl('product_show', ['id' => $product->getId()])
Сгенерированный URL учитывает префиксы и хосты.
Проблема: если имя маршрута изменено, а ссылки остались старыми, страница упадёт с 404. Используйте константы или генерацию через сервис router для тестирования.
Группировка маршрутов и префикс
Для модулей удобно группировать роуты с общим префиксом. В атрибутах можно использовать #[Route(path: '/admin', name: 'admin_')] на уровне класса:
#[Route('/admin', name: 'admin_')]
class AdminController extends AbstractController
{
#[Route('/dashboard', name: 'dashboard')]
public function dashboard(): Response {}
// итоговый путь: /admin/dashboard, имя: admin_dashboard
}
В YAML и PHP группа задаётся через префикс в родительском файле.
Типичная ошибка: дублирование префикса вложенных маршрутов (например /admin/admin/dashboard). Проверьте, чтобы пути методов не начинались с /, иначе префикс игнорируется.
Расширенные возможности: хост, методы, схема, условия
Маршрут может быть ограничен хостом (например api.example.com), HTTP-методом, схемой (https) или произвольным условием (условное выражение Symfony ExpressionLanguage).
#[Route('/api/users', name: 'api_users', host: 'api.example.com', methods: ['GET'], schemes: ['https'])]
public function listUsers(): Response { ... }
Ошибка: если условие condition синтаксически неверно, роутинг сломается. Используйте context() для проверки.
Локализация и перевод маршрутов
Для мультиязычных сайтов используют параметр {_locale}. Он автоматически подставляет код языка и определяет локаль приложения.
#[Route('/{_locale}/blog/{slug}', name: 'blog_show', requirements: ['_locale' => 'en|fr|de'])]
public function show(string $slug): Response {}
Если нужно переводить URL (например /fr/actualites), применяется пакет jms/translation-bundle или явное описание нескольких маршрутов.
Ошибка: забыть добавить требование для _locale - тогда любая строка будет восприниматься как локаль, и может возникнуть конфликт с другими параметрами.
Продвинутые примеры настройки маршрутов Symfony
1. Условная маршрутизация с ExpressionLanguage
// src/Controller/ConditionalController.php
use Symfony\Component\Routing\Annotation\Route;
class ConditionalController extends AbstractController
{
#[Route('/special-offer', name: 'special_offer', condition: "request.headers.get('X-Test') == 'true'")]
public function special(): Response
{
return new Response('Condition met');
}
}
Маршрут доступен только если в запросе присутствует заголовок X-Test: true. Условие может использовать объекты request, params, context. Ошибка: неправильный синтаксис выражения приводит к исключению.
// curl -H "X-Test: true" https://example.com/special-offer // > Condition met // curl https://example.com/special-offer // > 404 Not Found
2. Использование нескольких методов и схем
#[Route('/contact', name: 'contact', methods: ['GET', 'POST'], schemes: ['https'])]
public function contact(): Response
{
// ...
}
Метод может обрабатывать и GET (отображение формы) и POST (отправка). Схема https обязательна. Если не указать schemes, маршрут будет работать для любого протокола.
// http://example.com/contact -> 404, т.к. не https // https://example.com/contact -> OK
3. Редирект с помощью маршрута
// config/routes.yaml
old_products:
path: /old-products
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
defaults:
route: product_list
permanent: true
Вместо написания контроллера используется встроенный RedirectController. Редирект 301 (permanent) на маршрут product_list. Можно указать permanent: false для 302.
// GET /old-products -> 301 Location: /products
4. Сложные требования с регулярными выражениями
#[Route('/article/{slug}', name: 'article_show', requirements: ['slug' => '[a-z0-9\-]+'])]
public function show(string $slug): Response {}
Допускаются только строчные буквы, цифры и дефис. Ошибка: если slug содержит заглавные или спецсимволы, маршрут не срабатывает - возвращается 404.
// /article/my-article -> OK // /article/My_Article! -> 404
5. Группировка и префикс с общим контроллером
// src/Controller/Api/V1/UserController.php
#[Route('/api/v1/users', name: 'api_users_')]
class UserController extends AbstractController
{
#[Route('', name: 'list', methods: ['GET'])]
public function list(): Response {}
#[Route('/{id}', name: 'show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id): Response {}
}
Все маршруты класса начинаются с /api/v1/users. Имена: api_users_list, api_users_show. Параметр name на классе задаёт префикс имени.
// GET /api/v1/users -> list() // GET /api/v1/users/42 -> show(42)
6. Динамическая загрузка маршрутов через кастомный лоадер
// src/Routing/ExtraRouteLoader.php
class ExtraRouteLoader implements LoaderInterface
{
public function load($resource, $type = null)
{
$routes = new RouteCollection();
$route = new Route('/extra', ['_controller' => 'App\Controller\ExtraController::index']);
$routes->add('extra_route', $route);
return $routes;
}
public function supports($resource, $type = null)
{
return $type === 'extra';
}
}
Регистрируется как сервис с тегом routing.loader. В config/routes.yaml подключается: extra:
resource: .
type: extra. Позволяет генерировать маршруты программно, например, из базы данных.
// GET /extra -> ExtraController::index()
7. Маршруты с локализацией и переводом пути
// config/routes/translated.yaml
product_list_en:
path: /{_locale}/products
controller: App\Controller\ProductController::list
requirements:
_locale: en|fr
product_list_fr:
path: /{_locale}/produits
controller: App\Controller\ProductController::list
requirements:
_locale: fr
Для разных языков разные пути, ведущие к одному и тому же контроллеру. Лучше использовать пакет lunetics/locale-bundle или JMSTranslationBundle для автоматической генерации.
// /en/products -> controller // /fr/produits -> controller (тот же)