← Tous les cours / Symfony Routing avancé Cours
00 — #[ROUTE] — L'ATTRIBUT

#[Route] — l'attribut PHP

#[Route] est IS_REPEATABLE sur classe et méthode — ses paramètres couvrent path, methods, requirements, defaults, host, condition, priority.

L'attribut #[Route] est le point d'entrée universel du composant Routing. Il porte le flag IS_REPEATABLE — tu peux en empiler plusieurs sur la même méthode pour déclarer plusieurs chemins — et il fonctionne aussi bien sur une classe (préfixe) que sur une méthode (route finale).

Signature complète de #[Route]
Tous les paramètres sont optionnels sauf path (ou son alias positionnel).
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class Route
{
    public function __construct(
        public string|array|null $path        = null,   // chemin ou tableau de chemins
        public ?string           $name        = null,   // nom de la route
        public array             $requirements = [],    // regex par paramètre
        public array             $options      = [],    // options bas niveau
        public array             $defaults     = [],    // valeurs par défaut
        public ?string           $host        = null,   // contrainte de domaine
        array|string             $methods      = [],    // GET, POST…
        array|string             $schemes      = [],    // http, https
        public ?string           $condition   = null,   // expression ExpressionLanguage
        public ?int              $priority    = null,   // ordre de matching
        ?string                  $locale      = null,   // → defaults['_locale']
        ?string                  $format      = null,   // → defaults['_format']
        ?bool                    $utf8        = null,   // → options['utf8']
        ?bool                    $stateless   = null,   // → defaults['_stateless']
        string|array|null        $env         = null,   // filtre par environnement
    ) {}
}

Sucre syntaxique — locale, format, stateless

Ces trois paramètres sont des raccourcis : à la compilation, Symfony les déplace automatiquement dans defaults ou options.

ParamètreTraduit enExemple
locale: 'fr'defaults['_locale'] = 'fr'Force la locale sur cette route
format: 'json'defaults['_format'] = 'json'Format de réponse par défaut
utf8: trueoptions['utf8'] = trueActive le mode UTF-8 sur le pattern regex
stateless: truedefaults['_stateless'] = trueDésactive la session sur cette route

Prefix de classe — préfixer chemin et nom

Placer #[Route] sur la classe préfixe automatiquement toutes les routes des méthodes :

#[Route('/api', name: 'api_')]
class UserController
{
    #[Route('/users', name: 'users_list', methods: 'GET')]
    public function list(): Response { /* ... */ }
    // → route finale : nom = 'api_users_list', chemin = '/api/users'

    #[Route('/users/{id}', name: 'users_show', methods: 'GET')]
    public function show(int $id): Response { /* ... */ }
    // → route finale : nom = 'api_users_show', chemin = '/api/users/{id}'
}

Route multipaths — IS_REPEATABLE

La même action peut écouter sur plusieurs chemins grâce au flag IS_REPEATABLE :

#[Route('/home', name: 'homepage')]
#[Route('/', name: 'root')]
public function index(): Response
{
    // Répond à /home et à /
}
💡 Filtre env : restreindre une route à certains environnements évite de l'exposer en production. #[Route('/debug/profiler', env: 'dev')] — la route n'existe tout simplement pas en prod.
🔑 L'ordre des attributs compte : quand plusieurs routes matchent, Symfony utilise l'ordre de déclaration et le paramètre priority (valeur plus haute = priorité plus haute). Une route sans priority vaut 0.
01 — REQUIREMENTS ET PARAMÈTRES

Requirements et paramètres avancés

Les requirements valident les segments d'URL — les paramètres optionnels et les defaults contrôlent ce qui est requis pour matcher.

Les requirements sont des expressions régulières qui s'appliquent aux segments dynamiques de l'URL. Sans requirement, un paramètre accepte n'importe quelle valeur sauf le slash /. Avec un requirement, seules les URL qui satisfont la regex sont matchées.

Requirements — deux syntaxes
La syntaxe longue via le paramètre requirements de l'attribut, et la syntaxe inline directement dans le chemin.
// Syntaxe longue — requirements séparés
#[Route('/user/{id}', requirements: ['id' => '\d+'])]

