05 - Symfony : Le routing avancé
Objectifs
- ✅ Définir des paramètres pour tes routes
- ✅ Définir des contraintes pour tes routes
- ✅ Utiliser les routes définies
Pré-requis
Avoir terminé la quête
Introduction
User stories 📋
en tant que visiteur, je souhaite récupérer les données d'une série
en tant que visiteur, je souhaite récupérer la liste des séries
en tant que contributeur, je souhaite ajouter une série
In progress 🤸
en tant que visiteur, je souhaite visualiser la fiche d'une série
Doing 🏗️
création de la route /program/{id}
Done ✅
Installation des outils de gestion d'assets
En tant qu'utilisateur je souhaite accéder à la page d'accueil
Dans une précédente quête, tu as vu comment créer une simple route /program/ qui appelle une méthode de contrôleur avant d'afficher une vue Twig. Dans cette quête, tu vas voir comment créer des routes plus avancées, te permettant de passer des paramètres et d'imposer un certain nombre de contraintes.
Sommaire
Rappels

Rappel : les routes se configurent directement dans les contrôleurs à l'aide des attributs, une nouvelle syntaxe apportée par PHP8 et disponible depuis Symfony 6. Avant cela, la configuration utilisait les annotations. Tu risques encore de trouver beaucoup de documentation avec cette ancienne syntaxe. Il existe par ailleurs d'autres manières de configurer les routes (XML, Yaml, PHP), tu trouveras toutes ces informations dans la documentation, mais nous allons nous focaliser sur la nouvelle manière de faire avec les attributs.
Sur ce premier exemple, la syntaxe avec les annotations est donnée pour que tu puisses comparer, mais ensuite seuls les attributs seront évoqués.
// src/Controller/ProgramController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProgramController extends AbstractController
{
#[Route('/program/', name: 'program_index')]
public function index(): Response
{
// return render twig
}
}
// src/Controller/ProgramController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ProgramController extends AbstractController
{
/**
* @Route("/program/", name="program_index")
*/
public function index(): Response
{
// return render twig
}
}
Dans ce cas, lorsque le router de Symfony reçoit une requête vers la route /program/, il va regarder dans toutes les routes configurées dans l'application. Si l'une d'entre elles correspond à ce motif, il exécute la méthode associée à l'attribut, ici la méthode index() de ProgramController. S'il ne trouve pas la route, il renverra un code de statut HTTP 404.
Si plusieurs définitions de route correspondent au même chemin (ici /program/), c'est la première route rencontrée dans le fichier qui est prioritaire. Note donc bien que le routeur s'arrête dès qu'il a trouvé une correspondance.
Remarque : L'attribut #[Route()] peut également prendre un name en paramètre. Même si cela est optionnel, indique systématiquement un nom pour tes routes. Cela te facilitera grandement la vie ensuite pour générer dynamiquement des URLs dans ton code (pour des liens Twig ou des redirections).
Une application possède souvent des dizaines de routes. Pour les lister, il existe une commande Symfony dédiée (à exécuter depuis la racine du projet) :
php bin/console debug:router
Cette commande peut être d'une grande aide pour t'aider à déboguer des problèmes de routing, n'hésite donc pas à l'utiliser.
bin/console ?! C'est quoi ce bin's ?
Lors de la création d'un projet Symfony, tu as à ta disposition un utilitaire qu'on appelle la console. Il contient un ensemble de commandes très pratiques pour debugger, générer des fichiers automatiquement, effectuer des migrations en base de données...etc.
Pour lister toutes les commandes disponibles, exécute la commande suivante à la racine de ton projet :
php bin/console list
👀 Tu notes que la commande debug:router est bien listée.
Tout au long du projet Wild Series tu seras amené à utiliser différentes commandes de la console. Pour information, sache qu'il est également possible de créer tes propres commandes.
symfony console : lorsque tu te trouves à la racine de ton projet, tu peux aussi utiliser la console en lançant symfony console. Ceci est un alias pour php bin/console.
(Fin de la parenthèse..., retournons à nos moutons 🐑 )

