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.

Icône de calendrier
Intermédiaire
13 chapitres

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.

Schéma du fonctionnement du routeur dans Symfony

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.

copié !
/**
 * @Route('/route-de-demo', name='demo')
 */
  • /route-de-demo : chemin de la route
  • demo : 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.

copié !
#[Route('/route-de-demo', name: 'demo')]
  • /route-de-demo : chemin de la route
  • demo : 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 :

copié !
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 ».

copié !
#[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 route home et déclencherai la méthode home().
  • Si j’accède à monsite.com/blog, je matcherai avec la route article_list et déclencherai la méthode listArticles().
  • Si j’accède à monsite.com/admin/articles/ajouter, je matcherai avec la route article_add et déclencherai la méthode addArticle().

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.

copié !
// 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.

copié !
#[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.

copié !
#[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ère
  • monsite.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).

copié !
#[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.

copié !
#[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>}

copié !
#[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 et POST (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 et POST (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).
copié !
#[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…

copié !
#[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.

copié !
#[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.

copié !
#[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… 😥