// Syntaxe inline — pattern dans le chemin avec {nom}
#[Route('/user/{id<\d+>}')]

// Plusieurs requirements
#[Route('/blog/{year}/{slug}', requirements: [
    'year' => '\d{4}',
    'slug' => '[\w-]+',
])]

Patterns courants

PatternCorrespond àExemple
\d+Un ou plusieurs chiffres/user/42
\d{4}Exactement 4 chiffres/blog/2024
[\w-]+Lettres, chiffres, tirets/article/mon-titre
[a-z]{2}Code langue ISO 2 lettres/fr, /en
1|2|3Valeurs fixes/api/v1, /api/v2

Paramètres optionnels

Trois façons d'indiquer qu'un paramètre est facultatif :

// 1. Point d'interrogation sans valeur par défaut → null
#[Route('/blog/{page?}')]

// 2. Point d'interrogation avec valeur par défaut inline
#[Route('/blog/{page?1}')]

// 3. Via defaults (syntaxe explicite)
#[Route('/blog/{page}', defaults: ['page' => 1])]
Variables spéciales
Ces variables ne viennent pas du chemin — elles enrichissent le tableau de match ou contrôlent le comportement du framework.
VariableRôle
_localeLocale active sur cette route
_formatFormat de réponse (html, json, xml…)
_statelessSi true, désactive la session pour cette requête
_controllerContrôleur à invoquer — défini via defaults si pas d'attribut
_routeNom de la route matchée (lu en lecture seule)
_canonical_routeNom de la route parente pour les routes localisées

Requirements sur les enums PHP

Pour restreindre un paramètre aux cas d'un enum backed, on liste les valeurs :

enum Status: string
{
    case Active   = 'active';
    case Archived = 'archived';
    case Draft    = 'draft';
}

#[Route('/posts/{status}', requirements: ['status' => 'active|archived|draft'])]
// Encore mieux : générer dynamiquement depuis l'enum
#[Route('/posts/{status}', requirements: [
    'status' => implode('|', array_column(Status::cases(), 'value'))
])]
⚠️ Attention aux caractères spéciaux dans les regex : dans un attribut PHP, les backslashes doivent être doublés dans les chaînes entre guillemets ("\d+" = \\d+). Avec les heredocs ou les requirements en tableau inline d'attribut, un seul backslash suffit.

Saisis une URL et observe quelles routes la matchent.

02 — GÉNÉRER DES URLS

generate() — générer des URLs

generate() transforme un nom de route + paramètres en URL — les constantes de référence contrôlent si l'URL est absolue, relative, ou de réseau.

Générer une URL à la main c'est coller des chaînes — fragile, non refactorable. La méthode generate() de UrlGeneratorInterface résout ça : tu donnes un nom de route et un tableau de paramètres, elle retourne l'URL correcte selon la configuration actuelle.

Signature de generate()
generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string

Les 4 types de référence

ConstanteValeurFormat retournéExemple
ABSOLUTE_PATH 1 Chemin absolu (défaut) /blog/mon-article
ABSOLUTE_URL 0 URL complète avec schéma et hôte https://example.com/blog/mon-article
RELATIVE_PATH 2 Chemin relatif depuis l'URL courante ../../blog/mon-article
NETWORK_PATH 3 Sans schéma, avec hôte //example.com/blog/mon-article
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

// Injection dans un service
public function __construct(private readonly UrlGeneratorInterface $urlGenerator) {}

// Chemin absolu (par défaut)
$url = $this->urlGenerator->generate('blog_show', ['slug' => 'mon-article']);
// → /blog/mon-article

// URL absolue — pour les emails, les webhooks
$url = $this->urlGenerator->generate(
    'blog_show',
    ['slug' => 'mon-article'],
    UrlGeneratorInterface::ABSOLUTE_URL
);
// → https://example.com/blog/mon-article

Paramètres supplémentaires → query string

Tout paramètre passé à generate() qui ne correspond pas à un segment de la route est automatiquement ajouté en query string :

$url = $this->urlGenerator->generate('blog_list', [
    'page'     => 3,
    'category' => 'php',
    'sort'     => 'date',
]);
// → /blog?page=3&category=php&sort=date

Paramètres spéciaux