Regarde plus en détail le fonctionnement de la commande, les différents paramètres existants et n'hésite pas à la tester sur ton projet.
Documentation officielle sur le routing Symfony.
Le routing avancé
🃏 Les paramètres de route
Tu le sais, tu as parfois besoin de passer des paramètres à tes routes via l'url (l'id d'un produit pour afficher ses détails par exemple, etc.). Encore une fois, Symfony permet de spécifier, via les attributs, qu'une route attend un paramètre. Comment ? En incluant dans la route un { joker } (wildcard en anglais).
Prenons un exemple : (⚠️ Ne reproduis pas l'exemple dans ton Wild Series)
Imaginons que l'on souhaite lister toutes les séries (= programmes) de notre Wild Series. Comme on en a énoooormément, il va falloir paginer notre liste. On souhaite pouvoir spécifier le numéro de la page directement dans l'URL ; Par ex. : /program/list/5 affiche la liste des programmes de la 5e page.
Voici comment procéder :
// src/Controller/WildController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ProgramController extends AbstractController
{
[...]
#[Route('/program/list/{page}', name:'program_list')]
public function list(int $page): Response
{
return $this->render('program/list.html.twig', ['page' => $page]);
}
}
👀 Comme tu le vois, il est très simple de passer un paramètre via le router. Il suffit d'ajouter ce dernier entre accolades, ici {page}, où tu le souhaites dans ta route. Si tu saisis la route /program/list/5, le paramètre {page} de la route va prendre la valeur 5, puis transférer cette valeur dans le paramètre du même nom, ici $page, de la méthode list().
Il est bien entendu possible d'ajouter autant de paramètres que nécessaires, généralement séparés par des slashes, par exemple /program/list/{page}/{limit}. Fais cependant attention à la lisibilité de tes routes !
Tu peux ensuite utiliser la variable $page dans ton code comme bon te semble. Dans l'exemple, la donnée est passée en contexte au template Twig, mais tu pourrais t'en servir pour tout autre chose.
Exemple templates/program/list.html.twig :
<h1>Page number {{ page }}.</h1>
Valeur par défaut
Il est possible de spécifier une valeur par défaut pour les paramètres. Lorsqu'on utilise les attributs, les paramètres par défaut sont définis dans les arguments de l'action du contrôleur comme dans l'exemple ci-dessous :
#[Route('/program/list/{page}', name:'program_list')]
public function list(int $page = 1): Response
{
// ...
}
Dans ce cas, si l'on se rend sur la route /program/list/ le paramètre page prendra la valeur de 1.
Attention : Suivant la même logique qu'en PHP, si un paramètre est déclaré optionnel, tous les autres paramètres lui succédant devront, eux aussi, être optionnels. Autrement dit, les paramètres optionnels se situent toujours en fin de route (et donc de méthode).
Les contraintes de route
Restreindre le format des paramètres
La route que tu viens de définir nécessite donc un paramètre {page}. Logiquement, un numéro de page doit être un entier. Cependant, si tu ne fais aucun contrôle, il est tout à fait possible de saisir autre chose en paramètre, par ex /program/list/cinq/, ce qui pourrait poser problème. Il faut donc interdire cela.

Heureusement, le router de Symfony embarque la possibilité d'imposer des limitations aux paramètres.
#[Route('/program/list/{page}', requirements: ['page'=>'\d+'], name: 'program_list')]
public function list(int $page = 1): Response
{
// ...
}
L'exemple ci-dessus reprend le précédent, en ajoutant l'option requirements, qui permet d'ajouter des prérequis aux différents paramètres.
Ici, pour le paramètre page, le prérequis est \d+. Tu reconnaîtras une expression régulière qui correspond à un chiffre répété de 1 à n fois, autrement dit un entier. Les requirements sont toujours définis par une regex, qui est un moyen simple et puissant de décrire des motifs variables.
Alternative : il est aussi possible d'écrire les requirements de manière plus condensée : #[Route('/program/list/{page<\d+>}', name: 'program_list')]. Cette écriture inline nécessite l'utilisation des chevrons < > pour encadrer la regex.
Si jamais cette notion d'expression régulière t'est étrangère, tu peux jeter un œil à la quête ci-dessous :
- Quête 98 (hors archive)
Au delà des requirements sur le format des paramètres, il est également possible de définir d'autres contraintes à tes routes.
Autoriser certaines méthodes HTTP
Certaines routes ne devraient être accessibles qu'en GET, d'autres qu'en POST (et pareillement avec d'autres verbes HTTP). L'attribut #[Route()] peut donc prendre en paramètres les types de méthode acceptés.
Quelques exemples :
- Tu souhaites créer une nouvelle série (affichage en
GETet traitement du formulaire enPOST).
#[Route('/program/new', methods: ['GET', 'POST'], name: 'program_new')]
- Tu souhaites afficher une série en fonction de son identifiant, cela se fera plutôt par un lien, donc en
GET.
#[Route('/program/{id}', methods: ['GET'], name: 'program_show')]
- Tu souhaites supprimer une série, tu pourrais limiter la méthode à
DELETEuniquement.
#[Route('/program/{id}', methods: ['DELETE'], name: 'program_delete')]
Comme tu le remarques, dans ce cas les routes program_show et program_delete ont la même définition /program/{id}, mais la méthode HTTP définie n'étant pas la même, il n'y a pas de conflits entre elles.
Le router les considère bien comme deux routes différentes et saura quelle méthode appeler en fonction de la méthode HTTP utilisée.
Note : Les routes d'API se reposent beaucoup sur ces contraintes de méthodes HTTP.
Plus d'informations sur d'autres paramètres plus avancés comme _locale, _format, etc.
Un petit rappel des différentes méthodes HTTP.
Les préfixes de route
Le préfixe s'applique à la classe d'un contrôleur afin - comme son nom l'indique - de préfixer les routes contenues dans ce contrôleur. La syntaxe est exactement la même que pour les méthodes.
Il est également possible de spécifier un name afin de préfixer tous les noms de routes des méthodes d'un contrôleur.
Par exemple, on pourrait écrire :
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/program', name: 'program_')]
class ProgramController extends AbstractController
{
// Correspond à la route /program/ et au name "program_index"
#[Route('/', name: 'index')]
public function index(): Response
{
// ...
}
// Correspond à la route /program/new et au name "program_new"
#[Route('/new', name: 'new')]
public function new(): Response
{
// ...
}
}
Utiliser et générer les routes
... dans un contrôleur
L'utilisation la plus fréquente des routes au sein d'un contrôleur est la redirection (en GET), suite à la validation d'un formulaire par exemple.
Pour cela, tu utiliseras la méthode du contrôleur redirectToRoute() qui prend deux paramètres :
- le nom de la route
- un tableau de paramètres (optionnel) à passer à cette route.
Cette méthode redirectToRoute() remplace un header("Location: …") et renvoie un objet Response.
N'oublie pas le mot clé return devant, sinon cela ne fonctionnera pas !
public function new(): Response
{
// traitement d'un formulaire par exemple
// redirection vers la page 'program_show',
// correspondant à l'url /program/4
return $this->redirectToRoute('program_show', ['id' => 4]);
}
Il faut bien comprendre que la redirection revient au même que de taper l'url dans le navigateur, et donc le code de la méthode show() correspondant à la route /program/4 va bien s'exécuter et l'éventuelle vue associée s'affichera donc. Pour rappel, après une redirection, les données disponibles (en POST par exemple, ici celles de la page new), sont perdues.
dans le contrôleur
... dans un template Twig
Dans tes vues, tu es souvent amené à afficher des liens internes à ton application. Pour définir la cible de ces liens, tu pourrais très bien entrer la route "en dur", par exemple :
<a href="/program/list/5">Afficher la page 5</a>
Cependant, si tu modifies la définition de ta route dans ton contrôleur, alors ton lien ne fonctionnera plus 😭. Dommage !
Une manière plus robuste de faire cela est d'utiliser la fonction {{ path() }} de twig, qui sert à faire appel à une route prédéfinie. Là encore, il faut lui donner le nom de la route (d'où l'intérêt de bien définir un name à chacune de tes routes) et une liste optionnelle de paramètres (attention à la syntaxe particulière qui contient parenthèses et accolades).
<a href="{{ path('program_list', {page: 5}) }}">Afficher la page 5</a>
Maintenant, si tu devais modifier la définition de ta route program_list, de /program/list/{page} à /program/page/{page} par exemple, l'URL du lien serait mise à jour automatiquement de /program/list/5 à /program/page/5.
en Twig
💪 Challenge
Crée ta propre route
Crée une route /program/{id}/ permettant de charger une vue affichant l'id du programme sous forme de titre, dans une balise <h1>.
- Le paramètre
iddoit être un entier.- Si l'id n'est pas un entier, la route devra renvoyer une erreur 404 (utilise les requirements de paramètres).
- La route devra être reliée à une méthode
showduProgramController. - La route ne devra être accessible qu'en
GET.
Critères de validation
- La route est correctement définie, via un attribut, et est reliée à la méthode
show()deProgramController. - La classe
ProgramControllerest annotée de manière à ce que toutes les routes de ce controller soient préfixées par/program/et leur name préfixé par "program_". - Une vue templates/program/show.html.twig est créée.
- Le résultat correspond aux attentes :
- La route
/program/4affiche bien une vue avec en titre "Program 4" dans un<h1>. - La route
/program/quatren'affiche rien (erreur 404) car le paramètre n'est pas un entier.
- La route
- La route n'est accessible qu'en
GET. Tu peux tester qu'unPOSTsur ta route te retourne bien un 405 en exécutant la commande suivante dans ton terminal.
curl -I -X POST "http://localhost:8000/program/4"
Lien de téléchargement pour curl ou alors sudo apt install curl.
- Fais un commit et un push de ton travail.