Apprendre Symfony 6 : Routage
Dans Symfony, toute requête HTTP passe par le routeur qui va définir qui est en charge du traitement de la requête.
Qu’est-ce qu’une route ?
Le routage est un mécanisme par lequel les requêtes HTTP sont acheminées vers le code qui les gère.
Concrètement une route définie que lorsqu’un utilisateur demande une certaine URL ; on déclenche une méthode de contrôleur
Pour parler Programmation Orientée Objet, dans Symfony un contrôleur est une classe, et chaque route va appeler une de ses méthodes.
Le routeur est le garant du bon routage de l’application. Pour cela, il se base sur un fichier de routes associant à chaque URL qui existe pour l’application, les contrôleurs et leurs méthodes associées.
Où écrire ses routes ?
Il est aujourd’hui possible d’écrire ses routes à de nombreux endroits de notre application :
- Dans des fichiers dédiées (YAML, XML, PHP)
- Dans nos contrôleurs
Nous détaillerons dans cette formation l’écriture des routes dans nos contrôleurs, car il s’agit aujourd’hui de la méthode la plus couramment utilisée.
Une route s’écrit à travers une sorte de commentaire interprété appelé « annotations » (PHP < 8
) ou « attributs » (PHP ≥ 8
) située juste au-dessus de la méthode de contrôleur qu’elle doit déclencher.
Annotation
Les annotations permettent de définir des métadonnées dans votre code dans un bloc défini avec la syntaxe suivante /** * @... */
.
L’annotation @Route
permet de déclarer une route.
/**
* @Route('/route-de-demo', name='demo')
*/
/route-de-demo
: chemin de la routedemo
: nom de la route
Attribut PHP
Les attributs PHP permettent de définir des métadonnées dans votre code dans un bloc défini avec la syntaxe suivante #[...]
. Ces métadonnées sont ensuite lues par l’API Reflection de PHP.
L’attribut Route
permet de déclarer une route.
#[Route('/route-de-demo', name: 'demo')]
/route-de-demo
: chemin de la routedemo
: nom de la route
En raison de sa modernité, c’est cette approche que nous utiliserons au cours de cette formation.
Commençons donc par créer un contrôleur dans notre projet avec la ligne de commande suivante :
symfony console make:controller
Routes statiques
On entend ici par « route statique » une route dont le chemin ne change jamais, par opposition aux « routes dynamiques ».
#[Route('/', name: 'home')]
public function home(): Response { ... }
#[Route('/blog', name: 'article_list')]
public function listArticles(): Response { ... }
#[Route('/admin/articles/ajouter', name: 'admin_article_add')]
public function addArticle(): Response { ... }
Dans l’exemple précédent :
- Si j’accède à
monsite.com
, je matcherai avec la routehome
et déclencherai la méthodehome()
. - Si j’accède à
monsite.com/blog
, je matcherai avec la routearticle_list
et déclencherai la méthodelistArticles()
. - Si j’accède à
monsite.com/admin/articles/ajouter
, je matcherai avec la routearticle_add
et déclencherai la méthodeaddArticle()
.
Routes dynamiques
On entend ici par « route dynamique » une route dont le chemin varie en fonction des valeurs affectées à ses paramètres. Ces routes sont essentielles pour créer des pages au contenu dynamique, comme par exemple :
- Une page de produit
- Une page d’article de blog
- Un feed d’un utilisateur sur un réseau social
- Une page de profil utilisateur
- …
Paramètres de routes
Syntaxe
L’information qui va permettre de dynamiser ce contenu passe par l’URL : on ajoute en fait un paramètre dont le nom sera noté entre accolades.
// Exemple avec un paramètre "id" 👇
#[Route('/blog/{id}', name: 'article_show')]
public function showArticle(int $id): Response {
// Ici, je peux utiliser $id...
}
// Exemple avec un paramètre "slug" 👇
#[Route('/blog/{slug}', name: 'article_show')]
public function showArticle(string $slug): Response {
// Ici, je peux utiliser $slug...
}
Généralement, ce paramètre est un id
ou slug
(pour des URLs « SEO-friendly »).
Paramètres optionnels
Parfois, il est possible de ne pas préciser de valeur pour un paramètre lorsqu’on appelle une route. Dans ces cas-là, il est possible de lui définir une valeur par défaut.
Paramètre optionnel défini via l'argument nommé defaults
L’argument nommé defaults
permet de définir une valeur par défaut pour un paramètre.
#[Route('/blog/{id}', name: 'article_show', defaults: ['id' => 1])]
public function showArticle(int $id): Response { ... }
Paramètre optionnel défini au sein d'une méthode
Il est également possible d’affecter une valeur par défaut lors de la déclaration du paramètre au sein de la fonction.
#[Route('/blog/{id}', name: 'article_show')]
public function showArticle(int $id = 1): Response { ... }
Contraintes
On s’attend à ce que dans l’URL :
monsite.com/blog/{slug}
; slug soit une chaîne de caractèremonsite.com/blog/{id}
; id soit un nombre
C’est du bon sens… Mais le bon sens est humain et notre code ne l’est pas, il ne fait que ce qu’on lui dit de faire.
Il ne bronchera donc pas si je souhaite accéder à l’URL monsite.com/blog/784
ou monsite.com/blog/je-suis-un-article
.
Le typage du paramètre de route permettrait de définir un type scalaire attendu pour un paramètre (généralement int
ou string
).
#[Route('/blog/{id}', name: 'article_show')]
public function showArticle(int $id): Response { ... }
Si j’envoie une string dans le paramètre {id}
, Symfony génèrera une erreur serveur 500
. C’est mieux, mais pas encore idéal… nous préfèrerions plutôt afficher une jolie page 404
.
Pour contrôler la validité d’un paramètre de route, on exploite alors les expressions régulières PHP :
\d+
indique un nombre de n’importe quelle longueur\w
(=[a-zA-Z0-9_]
) indique un caractère alphanumérique ou un tiret de soulignement|
indique qu’un caractère doit matcher avec au moins une valeur dans une liste (très utilisé pour les traductions)- … toutes les expressions régulières ici
Cette contrainte peut être spécifiée via :
Contrainte définie via l'argument nommé requirements
L’argument nommé requirements
permet de définir un format attendu pour un paramètre.
#[Route('/blog/{id}', name: 'article_show', requirements: ['id' => '\d+'])]
public function showArticle(int $id): Response { ... }
Contrainte définie au sein du paramètre de route
Avec la syntaxe {parametre<expression_reguliere>}
#[Route('/blog/{id<\d+>}', name: 'article_show')]
public function showArticle(int $id): Response { ... }
Méthodes HTTP
Une requête HTTP peut être envoyée du client vers le serveur selon diverses méthodes HTTP (aussi appelées verbes HTTP).
GET
pour récupérer des données du serveur (généralement pour l’affichage d’une page web)POST
pour écrire des données sur le serveur (création d’un élément après avoir soumis un formulaire).PATCH
/PUT
pour modifier des données sur le serveur (édition d’un élément après avoir soumis un formulaire).DELETE
pour supprimer des données sur le serveur (suppression d’un élément).
Nous effectuerons donc la récupération via la méthode GET
et l’ajout, la modification ou encore la suppression via la méthode POST
.
Par défaut, une route est appelable avec n’importe quelle méthode HTTP. Il est néanmoins possible de dire à Symfony : « cette route ne répondra qu’à un appel à une requête de type POST
» avec l’argument methods
.
Dans un CRUD
:
- Une route qui liste des articles de blog verra ses requêtes d’accès en
GET
(récupération des articles depuis le serveur). - Une route qui affiche les détails d’un article de blog verra ses requêtes d’accès en
GET
(récupération de l’article depuis le serveur). - Une route qui affiche et traite un formulaire d’ajout d’article verra ses requêtes d’accès en
GET
etPOST
(récupération et affichage du formulaire + soumission du formulaire). - Une route qui affiche et traite un formulaire d’édition d’article verra ses requêtes d’accès en
GET
etPOST
(récupération et affichage du formulaire + soumission du formulaire). - Une déclenchée pour supprimer un article verra ses requêtes d’accès en
POST
(suppression des données sur le serveur).
#[Route('/blog', name: 'article_list', methods: ['GET'])]
public function listArticles(): Response { ... }
#[Route('/blog/{slug}', name: 'article_show', methods: ['GET'])]
public function showArticle(string $slug): Response { ... }
#[Route('/admin/articles/ajouter', name: 'admin_article_add', methods: ['GET', 'POST'])]
public function addArticle(): Response { ... }
#[Route('/admin/articles/{id}/modifier', name: 'admin_article_edit', methods: ['GET', 'POST'])]
public function editArticle(int $id): Response { ... }
#[Route('/admin/articles/{id}/supprimer', name: 'admin_article_delete', methods: ['POST'])]
public function deleteArticle(int $id): Response { ... }
Préfixe de routes
Par convention, on crée un contrôleur par grande partie de notre site.
Je pourrais donc très bien choisir de rassembler :
- Toute la gestion de mes articles dans un unique contrôleur
ArticleController
- La partie administration de mon blog (création, modification et suppression d’articles) dans un contrôleur dédié
AdminController
et la partie publique de mon site (consultation des articles) dans un contrôleur dédiéPublicController
.
Si je choisis de préfixer toutes les URL de mes routes d’administration par /admin
, je vais donc devoir le faire dans toutes mes routes individuellement…
#[Route('/admin/articles/ajouter', name: 'admin_article_add', methods: ['GET', 'POST'])]
public function addArticle(): Response { ... }
#[Route('/admin/articles/{id}/modifier', name: 'admin_article_edit', methods: ['GET', 'POST'])]
public function editArticle(int $id): Response { ... }
#[Route('/admin/articles/{id}/supprimer', name: 'admin_article_delete', methods: ['POST'])]
public function deleteArticle(int $id): Response { ... }
Il est alors possible de déclarer ce préfixe de route à l’ensemble des méthodes d’un contrôleur en ajoutant un attribut PHP Route
à la classe entière.
#[Route('/admin/articles')]
class AdminController {
// /admin automatiquement ajouté au début des routes
}
Ordre des routes
Imaginons avoir deux routes (une statique et une dynamique) avec la même architecture :
/produits/nouveau
/produits/{slug}
On doit alors toujours, dans notre fichier de routage, déclarer en dernier la route dynamique. Dans le cas inverse, la route dynamique bypasserait toujours la route statique, en supposant que nouveau
soit un paramètre de route pour le slug.
#[Route('/produits/{slug}', name: 'product_show')]
public function showProduct(string $slug): Response { ... }
#[Route('/produits/nouveau', name: 'product_add')]
public function addProduct(): Response { ... }
Si je souhaite accéder à l’URL monsite.com/produits/nouveau
pour accéder au formulaire d’ajout de produit, cela matchera avec la première route car Symfony va penser que « nouveau » est le slug d’un produit… 😥