// _fragment → ancre #
$url = $this->urlGenerator->generate('article_show', [
    'slug'      => 'intro-symfony',
    '_fragment' => 'section-2',
]);
// → /article/intro-symfony#section-2

// _query → fusion avec les paramètres existants
$url = $this->urlGenerator->generate('search', [
    '_query' => ['q' => 'symfony', 'page' => 1],
]);
// → /search?q=symfony&page=1

Exceptions

ExceptionCause
RouteNotFoundException Le nom de route passé n'existe pas dans la RouteCollection
MissingMandatoryParametersException Un paramètre sans valeur par défaut est absent de l'appel
InvalidParameterException La valeur d'un paramètre ne satisfait pas son requirement
💡 ABSOLUTE_URL nécessite un RequestContext configuré. En dehors d'une requête HTTP (commande console, queue), il faut configurer manuellement RequestContext::setHost(), setScheme(), setHttpPort() — sinon Symfony génère une URL avec localhost.
🔑 NETWORK_PATH est utile pour les CDN : l'URL s'adapte automatiquement au schéma de la page qui la référence (HTTP ou HTTPS) sans hardcoder le protocole.
03 — ROUTING DANS LE CONTRÔLEUR

Routing dans le contrôleur

Dans les contrôleurs Symfony, #[Route] décrit la route, generateUrl() génère les URLs, et $request->attributes contient les paramètres matchés.

Dans un contrôleur Symfony, le routing s'intègre naturellement : l'attribut #[Route] décrit la route, les paramètres du chemin arrivent directement comme arguments de la méthode, et generateUrl() encapsule l'accès au générateur.

#[Route] sur une méthode de contrôleur

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/blog', name: 'blog_')]
final class BlogController extends AbstractController
{
    #[Route('/', name: 'index', methods: 'GET')]
    public function index(): Response
    {
        // route blog_index → GET /blog/
        return $this->render('blog/index.html.twig');
    }

    #[Route('/{slug}', name: 'show', methods: 'GET',
        requirements: ['slug' => '[\w-]+'])]
    public function show(string $slug): Response
    {
        // Symfony injecte $slug depuis les attributs de la requête
        // route blog_show → GET /blog/{slug}
        return $this->render('blog/show.html.twig', ['slug' => $slug]);
    }
}

generateUrl() — raccourci de AbstractController

// generateUrl() enveloppe UrlGeneratorInterface::generate()
$url = $this->generateUrl('blog_show', ['slug' => 'mon-article']);
// → /blog/mon-article

// URL absolue
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

$url = $this->generateUrl(
    'blog_show',
    ['slug' => 'mon-article'],
    UrlGeneratorInterface::ABSOLUTE_URL
);
// → https://example.com/blog/mon-article

$request->attributes — lire les paramètres matchés

use Symfony\Component\HttpFoundation\Request;

public function show(Request $request, string $slug): Response
{
    // Paramètres disponibles dans $request->attributes
    $routeName  = $request->attributes->get('_route');      // 'blog_show'
    $routeParams = $request->attributes->get('_route_params'); // ['slug' => '...']
    $controller = $request->attributes->get('_controller'); // 'App\...\BlogController::show'
    $slugValue  = $request->attributes->get('slug');       // idem que $slug
}

redirect() et redirectToRoute()

// Redirection vers une URL externe ou interne
return $this->redirect('https://example.com');
return $this->redirect($this->generateUrl('homepage'), 301);

// Redirection via nom de route — le plus courant
return $this->redirectToRoute('blog_index');
return $this->redirectToRoute('blog_show', ['slug' => 'nouveaute'], 302);
Injection directe de UrlGeneratorInterface
Dans un service hors contrôleur, injecte directement l'interface via le constructeur. Ne pas hériter d'AbstractController depuis un service — c'est réservé aux contrôleurs.
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

final class NotificationMailer
{
    public function __construct(
        private readonly UrlGeneratorInterface $urlGenerator
    ) {}

    public function sendConfirmation(string $token): void
    {
        $link = $this->urlGenerator->generate(
            'confirm_email',
            ['token' => $token],
            UrlGeneratorInterface::ABSOLUTE_URL
        );
    }
}
🔑 Les paramètres de la route arrivent comme arguments de méthode grâce à l'argument resolver de Symfony. Nomme-les exactement comme dans le chemin de la route — {slug} → argument string $slug. Le type PHP est automatiquement casté (int pour \d+).
04 — CONDITION ET ROUTES DYNAMIQUES

