Продвинутые возможности Routing — сложная тема экзамена. Condition expression language, subdomain routing, localized routes и route groups проверяются на hard-уровне.
Route Prefix на уровне класса
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// Class-level Route provides prefix for all methods
#[Route('/api/v1/products', name: 'api_v1_product_')]
class ProductApiController extends AbstractController
{
// Final path: /api/v1/products
// Route name: api_v1_product_list
#[Route('', name: 'list', methods: ['GET'])]
public function list(): Response { return $this->json([]); }
// Final path: /api/v1/products/{id}
// Route name: api_v1_product_show
#[Route('/{id<\d+>}', name: 'show', methods: ['GET'])]
public function show(int $id): Response { return $this->json(['id' => $id]); }
// Final path: /api/v1/products
// Route name: api_v1_product_create
#[Route('', name: 'create', methods: ['POST'])]
public function create(): Response { return $this->json([], 201); }
}
Route Groups (Symfony 6.2+)
<?php
declare(strict_types=1);
// Class-level Route with full configuration — acts as a "group"
#[Route(
path: '/api/v2',
name: 'api_v2_',
host: 'api.example.com',
schemes: ['https'],
defaults: ['_format' => 'json'],
stateless: true,
)]
class ApiV2Controller extends AbstractController
{
// Inherits: host, scheme, stateless, format, name prefix
#[Route('/users', name: 'users', methods: ['GET'])]
// Full: https://api.example.com/api/v2/users
// Name: api_v2_users, stateless: true, format: json
public function users(): Response
{
return $this->json([]);
}
#[Route('/orders', name: 'orders', methods: ['GET'])]
// Full: https://api.example.com/api/v2/orders
// Name: api_v2_orders
public function orders(): Response
{
return $this->json([]);
}
}
YAML Prefix
# config/routes.yaml
admin_controllers:
resource:
path: ../src/Controller/Admin/
namespace: App\Controller\Admin
type: attribute
prefix: /admin
name_prefix: admin_
host: admin.example.com
schemes: [https]
Подвох экзамена: Все параметры class-level
#[Route]наследуются method-level маршрутами.pathконкатенируется,nameконкатенируется,host/schemes/statelessнаследуются (если не переопределены на уровне метода).
Host Matching (subdomain routing)
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\Routing\Attribute\Route;
class SubdomainController extends AbstractController
{
// Match specific subdomain
#[Route('/dashboard', name: 'admin_dashboard', host: 'admin.example.com')]
public function adminDashboard(): Response
{
return $this->render('admin/dashboard.html.twig');
}
// Host with parameters
#[Route('/dashboard', name: 'tenant_dashboard',
host: '{tenant}.example.com',
requirements: ['tenant' => '[a-z]+']
)]
public function tenantDashboard(string $tenant): Response
{
// acme.example.com/dashboard → tenant='acme'
// beta.example.com/dashboard → tenant='beta'
return $this->render('tenant/dashboard.html.twig', ['tenant' => $tenant]);
}
// Host with default
#[Route('/', name: 'subdomain_home',
host: '{subdomain}.{domain}',
defaults: ['subdomain' => 'www', 'domain' => 'example.com'],
requirements: [
'subdomain' => 'www|blog|shop',
'domain' => 'example\.com|example\.org',
]
)]
public function home(string $subdomain, string $domain): Response
{
return $this->render('home.html.twig');
}
}
YAML host matching
# config/routes.yaml
api_products:
path: /products
controller: App\Controller\Api\ProductController::list
host: 'api.{domain}'
defaults:
domain: example.com
requirements:
domain: 'example\.(com|org)'
schemes: [https]
Подвох экзамена: Host parameters доступны в контроллере как обычные аргументы. При генерации URL с host-параметрами нужно передавать их:
$this->generateUrl('tenant_dashboard', ['tenant' => 'acme']). Без host-параметра генерация URL выбросит исключение.
Condition Expression Language
Conditions используют Symfony ExpressionLanguage для сложного matching, выходящего за рамки URL-паттерна.
<?php
declare(strict_types=1);
use Symfony\Component\Routing\Attribute\Route;
class ConditionalController extends AbstractController
{
// Match only if specific header is present
#[Route('/api/internal',
name: 'api_internal',
condition: "request.headers.has('X-Internal-Token')"
)]
public function internal(): Response
{
return $this->json(['source' => 'internal']);
}
// Match based on Accept header
#[Route('/data',
name: 'data_json',
condition: "request.headers.get('Accept') matches '/json/'"
)]
public function dataJson(): Response
{
return $this->json([]);
}
// Match AJAX requests only
#[Route('/api/quick',
name: 'api_quick',
condition: "request.isXmlHttpRequest()"
)]
public function quickApi(): Response
{
return $this->json([]);
}
// Match based on query parameter
#[Route('/search',
name: 'search_advanced',
condition: "request.query.has('advanced') and request.query.get('advanced') == '1'"
)]
public function advancedSearch(): Response
{
return $this->json([]);
}
// Combined conditions
#[Route('/secure',
name: 'secure_endpoint',
condition: "request.isSecure() and request.getClientIp() starts with '10.'"
)]
public function secureInternal(): Response
{
return $this->json([]);
}
}
Доступные переменные в condition
| Переменная | Тип | Описание |
|---|---|---|
context |
RequestContext |
Контекст маршрутизации |
request |
Request |
Текущий HTTP-запрос (если есть) |
params |
array | Параметры маршрута |
<?php
declare(strict_types=1);
// Useful methods on request:
// request.getClientIp()
// request.getMethod()
// request.getHost()
// request.getScheme()
// request.getPathInfo()
// request.isSecure()
// request.isXmlHttpRequest()
// request.headers.get('name')
// request.headers.has('name')
// request.query.get('name')
// request.query.has('name')
// request.cookies.get('name')
// request.getPreferredLanguage(['en', 'ru'])
// Useful methods on context:
// context.getMethod()
// context.getHost()
// context.getScheme()
// context.getPathInfo()
Подвох экзамена: Conditions выполняются ПОСЛЕ базового URL matching, но ДО вызова контроллера. Если condition false — маршрут считается несовпавшим, Router пробует следующий. Conditions НЕ могут использовать сервисы из контейнера — только
context,requestиparams.
Localized Routes
Массив путей по локалям
<?php
declare(strict_types=1);
class PageController extends AbstractController
{
// Different paths per locale
#[Route(
path: [
'en' => '/about-us',
'ru' => '/o-nas',
'de' => '/uber-uns',
],
name: 'about'
)]
public function about(): Response
{
// Current locale determines which path matches
return $this->render('page/about.html.twig');
}
#[Route(
path: [
'en' => '/contact',
'ru' => '/kontakty',
'de' => '/kontakt',
],
name: 'contact'
)]
public function contact(): Response
{
return $this->render('page/contact.html.twig');
}
}
Конфигурация локалей
# config/packages/framework.yaml
framework:
default_locale: en
enabled_locales: ['en', 'ru', 'de']
translator:
default_path: '%kernel.project_dir%/translations'
Как генерируются маршруты
# Localized routes generate SEPARATE routes per locale:
# about.en → /about-us
# about.ru → /o-nas
# about.de → /uber-uns
# URL generation uses current locale:
# path('about') → /o-nas (if current locale is 'ru')
# path('about', {_locale: 'en'}) → /about-us (force English)
Подвох экзамена: Localized routes с массивом
pathгенерируют ОТДЕЛЬНЫЕ маршруты:about.en,about.ru,about.de. При генерации URL Symfony использует текущую локаль.path('about')вернёт/o-nasесли локальru. Для явной локали:path('about', {_locale: 'en'}).
Locale prefix стратегия
<?php
declare(strict_types=1);
// Strategy: locale in URL prefix
#[Route('/{_locale}', requirements: ['_locale' => 'en|ru|de'])]
class LocalizedController extends AbstractController
{
// /en/products, /ru/products, /de/products
#[Route('/products', name: 'product_list')]
public function list(): Response
{
// _locale automatically sets $request->setLocale()
return $this->render('product/list.html.twig');
}
// /en/products/42, /ru/products/42
#[Route('/products/{id<\d+>}', name: 'product_show')]
public function show(int $id): Response
{
return $this->render('product/show.html.twig');
}
}
Priority и порядок маршрутов
Числовой приоритет
<?php
declare(strict_types=1);
class CatchAllController extends AbstractController
{
// Priority 10 — checked first
#[Route('/products/featured', name: 'product_featured', priority: 10)]
public function featured(): Response
{
return $this->render('product/featured.html.twig');
}
// Priority 5 — checked second
#[Route('/products/new', name: 'product_new', priority: 5)]
public function new(): Response
{
return $this->render('product/new.html.twig');
}
// Priority 0 (default) — checked last
#[Route('/products/{slug}', name: 'product_show')]
public function show(string $slug): Response
{
// /products/featured → product_featured (priority 10)
// /products/new → product_new (priority 5)
// /products/widget → product_show (priority 0)
return $this->render('product/show.html.twig');
}
}
Порядок объявления
<?php
declare(strict_types=1);
// WITHOUT priority: order of declaration matters
class OrderMattersController extends AbstractController
{
// Declared FIRST — matched first
#[Route('/products/new', name: 'product_new')]
public function new(): Response { /* ... */ }
// Declared SECOND — matched if /new doesn't match
#[Route('/products/{slug}', name: 'product_show')]
public function show(string $slug): Response { /* ... */ }
}
Подвох экзамена: Более высокий priority = раньше проверяется. При одинаковом priority — порядок объявления в файле. Для YAML — порядок в конфиг-файле. Для attributes — порядок методов в классе и порядок загрузки файлов.
Schemes (HTTP/HTTPS)
<?php
declare(strict_types=1);
// HTTPS only — HTTP requests get 301 redirect to HTTPS
#[Route('/secure/payment', schemes: ['https'])]
public function payment(): Response
{
return $this->render('payment.html.twig');
}
// HTTP only (rare, but possible)
#[Route('/unsecure', schemes: ['http'])]
public function unsecure(): Response
{
return new Response('OK');
}
Подвох экзамена: Если маршрут требует
httpsи запрос приходит поhttp, Symfony вернёт 301 redirect на https-версию. Это НЕ 404 и НЕ 403. Аналогично и в обратную сторону.
Multiple Routes на одном методе
<?php
declare(strict_types=1);
class LegacyController extends AbstractController
{
// Multiple routes pointing to same action
#[Route('/products', name: 'product_list')]
#[Route('/catalog', name: 'catalog_list')]
#[Route('/shop', name: 'shop_list')]
public function list(): Response
{
return $this->render('product/list.html.twig');
}
// Different methods, same action
#[Route('/items/{id}', name: 'item_update_put', methods: ['PUT'])]
#[Route('/items/{id}', name: 'item_update_patch', methods: ['PATCH'])]
public function update(int $id, Request $request): Response
{
return $this->json(['id' => $id, 'method' => $request->getMethod()]);
}
}
UTF-8 Routes
<?php
declare(strict_types=1);
// UTF-8 route parameters
#[Route('/kategorie/{name}', requirements: ['name' => '.+'], utf8: true)]
public function category(string $name): Response
{
// /kategorie/электроника → name='электроника'
return $this->json(['name' => $name]);
}
// UTF-8 in path
#[Route('/новости/{slug}', utf8: true)]
public function news(string $slug): Response
{
return $this->render('news/show.html.twig');
}
Дебаг маршрутов
# List all routes
php bin/console debug:router
# Show specific route details
php bin/console debug:router product_show
# Match a URL
php bin/console router:match /products/42
# Match with method
php bin/console router:match /products --method=POST
# Match with host
php bin/console router:match /products --host=api.example.com
# Output as JSON
php bin/console debug:router --format=json
Частые проблемы маршрутизации
| Проблема | Симптом | Решение |
|---|---|---|
| Порядок маршрутов | Другой маршрут совпал раньше | Используйте priority |
| Requirements | 404 при валидном URL | Проверьте regex |
| Methods | 405 Method Not Allowed | Добавьте метод в methods |
| Host | 404 на правильном URL | Проверьте host matching |
| Condition | Route не совпадает | router:match для дебага |
| Trailing slash | 301 redirect loop | Уберите или добавьте / |
| Cache | Старые маршруты | cache:clear |
Подвох экзамена:
router:match— ключевая команда для дебага. Она показывает какой маршрут совпал и почему остальные не подошли. На экзамене спрашивают "какой командой проверить matching URL" — ответ:php bin/console router:match /url.