Archives Wild Code School
Symfony

05 - Symfony : Le routing avancé

Quête 8 sur 35 · 1 image archivée

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

Quêtes liées
title-book
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

Attention

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.

php
// 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
   }
}
php
// 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.

Info

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) :

bash
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 :

bash
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.

Info

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 🐑 )

↗
Deboguer le router

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.

symfony.com
↗
Routing Symfony

Documentation officielle sur le routing Symfony.

symfony.com

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 :

php
// 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 :

php
<h1>Page number {{ page }}.</h1>

↗
Les paramètres dans les routes
symfony.com

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 :

php
    #[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

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).

↗
Paramètres optionnels
symfony.com

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.

php
  #[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.

Info

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êtes liées
  • 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.

↗
Validation des paramètres
symfony.com

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 GET et traitement du formulaire en POST).
php
#[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.
php
#[Route('/program/{id}', methods: ['GET'], name: 'program_show')]
  • Tu souhaites supprimer une série, tu pourrais limiter la méthode à DELETE uniquement.
php
#[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.

↗
Paramètres de routes spéciaux

Plus d'informations sur d'autres paramètres plus avancés comme _locale, _format, etc.

symfony.com
↗
Les méthodes HTTP

Un petit rappel des différentes méthodes HTTP.

fr.wikipedia.org

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 :

php
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
     {
         // ...
     }
}
↗
Préfixage des noms de routes
symfony.com

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 !

php
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]);
}
Info

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.

↗
Rediriger vers une route

dans le contrôleur

symfony.com

... 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 :

twig
<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).

twig
<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.

↗
Faire un lien vers une route dans la vue

en Twig

symfony.com

💪 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 id doit ê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 show du ProgramController.
  • 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() de ProgramController.
  • La classe ProgramController est 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/4 affiche bien une vue avec en titre "Program 4" dans un <h1>.
    • La route /program/quatre n'affiche rien (erreur 404) car le paramètre n'est pas un entier.
  • La route n'est accessible qu'en GET. Tu peux tester qu'un POST sur ta route te retourne bien un 405 en exécutant la commande suivante dans ton terminal.
bash
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.