Condition et routes dynamiques

L'option condition filtre les routes avec une expression ExpressionLanguage — les routes s'appliquent seulement si l'expression est vraie.

Le paramètre condition d'une route accepte une expression ExpressionLanguage. Contrairement aux requirements (qui portent sur la forme de l'URL), la condition s'évalue à l'exécution en interrogeant la requête ou le contexte — un filtre dynamique sur le monde réel.

Trois variables disponibles dans une condition
request — l'objet HttpFoundation\Request complet
context — le RequestContext (hôte, schéma, méthode, baseUrl)
params — les paramètres déjà extraits par le matching de l'URL
// Conditionner sur un header HTTP
#[Route('/api/data',
    condition: "request.headers.get('Accept') === 'application/json'"
)]

// Conditionner sur le sous-domaine
#[Route('/dashboard',
    condition: "context.getHost() matches '/^admin\\..*$/'"
)]

// Conditionner sur HTTPS
#[Route('/checkout',
    condition: "context.getScheme() === 'https'"
)]

// Combiner plusieurs conditions
#[Route('/api/beta',
    condition: "request.headers.has('X-Beta') and context.getScheme() === 'https'"
)]

Condition vs priority — deux mécanismes distincts

prioritycondition
Évaluation À la compilation (ordre statique) À l'exécution (par requête)
Coût Nul — déterminé une fois Faible — exécuté à chaque requête
Porte sur L'ordre de test des routes Le résultat du test (oui/non)
Usage Routes génériques vs spécifiques sur même chemin Variantes d'une route selon le contexte

Cas d'usage typiques

Route dev-only
Combine le filtre env (qui supprime la route en prod) avec une condition sur l'IP pour une protection supplémentaire :
#[Route('/debug/mail', env: 'dev',
    condition: "request.getClientIp() === '127.0.0.1'"
)]
Route pour robots / crawlers
Répondre différemment aux bots sans changer l'URL :
#[Route('/article/{slug}', name: 'article_bot', priority: 1,
    condition: "request.headers.get('User-Agent') matches '/Googlebot|Bingbot/i'"
)]
#[Route('/article/{slug}', name: 'article_show')]
💡 A/B testing : teste une fonctionnalité pour un sous-ensemble d'utilisateurs en conditionnant sur un cookie ou un header : condition: "request.cookies.has('ab_variant_b')"
⚠️ Une route avec condition non satisfaite n'est pas ignorée définitivement : si aucune autre route ne matche, Symfony lance quand même une ResourceNotFoundException. La condition ne "saute pas" la route — elle empêche son match, ce qui peut mener à un 404 si c'était la seule candidate.
05 — ROUTECOLLECTION ET LOADERS

RouteCollection et loaders

RouteCollection accumule les routes issues de toutes les sources — les loaders décrivent comment les charger.

Derrière les attributs et les fichiers YAML se cache une structure commune : la RouteCollection. C'est elle qui est compilée en cache PHP. Les loaders sont les adaptateurs qui lisent vos sources et alimentent cette collection.

RouteCollection
Conteneur de routes indexées par nom. Toutes les sources (attributs, YAML, PHP, closures) finissent dans une seule RouteCollection fusionnée avant compilation.

API de RouteCollection

use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();

// Ajouter une route
$routes->add('blog_show', new Route('/blog/{slug}', [
    '_controller' => 'App\Controller\BlogController::show',
]));

// Préfixer tous les chemins de la collection
$routes->addPrefix('/api');
// Chaque route /blog/{slug} devient /api/blog/{slug}

// Préfixer tous les noms
$routes->addNamePrefix('api_');
// 'blog_show' → 'api_blog_show'

// Restreindre toutes les routes à un host
$routes->setHost('api.{domain}', [], ['domain' => '\w+']);

// Fusionner deux collections
$main->addCollection($routes);

routes.php — RoutingConfigurator

Symfony fournit une API fluide pour déclarer les routes en PHP, plus souple que YAML :

