Symfony Router : routes avancees (priority, conditions, locale)
Le composant Router de Symfony est l'un des plus puissants du framework. Au-dela des routes basiques, il offre des fonctionnalites avancees - priorite, conditions, routage par locale, host-based routing - qui permettent de gerer des architectures complexes avec elegance. Ce guide couvre les techniques avancees du routage Symfony 7.2 avec des attributs PHP 8.3.
Definition de routes avec les attributs PHP
Depuis Symfony 6.2, les attributs PHP sont la methode recommandee pour definir les routes. Ils remplacent les annotations Doctrine et les fichiers YAML/XML pour la plupart des cas d'usage :
<?php
// src/Controller/ArticleController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/articles', name: 'app_article_')]
class ArticleController extends AbstractController
{
#[Route('', name: 'index', methods: ['GET'])]
public function index(): Response
{
// Liste des articles
return $this->render('article/index.html.twig');
}
#[Route('/{slug}', name: 'show', methods: ['GET'])]
public function show(string $slug): Response
{
// Detail d'un article
return $this->render('article/show.html.twig', ['slug' => $slug]);
}
#[Route('/new', name: 'create', methods: ['GET', 'POST'])]
public function create(): Response
{
// Creation d'un article
return $this->render('article/create.html.twig');
}
}
L'attribut #[Route] au niveau de la classe definit un prefixe commun pour toutes les routes du controleur. Le prefixe de nom app_article_ est concatene avec le nom de chaque methode.
Requirements : valider les parametres de route
Les requirements permettent de contraindre les parametres de route avec des expressions regulieres. C'est essentiel pour la securite et pour eviter les conflits entre routes :
#[Route('/article/{id}', name: 'app_article_show', requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
// $id est garanti d'etre un entier positif
}
#[Route('/blog/{slug}', name: 'app_blog_show', requirements: ['slug' => '[a-z0-9-]+'])]
public function blogShow(string $slug): Response
{
// $slug ne contient que des lettres minuscules, chiffres et tirets
}
#[Route('/archive/{year}/{month}', name: 'app_archive', requirements: [
'year' => '\d{4}',
'month' => '0[1-9]|1[0-2]'
])]
public function archive(int $year, string $month): Response
{
// year = 4 chiffres, month = 01 a 12
}
Vous pouvez aussi utiliser les constantes de la classe Requirement pour les patterns courants :
use Symfony\Component\Routing\Requirement\Requirement;
#[Route('/product/{id}', requirements: ['id' => Requirement::POSITIVE_INT])]
#[Route('/user/{uid}', requirements: ['uid' => Requirement::UID_RFC4122])]
#[Route('/page/{date}', requirements: ['date' => Requirement::DATE_YMD])]
Valeurs par defaut
Les parametres de route peuvent avoir des valeurs par defaut, rendant le parametre optionnel dans l'URL :
#[Route('/blog/{page}', name: 'app_blog_index', defaults: ['page' => 1], requirements: ['page' => '\d+'])]
public function index(int $page): Response
{
// /blog => page = 1
// /blog/3 => page = 3
}
// Syntaxe alternative avec valeur par defaut dans le path
#[Route('/blog/{page<\d+>?1}', name: 'app_blog_index')]
public function indexAlt(int $page): Response
{
// Meme comportement, syntaxe plus compacte
}
La syntaxe inline {param<requirement>?default} est particulierement pratique pour les cas simples.
Host-based routing : routes par sous-domaine
Symfony permet de router en fonction du nom d'hote. C'est indispensable pour les applications multi-tenant ou avec des sous-domaines specifiques :
#[Route('/dashboard', name: 'admin_dashboard', host: 'admin.example.com')]
public function adminDashboard(): Response
{
// Accessible uniquement via admin.example.com/dashboard
}
#[Route('/dashboard', name: 'api_dashboard', host: 'api.{domain}', requirements: ['domain' => '.+'])]
public function apiDashboard(string $domain): Response
{
// api.example.com/dashboard, api.example.fr/dashboard, etc.
}
// Au niveau de la classe pour tout un controleur
#[Route(host: 'admin.{domain}', requirements: ['domain' => 'example\.com|example\.fr'])]
class AdminController extends AbstractController
{
// Toutes les routes de ce controleur sont limitees aux sous-domaines admin.*
}
Routage par locale : applications multilingues
Pour les applications multilingues, Symfony offre un support natif du routage par locale avec plusieurs strategies :
// Strategie 1 : Prefixe de locale dans le path
#[Route('/{_locale}/articles', name: 'app_articles', requirements: ['_locale' => 'fr|en|de'])]
public function articles(string $_locale): Response
{
// /fr/articles, /en/articles, /de/articles
}
// Strategie 2 : Paths differents par locale
#[Route(path: [
'fr' => '/articles',
'en' => '/articles-list',
'de' => '/artikel'
], name: 'app_articles')]
public function articlesLocalized(): Response
{
// Chaque locale a son propre path
}
// Strategie 3 : Prefixe global dans config/routes.yaml
// config/routes.yaml
// controllers:
// resource: ../src/Controller/
// type: attribute
// prefix: /{_locale}
// requirements:
// _locale: fr|en|de
// defaults:
// _locale: fr
Le parametre special _locale est reconnu par Symfony et definit automatiquement la locale de la requete, ce qui impacte les traductions. Pour approfondir l'internationalisation, consultez notre article sur Symfony Translator et l'i18n avancee.
Priorite des routes
Quand deux routes peuvent matcher la meme URL, la priorite determine laquelle est selectionnee. Par defaut, les routes sont evaluees dans l'ordre de declaration. L'attribut priority permet de forcer un ordre specifique :
// Sans priorite, cette route serait evaluee apres les routes dynamiques
// et ne serait jamais matchee pour /articles/featured
#[Route('/articles/featured', name: 'app_articles_featured', priority: 10)]
public function featured(): Response
{
// Prend la priorite sur /articles/{slug} grace a priority: 10
}
#[Route('/articles/{slug}', name: 'app_articles_show', priority: 0)]
public function show(string $slug): Response
{
// Evaluee apres /articles/featured
}
Les valeurs de priorite sont relatives : une priorite plus elevee signifie une evaluation plus tot. En pratique, definissez des priorites explicites uniquement quand un conflit existe.
Conditions de route
L'attribut condition permet d'ajouter des conditions supplementaires basees sur le langage d'expression Symfony :
#[Route('/api/data', name: 'api_data', condition: "request.headers.get('Accept') matches '/application\\/json/'")]
public function apiData(): Response
{
// Match uniquement si le header Accept contient application/json
}
#[Route('/promo', name: 'app_promo', condition: "request.getClientIp() in ['127.0.0.1', '::1']")]
public function promo(): Response
{
// Accessible uniquement en local
}
Les conditions sont puissantes mais a utiliser avec parcimonie. Pour la plupart des cas, les requirements et les methodes HTTP suffisent.
Generation d'URLs avec UrlGeneratorInterface
Generer des URLs a partir de noms de routes est une pratique essentielle pour maintenir des liens coherents :
<?php
namespace App\Controller;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class NewsletterController extends AbstractController
{
#[Route('/newsletter/confirm/{token}', name: 'app_newsletter_confirm')]
public function confirm(string $token): Response
{
return new Response('Confirme !');
}
#[Route('/newsletter/send', name: 'app_newsletter_send')]
public function send(UrlGeneratorInterface $urlGenerator): Response
{
// URL relative : /newsletter/confirm/abc123
$relativeUrl = $urlGenerator->generate('app_newsletter_confirm', [
'token' => 'abc123'
]);
// URL absolue : https://example.com/newsletter/confirm/abc123
$absoluteUrl = $urlGenerator->generate('app_newsletter_confirm', [
'token' => 'abc123'
], UrlGeneratorInterface::ABSOLUTE_URL);
// Dans un controleur, raccourci disponible :
$url = $this->generateUrl('app_newsletter_confirm', ['token' => 'abc123']);
return new Response("URL: $absoluteUrl");
}
}
En Twig, utilisez les fonctions path() et url() :
{# URL relative #}
<a href="{{ path('app_article_show', {slug: article.slug}) }}">Lire</a>
{# URL absolue (pour les emails par exemple) #}
<a href="{{ url('app_article_show', {slug: article.slug}) }}">Lire</a>
Debugger les routes : debug:router
La commande debug:router est votre meilleur allie pour diagnostiquer les problemes de routage :
# Lister toutes les routes
php bin/console debug:router
# Filtrer par nom
php bin/console debug:router --show-controllers | grep article
# Details d'une route specifique
php bin/console debug:router app_article_show
# Tester quelle route match une URL
php bin/console router:match /articles/mon-article
# Tester avec une methode HTTP specifique
php bin/console router:match /api/users --method=POST
La commande router:match est particulierement utile en debug. Elle indique exactement quelle route est matchee pour une URL donnee, avec tous les parametres extraits.
Bonnes pratiques de routage
Voici les conventions que j'applique systematiquement sur mes projets Symfony :
- Nommez toutes vos routes : ne generez jamais d'URL avec le path en dur. Le nom de route protege contre les changements d'URL
- Utilisez des prefixes de nom au niveau de la classe pour eviter les collisions
- Specifiez les methodes HTTP :
methods: ['GET'],methods: ['POST']. Ne laissez jamais une route accepter toutes les methodes - Definissez des requirements sur tous les parametres dynamiques pour eviter les matchs indesirables
- Evitez les conditions complexes : si votre condition devient illisible, il vaut mieux la deplacer dans un EventListener sur
kernel.request
Pour comprendre comment Flex gere l'enregistrement automatique des routes, consultez notre guide sur Symfony Flex et les recipes. Et pour securiser l'acces a vos routes, ne manquez pas notre article sur la securite Symfony.
Vous avez un projet Symfony avec des besoins de routage complexes - multi-tenant, multilingue, API versionnee ? Consultez mes tarifs, explorez mes services, ou contactez-moi pour en discuter.