// config/routes.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return function (RoutingConfigurator $routes): void {
    $routes->add('homepage', '/')
        ->controller([HomeController::class, 'index'])
        ->methods(['GET']);

    $routes->add('blog_show', '/blog/{slug}')
        ->controller([BlogController::class, 'show'])
        ->requirements(['slug' => '[\w-]+'])
        ->defaults(['_format' => 'html'])
        ->methods(['GET'])
        ->host('example.com')
        ->priority(10);

    // Importer un autre fichier ou un répertoire de contrôleurs
    $routes->import('../src/Controller/', 'attribute')
        ->prefix('/app')
        ->namePrefix('app_');
};

Loaders disponibles

LoaderSourceType d'import
AttributeDirectoryLoader Répertoire de classes PHP 'attribute'
AttributeFileLoader Fichier PHP unique 'attribute'
YamlFileLoader Fichier .yaml / .yml 'yaml'
PhpFileLoader Fichier routes.php 'php'
GlobFileLoader Pattern glob (*.yaml) 'glob'
ClosureLoader Closure retournant une RouteCollection 'closure'
💡 CompiledRoute — le cache : Symfony compile chaque route en un objet CompiledRoute avec staticPrefix, regex, tokens et pathVariables. Ce code PHP compilé est stocké dans var/cache/ et rechargé à chaque requête sans relire les fichiers de config.
🔑 Ordre de fusion : les collections ajoutées via addCollection() ne préservent pas automatiquement la priorité. Si deux routes ont le même chemin, c'est la dernière ajoutée qui gagne — sauf si tu utilises explicitement priority.
06 — ROUTES AVEC HOST

Routes avec host

L'option host limite une route à un domaine ou un sous-domaine — utile pour les multi-tenants ou les sous-domaines d'API.

L'option host ajoute une contrainte de domaine à la route — elle ne matche que si la requête arrive sur le bon hôte. C'est le mécanisme de base pour les architectures multi-tenants, les sous-domaines d'API ou les domaines par locale.

Domaine fixe

// Cette route ne répond qu'à api.example.com
#[Route('/users', name: 'api_users', host: 'api.example.com')]
public function users(): Response { /* ... */ }

// Sur www.example.com, cette route retourne une ResourceNotFoundException

Variable dans le host

Le domaine peut contenir des paramètres, exactement comme un chemin :

// Sous-domaine dynamique : alice.myapp.com, bob.myapp.com
#[Route('/dashboard',
    name:         'tenant_dashboard',
    host:         '{tenant}.myapp.com',
    requirements: ['tenant' => '[a-z][a-z0-9-]+'],
)]
public function dashboard(string $tenant): Response
{
    // $tenant vaut 'alice' si la requête vient de alice.myapp.com
}

setHost() sur RouteCollection

Pour appliquer une contrainte de host à un groupe entier de routes en routes.php :

return function (RoutingConfigurator $routes): void {
    $apiRoutes = $routes->import('../src/Controller/Api/', 'attribute');
    $apiRoutes->host('api.{domain}')
               ->requirements(['domain' => 'example\.com|example\.fr'])
               ->prefix('/v1')
               ->namePrefix('api_v1_');
};

RequestContext pour la génération d'URL absolues

Quand on génère une URL avec une contrainte de host, Symfony a besoin du contexte courant. En dehors d'une requête HTTP :

use Symfony\Component\Routing\RequestContext;

$context = new RequestContext();
$context->setHost('alice.myapp.com');
$context->setScheme('https');
$context->setHttpsPort(443);

$urlGenerator->setContext($context);

$url = $urlGenerator->generate(
    'tenant_dashboard',
    ['tenant' => 'alice'],
    UrlGeneratorInterface::ABSOLUTE_URL
);
// → https://alice.myapp.com/dashboard
🔑 La contrainte host est évaluée après le chemin. Symfony teste d'abord le chemin — si plusieurs routes ont le même chemin mais des hosts différents, il sélectionne celle dont le host matche. Si aucun host ne matche, ResourceNotFoundException.
💡 Développement local : sans domaine personnalisé, le host peut être défini via SERVER_NAME dans ta configuration web. Utilise http://alice.localhost ou configure un wildcard dans /etc/hosts pour tester les routes à sous-domaine.
07 — ROUTES LOCALISÉES

Routes localisées

Les routes localisées permettent d'avoir des paths différents par locale — Symfony génère automatiquement une route par locale.

Les routes localisées permettent d'avoir une URL différente par langue tout en gardant un seul contrôleur. Symfony crée automatiquement une route par locale et maintient un lien entre elles via _canonical_route.

Routes localisées en attribut PHP
Le paramètre path accepte un tableau associatif locale → chemin. Symfony génère autant de routes que de locales.
#[Route(
    path: ['en' => '/blog/{slug}', 'fr' => '/article/{slug}'],
    name: 'blog_post',
    methods: 'GET',
    requirements: ['slug' => '[\w-]+'],
)]
public function show(string $slug): Response { /* ... */ }

Symfony génère deux routes distinctes :

Nom de routeChemin_canonical_route
blog_post.en/blog/{slug}blog_post
blog_post.fr/article/{slug}blog_post

Génération d'URL avec _locale

// Générer la version française
$url = $this->generateUrl('blog_post', [
    'slug'    => 'mon-article',
    '_locale' => 'fr',
]);
// → /article/mon-article

// Générer la version anglaise
$url = $this->generateUrl('blog_post', [
    'slug'    => 'my-article',
    '_locale' => 'en',
]);
// → /blog/my-article

// Cibler directement une variante localisée
$url = $this->generateUrl('blog_post.fr', ['slug' => 'mon-article']);
// → /article/mon-article

_canonical_route — SEO et navigation

_canonical_route est automatiquement défini à 'blog_post' sur les deux variantes. Il sert à générer les balises hreflang :

// Dans un contrôleur ou un template : générer les alternatives linguistiques
$canonicalRoute = $request->attributes->get('_canonical_route'); // 'blog_post'
$currentParams  = $request->attributes->get('_route_params');      // ['slug' => '...']

// <link rel="alternate" hreflang="fr" href="..." />
foreach (['en', 'fr'] as $locale) {
    $href = $this->generateUrl($canonicalRoute, array_merge(
        $currentParams,
        ['_locale' => $locale]
    ), UrlGeneratorInterface::ABSOLUTE_URL);
}

Routes localisées en YAML

# config/routes.yaml
blog_post:
    path:
        en: /blog/{slug}
        fr: /article/{slug}
    controller: App\Controller\BlogController::show
    methods: GET
    requirements:
        slug: '[\w-]+'
🔑 Préférer le nom canonique dans les templates Twig : utilise path('blog_post', {'slug': ..., '_locale': app.locale}) plutôt que path('blog_post.fr', ...) — le nom canonique s'adapte automatiquement à la locale active, tandis que la variante suffixée la force.
💡 Combinaison avec le préfixe de locale : dans les applications multilingues, certains architectes préfèrent /{_locale}/blog/{slug} pour une URL /en/blog/my-article. Les routes localisées font l'inverse — un chemin totalement différent par langue. Les deux approches sont légitimes selon le besoin SEO.
08 — MATCH() ET EXCEPTIONS

match() et exceptions

Le matching transforme une URL en tableau de paramètres — les exceptions signalent les cas d'erreur avec précision.

Le matching est l'opération inverse de la génération : une URL entre, un tableau de paramètres sort. UrlMatcherInterface::match() est le cœur du processus, et ses exceptions décrivent précisément pourquoi une URL n'a pas matché.

Signature de match()
match(string $pathinfo): array — retourne un tableau associatif ou lance une exception.
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

$context = new RequestContext();
$matcher = new UrlMatcher($routes, $context);

// Matcher un chemin
$params = $matcher->match('/blog/mon-article');

// Résultat typique :
// [
//   '_route'      => 'blog_show',
//   '_controller' => 'App\Controller\BlogController::show',
//   'slug'        => 'mon-article',
//   '_locale'     => 'fr',     // si route localisée
//   '_canonical_route' => 'blog_show', // si variante localisée
// ]

matchRequest() — avec la méthode HTTP

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;

// matchRequest prend en compte Request::getMethod()
$params = $matcher->matchRequest($request);

Les trois exceptions

ExceptionHTTPCauseInfos supplémentaires
ResourceNotFoundException 404 Aucune route ne correspond au chemin
MethodNotAllowedException 405 Le chemin matche mais pas la méthode HTTP getAllowedMethods(): string[]
RouteNotFoundException Nom de route inexistant dans generate()
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;

try {
    $params = $matcher->match($path);
} catch (MethodNotAllowedException $e) {
    // Chemin trouvé mais mauvaise méthode
    $allowed = $e->getAllowedMethods(); // ['GET', 'HEAD']
    // → répondre 405 avec header Allow: GET, HEAD
} catch (ResourceNotFoundException $e) {
    // Aucune route ne correspond
    // → répondre 404
}

Déboguer avec debug:router

# Lister toutes les routes
php bin/console debug:router

# Chercher une route par nom
php bin/console debug:router blog_show

# Tester le matching d'une URL
php bin/console router:match /blog/mon-article

# Tester avec une méthode spécifique
php bin/console router:match /blog/mon-article --method=POST
💡 router:match est ton meilleur outil de diagnostic — il te dit exactement quelle route a matché, quels paramètres ont été extraits, et si une route est rejetée pour cause de méthode non autorisée.

Saisis un chemin, sélectionne une méthode HTTP, et observe le résultat du matching.

09 — SYNTHÈSE

Synthèse — guide de choix et anti-patterns

Le guide de choix et les anti-patterns du Routing Symfony.

Dix sections en arrière, tu avais un attribut #[Route]. Maintenant tu as le tableau complet : requirements, génération, conditions, collections, hosts, locales et matching. Voici le guide de décision pour choisir le bon outil.

Quand utiliser quoi

BesoinOutilPourquoi
Contraindre la forme d'un segment d'URL requirements Évalué à la compilation, coût nul à l'exécution
Filtrer selon la requête (header, schéma, IP) condition Accès à request et context via ExpressionLanguage
Deux routes sur même chemin, une plus spécifique priority Ordre de test statique, sans coût d'exécution
Restreindre à un sous-domaine / domaine host Contrainte sur l'hôte de la requête, pas le chemin
URLs différentes par langue Routes localisées (path: {en: ..., fr: ...}) Symfony génère une route par locale, _canonical_route les lie
Grouper des routes (préfixe, host, nom) RouteCollection ou import avec prefix/host/namePrefix Évite la répétition, modifiable en un seul endroit

Variables spéciales — récapitulatif

VariableDirectionRôle
_routematch() → outNom de la route matchée
_controllerdefaults → in / match() → outContrôleur à invoquer
_localedefaults ou path / generate() inLocale active
_formatdefaultsFormat de réponse par défaut
_statelessdefaultsDésactive la session
_canonical_routematch() → outNom canonique des routes localisées
_fragmentgenerate() inAncre #... dans l'URL générée
_querygenerate() inFusion query string dans l'URL générée

Exceptions — récapitulatif

ExceptionContexteHTTP
ResourceNotFoundException match() — aucune route ne correspond 404
MethodNotAllowedException match() — chemin OK mais méthode interdite 405
RouteNotFoundException generate() — nom de route inexistant
MissingMandatoryParametersException generate() — paramètre requis absent
InvalidParameterException generate() — valeur ne satisfait pas le requirement

4 anti-patterns à éviter

🚫 Requirements trop permissifs
{id} sans requirement \d+ matche aussi /user/delete ou /user/export — des mots qui ne sont pas des IDs. Toujours contraindre les paramètres numériques.
🚫 Condition à la place de priority
Utiliser condition: "true" ou une condition statique pour forcer l'ordre de matching au lieu de priority. La condition est évaluée à chaque requête — priority est déterminé une fois à la compilation.
🚫 generate() avec ABSOLUTE_URL sans RequestContext
Dans une commande console ou un worker de queue, le contexte n'est pas automatiquement alimenté. Oublier de configurer RequestContext::setHost() produit des URLs avec http://localhost/....
🚫 Ignorer _canonical_route dans les templates multilingues
Générer les liens hreflang depuis _route (qui vaut blog_post.fr) au lieu de _canonical_route (qui vaut blog_post) produit des URLs hreflang incorrectes — blog_post.fr n'existe pas avec _locale=en.