Aller au contenu

Routing

Bloc Laravel · séance 2 · ~60 min

Slides du chapitre (PDF)PDF

Au chapitre MVC, vous avez écrit une seule route : quand le navigateur demande /posts avec une requête GET, Laravel appelle la méthode index() de PostController. Un vrai site a besoin de bien plus de routes. Pour afficher la fiche d'un livre, l'adresse contient son numéro, comme /livres/12. Pour enregistrer un formulaire, le navigateur envoie une requête POST plutôt que GET. Toutes ces routes se déclarent dans routes/web.php. À chaque requête, Laravel parcourt ces routes de haut en bas et garde la première qui correspond à l'adresse et au verbe demandés.

À la fin de ce chapitre, vous serez capables de :

  1. écrire une route qui reçoit un ou plusieurs paramètres, obligatoires ou optionnels, et limiter les valeurs acceptées,
  2. prédire quelle route répond à une requête, ou pourquoi Laravel répond 404 ou 405,
  3. corriger deux routes déclarées dans le mauvais ordre, de deux façons,
  4. choisir entre une fonction anonyme, un contrôleur, Route::view et Route::redirect,
  5. nommer une route et produire son adresse avec route().

1. La route du chapitre MVC

Voici cette route telle qu'elle figure dans routes/web.php, dans votre projet premier-test-laravel :

Route::get('/posts', [PostController::class, 'index']);

La commande php artisan route:list affiche toutes les routes que Laravel connaît. Au chapitre MVC, cette liste contenait aussi des routes que vous n'aviez pas écrites, comme storage/{path} et up. Le code qui leur répond se trouve dans le dossier vendor/. La route up, par exemple, est déclarée dans votre projet par la ligne health: '/up' de bootstrap/app.php, vue au chapitre Structure d'un projet Laravel, mais c'est une classe de Laravel, dans vendor/, qui lui répond. L'option --except-vendor retire de la liste les routes dont le code est dans vendor/. Il ne reste que celles dont le code est dans vos fichiers.

php artisan route:list --except-vendor
  GET|HEAD       / .............................................................. routes/web.php:6
  GET|HEAD       posts ...................................................... PostController@index

                                                                                Showing [2] routes

Chaque ligne se lit de gauche à droite. Elle donne d'abord la méthode HTTP, qu'on appelle aussi le verbe, le mot employé dans ce chapitre. Viennent ensuite le chemin, écrit sans la barre oblique du début, puis l'endroit où se trouve le code qui répond. La route / est une fonction écrite directement dans le fichier des routes. La liste indique donc le fichier et la ligne où sa déclaration commence, routes/web.php:6. La route posts appelle un contrôleur, et la liste indique la classe et la méthode, PostController@index.

2. Une route, quatre morceaux

Tout au long de ce chapitre, vous complétez le même fichier routes/web.php. Les deux routes du projet, / et /posts, restent en tête du fichier, et chaque route que vous écrivez s'ajoute à la suite des précédentes. Quand une route déjà écrite reçoit une contrainte ou un nom, le texte vous demande de la remplacer. Les numéros de ligne affichés par route:list correspondent au fichier obtenu en suivant cet ordre.

Voici une route qui réunit les quatre éléments qu'une déclaration peut contenir. Ne la recopiez pas : la classe BookController n'existe pas dans votre projet, vous l'écrirez au chapitre Controllers.

Route::get('/livres/{id}', [BookController::class, 'show'])->name('books.show');
  • Le verbe : le nom de la méthode appelée sur Route indique le verbe HTTP que la route accepte. Route::get répond aux requêtes GET, Route::post aux requêtes POST.
  • Le chemin : '/livres/{id}' contient une partie fixe, livres, et une partie entre accolades, {id}, qui accepte n'importe quelle valeur.
  • L'action : le code que Laravel exécute quand l'adresse demandée correspond à cette route. Ici, c'est la méthode show() de BookController.
  • Le nom : ->name('books.show') donne à la route un nom que vous utiliserez dans votre code pour obtenir son adresse, au lieu de la recopier à la main. Le nom est facultatif, et la section 8 montre à quoi il sert.

Anatomie d'une route Laravel : verbe, chemin avec paramètre, action et nom, chacun dans un médaillon numéroté

Une fonction anonyme comme action

Une fonction anonyme est une fonction sans nom, écrite directement à l'endroit où elle sert. Vous en avez lu une au chapitre Lire du code Laravel du bloc POO, avec function (Blueprint $table). Quand le second argument de Route::get est une fonction anonyme, Laravel l'exécute chaque fois qu'une requête correspond à la route, puis envoie au navigateur la valeur qu'elle a renvoyée. Ajoutez cette route à la fin de routes/web.php, sous la route /posts :

Route::get('/ping', function () {
    return 'pong';
});

Demandez ensuite la page avec curl. Sous PowerShell, tapez curl.exe à la place de curl.

curl -s http://premier-test-laravel.test/ping
pong

Laravel place la chaîne renvoyée par la fonction dans le contenu de la réponse, ce que le navigateur affiche, et y joint le code 200, qui signifie que la requête a abouti. La route / créée avec le projet a la même forme : sa fonction anonyme renvoie view('welcome').

Tout écrire dans routes/web.php ?

Vous pourriez aussi écrire la route /posts du chapitre MVC avec une fonction anonyme. Il suffit de copier le contenu de la méthode index() dans la fonction. Comme la route n'utilise plus PostController mais le modèle Post, la ligne use en haut du fichier importe Post à la place :

use App\Models\Post;
use Illuminate\Support\Facades\Route;

// …

Route::get('/posts', function () {
    $posts = Post::latest()->get();

    return view('posts.index', ['posts' => $posts]);
});

Le navigateur reçoit exactement la même page qu'au chapitre MVC, au caractère près. La seule différence apparaît dans php artisan route:list --except-vendor : la route posts n'affiche plus un contrôleur, mais la ligne de routes/web.php où la fonction est écrite.

  GET|HEAD       / .............................................................. routes/web.php:6
  GET|HEAD       ping .......................................................... routes/web.php:16
  GET|HEAD       posts ......................................................... routes/web.php:10

                                                                                Showing [3] routes

Si vous avez fait l'essai, remettez ensuite la route /posts et l'import de PostController du chapitre MVC. Les deux écritures fonctionnent. Dans ce cours, vous relierez pourtant /posts à PostController, pour trois raisons :

  • routes/web.php se lit comme une liste. Quand chaque route appelle un contrôleur, elle tient sur une ligne qui donne le verbe, le chemin et la méthode à appeler. Si chacune de trente routes contenait dix lignes de code, il faudrait parcourir trois cents lignes pour retrouver les adresses du site.
  • Chaque fichier garde son rôle, comme au chapitre MVC. C'est le contrôleur qui demande les articles au modèle et choisit la vue. Le fichier des routes se contente d'indiquer quel contrôleur appeler pour chaque adresse.
  • route:list vous dit quel fichier ouvrir. Avec un contrôleur, la liste affiche PostController@index, et vous savez tout de suite quelle méthode modifier. Avec une fonction anonyme, elle affiche seulement routes/web.php:10, un numéro de ligne qui change dès que vous ajoutez une route plus haut dans le fichier.

Dans ce cours, une route qui interroge un modèle ou prépare une vue avec des données appelle un contrôleur. Les exemples et les exercices de ce chapitre renvoient seulement une phrase, sans modèle ni vue. Une fonction anonyme suffit donc, et vous passerez aux contrôleurs au chapitre Controllers.

3. Comment Laravel choisit une route

Ajoutez ces quatre routes à la fin de routes/web.php, sous la route /ping :

Route::get('/livres', function () {
    return 'Liste des livres';
});

Route::post('/livres', function () {
    return 'Livre enregistré';
});

Route::get('/livres/{id}', function (string $id) {
    return "Fiche du livre $id";
});

Route::get('/auteurs/{name}', function (string $name) {
    return "Livres de $name";
});

Pour choisir une route, Laravel découpe d'abord l'adresse en segments, les morceaux compris entre deux barres obliques. /livres/12 compte ainsi deux segments, livres et 12.

Laravel reprend ensuite les routes de routes/web.php une à une, de haut en bas, et se pose deux questions pour chacune. Le chemin de la route a-t-il autant de segments que l'adresse, avec les mêmes mots aux places fixes ? Un segment entre accolades, comme {id}, accepte n'importe quel mot à sa place. La route accepte-t-elle le verbe de la requête ? Dès qu'une route répond oui aux deux questions, Laravel exécute son action et ne regarde pas les routes suivantes.

L'ensemble des routes, parcouru dans cet ordre, s'appelle la table de routage. Le tableau suivant indique, pour sept requêtes, la route que Laravel retient, la valeur que reçoit la fonction et la réponse renvoyée :

Requête Route retenue Valeur reçue Réponse
GET /ping GET /ping aucune pong
GET /livres GET /livres aucune Liste des livres
POST /livres POST /livres aucune Livre enregistré
GET /livres/12 GET /livres/{id} $id vaut "12" Fiche du livre 12
GET /auteurs/Nothomb GET /auteurs/{name} $name vaut "Nothomb" Livres de Nothomb
GET /livres/12/avis aucune aucune 404
DELETE /livres aucune pour ce verbe aucune 405

/livres/12/avis compte trois segments, et aucune route n'en déclare trois. Laravel répond alors par le code 404, « page introuvable » :

curl -s -i http://premier-test-laravel.test/livres/12/avis
HTTP/1.1 404 Not Found
…

Sans option, curl n'affiche que le contenu de la réponse. L'option -i, vue au chapitre Framework et librairie, affiche en plus la ligne de statut, la première ligne, qui porte le code, puis les en-têtes. Le contenu, remplacé ici par …, est une page HTML intitulée Not Found.

Pour DELETE /livres, deux routes déclarent bien le chemin /livres, mais l'une accepte GET et l'autre POST. Aucune n'accepte DELETE. L'option -X DELETE demande à curl d'envoyer une requête DELETE :

curl -s -i -X DELETE http://premier-test-laravel.test/livres
HTTP/1.1 405 Method Not Allowed
…

Laravel répond par le code 405, Method Not Allowed, c'est-à-dire « verbe non autorisé ». Comme votre .env contient APP_DEBUG=true, la page d'erreur donne le détail, avec ce message :

The DELETE method is not supported for route livres. Supported methods: GET, HEAD, POST.

4. Les paramètres de route

Un paramètre obligatoire

À l'exercice 2 du chapitre MVC, vous avez prévu une route pour /posts/3, avec un paramètre pour recevoir le 3. Ce paramètre s'écrit entre accolades. Dans '/livres/{id}', le segment {id} s'appelle un paramètre de route. Quand la requête /livres/12 arrive, Laravel lit 12 à cette place dans l'adresse et le donne à la fonction anonyme, qui le reçoit dans sa variable $id. Ce paramètre fait partie du chemin, contrairement au paramètre d'URL ?ordre=ancien du chapitre MVC, placé après le point d'interrogation.

curl -s http://premier-test-laravel.test/livres/12
curl -s http://premier-test-laravel.test/livres/abc
Fiche du livre 12
Fiche du livre abc

Laravel transmet le segment exactement comme il est écrit dans l'adresse, sous forme de chaîne de caractères, sans vérifier qu'il s'agit d'un nombre. abc est donc accepté comme numéro de livre, et vous verrez plus bas comment le refuser.

Plusieurs paramètres

Une route peut contenir plusieurs paramètres. Imaginez un agenda en ligne dont chaque page montre un mois précis, avec une adresse comme /agenda/2026/10 pour octobre 2026. Ajoutez cette route à la fin du fichier :

Route::get('/agenda/{year}/{month}', function (int $year, int $month) {
    return "Agenda du mois $month de l'année $year";
})->whereNumber(['year', 'month']);
curl -s http://premier-test-laravel.test/agenda/2026/10
Agenda du mois 10 de l'année 2026

La fin de la déclaration, ->whereNumber(['year', 'month']), ajoute une condition à la route : les segments year et month ne doivent contenir que des chiffres. Pour l'adresse /agenda/2026/octobre, la condition n'est pas remplie, la route ne correspond pas, et Laravel répond 404. Ce type de condition s'appelle une contrainte, et la fin de cette section en présente d'autres.

Laravel transmet toujours les segments sous forme de chaînes, ici "2026" et "10". Comme la fonction déclare int $year et int $month, PHP convertit ces chaînes en entiers au moment de l'appel.

Sans whereNumber, la route accepterait /agenda/2026/octobre. Laravel appellerait la fonction avec la chaîne "octobre" pour $month, et PHP ne pourrait pas la convertir en entier. La réponse porterait le code 500, erreur du serveur, et la page d'erreur afficherait une TypeError avec ce message :

Argument #2 ($month) must be of type int, string given

Laravel donne les valeurs à la fonction dans l'ordre où les accolades apparaissent dans le chemin, sans regarder le nom des variables. Si vous écrivez function (int $month, int $year), la même adresse affiche Agenda du mois 2026 de l'année 10.

Un paramètre optionnel

Un point d'interrogation placé juste après le nom, à l'intérieur des accolades, comme {day?}, rend le paramètre optionnel : l'adresse peut contenir ce segment ou s'arrêter avant. La fonction doit alors prévoir une valeur par défaut, utilisée quand l'adresse ne fournit rien. Dans l'exemple suivant, une bibliothèque publie ses horaires, et sans jour dans l'adresse, la page montre ceux du lundi. Ajoutez cette route à la fin du fichier :

Route::get('/horaires/{day?}', function (string $day = 'lundi') {
    $hours = match ($day) {
        'samedi' => 'de 9 h à 12 h',
        'dimanche' => 'fermé',
        default => 'de 9 h à 18 h',
    };

    return "Le $day : $hours";
});
curl -s http://premier-test-laravel.test/horaires
curl -s http://premier-test-laravel.test/horaires/samedi
curl -s http://premier-test-laravel.test/horaires/mardi
Le lundi : de 9 h à 18 h
Le samedi : de 9 h à 12 h
Le mardi : de 9 h à 18 h

match compare $day aux valeurs écrites à gauche des flèches, 'samedi' puis 'dimanche', et renvoie le texte écrit à droite de la première valeur égale. La ligne default s'applique à tous les autres jours, comme mardi.

Si vous oubliez la valeur par défaut et écrivez function (string $day), l'adresse /horaires/samedi fonctionne encore, car elle fournit un jour. Pour /horaires, en revanche, Laravel appelle la fonction sans argument, alors qu'elle en exige un. La réponse porte le code 500, et la page d'erreur affiche ArgumentCountError avec ce message :

Too few arguments to function …{closure:…/routes/web.php:36}(), 0 passed … and exactly 1 expected

Le nombre 36 est la ligne où commence la route /horaires dans le fichier. Remettez ensuite la valeur par défaut 'lundi'.

Les contraintes

whereNumber n'est pas la seule contrainte. Chaque contrainte fixe une condition que la valeur d'un paramètre doit remplir. Si la valeur ne la remplit pas, Laravel considère que la route ne correspond pas et essaie la route suivante du fichier. Voici trois routes qui utilisent chacune une contrainte différente. Remplacez la route /auteurs/{name} écrite plus haut par la première, puis ajoutez les deux autres à la fin du fichier :

Route::get('/auteurs/{name}', function (string $name) {
    return "Livres de $name";
})->whereAlpha('name');

Route::get('/convertir/{amount}/{currency}', function (int $amount, string $currency) {
    $rate = ['usd' => 1.17, 'gbp' => 0.87];

    return "$amount EUR = " . $amount * $rate[$currency] . ' ' . strtoupper($currency);
})->whereNumber('amount')->whereIn('currency', ['usd', 'gbp']);

Route::get('/themes/{slug}', function (string $slug) {
    return "Livres du thème $slug";
})->where('slug', '[a-z0-9-]+');

Les taux de change écrits dans $rate sont inventés pour l'exemple. Voici ce que renvoient ces trois routes pour huit adresses :

Adresse Code Réponse
/auteurs/Nothomb 200 Livres de Nothomb
/auteurs/Nothomb2 404
/convertir/100/usd 200 100 EUR = 117 USD
/convertir/100/gbp 200 100 EUR = 87 GBP
/convertir/100/chf 404
/convertir/12.5/usd 404
/themes/romans-policiers 200 Livres du thème romans-policiers
/themes/Romans 404
Contrainte Valeurs acceptées Refusé dans l'exemple
whereNumber('amount') des chiffres seulement 12.5, à cause du point
whereAlpha('name') des lettres sans accent seulement Nothomb2, et aussi Hergé
whereIn('currency', ['usd', 'gbp']) une valeur de la liste chf
where('slug', '[a-z0-9-]+') le motif donné Romans, à cause de la majuscule

Le second argument de where(), '[a-z0-9-]+', est une expression régulière : un motif qui décrit les caractères permis. Les crochets listent les caractères acceptés, ici les minuscules de a à z, les chiffres et le tiret. Le + qui suit exige au moins un de ces caractères.

La contrainte whereIn protège aussi le calcul. Sans elle, l'adresse /convertir/100/chf correspondrait à la route, la fonction chercherait $rate['chf'], une case absente du tableau, et Laravel afficherait une page d'erreur avec le code 500.

5. Le piège de l'ordre

Imaginez un site de recettes de cuisine. Une première page affiche une recette à partir de son numéro, une seconde affiche le formulaire qui permet d'en ajouter une nouvelle. Ajoutez ces deux routes à la fin du fichier, dans cet ordre :

Route::get('/recettes/{id}', function (string $id) {
    return "Recette $id";
});

Route::get('/recettes/nouvelle', function () {
    return 'Formulaire de nouvelle recette';
});
curl -s http://premier-test-laravel.test/recettes/12
curl -s http://premier-test-laravel.test/recettes/nouvelle
Recette 12
Recette nouvelle

La phrase « Formulaire de nouvelle recette » n'apparaît jamais. Pour l'adresse /recettes/nouvelle, Laravel essaie d'abord la route /recettes/{id}, déclarée en premier. Les deux chemins comptent deux segments, le premier vaut recettes des deux côtés, et {id} accepte n'importe quel mot, y compris nouvelle. Cette première route correspond donc, sa fonction reçoit "nouvelle" dans $id, et Laravel ne consulte jamais la seconde.

La table de routage parcourue de haut en bas pour GET /recettes/nouvelle, puis la même table dans le bon ordre

php artisan route:list ne vous aide pas à repérer ce problème : la commande trie les routes par ordre alphabétique de leur chemin, et non dans l'ordre où Laravel les essaie.

php artisan route:list --path=recettes
  GET|HEAD       recettes/nouvelle ............................................. routes/web.php:60
  GET|HEAD       recettes/{id} ................................................. routes/web.php:56

                                                                                Showing [2] routes

L'option --path=recettes limite la liste aux routes dont le chemin contient recettes. Pour retrouver l'ordre dans lequel Laravel les essaie, regardez les numéros de ligne : recettes/{id} est déclarée à la ligne 56, donc avant recettes/nouvelle, déclarée à la ligne 60.

Vous pouvez corriger ce problème de deux façons. La première consiste à déclarer d'abord la route dont le chemin ne contient que des segments fixes, /recettes/nouvelle, puis la route à paramètre. Échangez les deux routes dans votre fichier :

Route::get('/recettes/nouvelle', function () {
    return 'Formulaire de nouvelle recette';
});

Route::get('/recettes/{id}', function (string $id) {
    return "Recette $id";
});
Recette 12
Formulaire de nouvelle recette

La seconde façon garde l'ordre d'origine et ajoute une contrainte à {id}. Remettez les deux routes dans l'ordre de départ, avec la contrainte :

Route::get('/recettes/{id}', function (string $id) {
    return "Recette $id";
})->whereNumber('id');

Route::get('/recettes/nouvelle', function () {
    return 'Formulaire de nouvelle recette';
});
Recette 12
Formulaire de nouvelle recette

Avec la contrainte, {id} n'accepte plus que des chiffres. Le mot nouvelle est refusé, la première route ne correspond pas, et Laravel essaie la suivante, qui affiche le formulaire. Une adresse comme /recettes/abc, qui ne correspond plus à aucune des deux routes, reçoit désormais un 404.

Déclarez les routes fixes avant les routes à paramètre qui commencent de la même façon. Laravel applique lui-même cette règle. Au chapitre Controllers, vous verrez Route::resource(), une instruction qui déclare d'un coup toutes les routes d'une ressource. Elle déclare /recettes/create avant /recettes/{recette}, précisément pour éviter ce piège.

6. Les verbes HTTP

Le chemin d'une requête désigne ce dont on parle, par exemple les livres avec /livres. On appelle cela une ressource. Le verbe indique ce qu'on veut en faire : lire, créer, modifier ou supprimer. Ces quatre opérations de base portent un nom, CRUD, formé des initiales anglaises Create, Read, Update et Delete.

Verbe Usage Action CRUD
GET lire une page ou des données Read, lire
POST créer une ressource, envoyer un formulaire Create, créer
PUT ou PATCH modifier une ressource existante Update, modifier
DELETE supprimer une ressource Delete, supprimer

Quand vous tapez une adresse dans le navigateur, il envoie toujours une requête GET. Pour qu'il envoie une requête POST, il faut un formulaire HTML, et vous en écrirez un au chapitre Views. En attendant, l'option -X POST demande à curl d'envoyer une requête POST. Essayez-la sur la route POST /livres déclarée à la section 3 :

curl -s -X POST http://premier-test-laravel.test/livres
<!DOCTYPE html>
…
        <title>Page Expired</title>
…

Au lieu de « Livre enregistré », Laravel répond par le code 419 et une page intitulée Page Expired. La requête a été refusée avant d'atteindre la fonction. Pour toute requête POST, PUT, PATCH ou DELETE, Laravel vérifie d'abord qu'elle a été envoyée depuis une page de votre propre site.

Cette vérification protège contre la CSRF, la falsification de requête entre sites. Imaginez une page piégée, sur un autre site, qui contient un formulaire caché dirigé vers /livres de votre application. Si l'un de vos utilisateurs ouvre cette page, son navigateur envoie le formulaire à votre site sans qu'il s'en rende compte. La requête de curl ne vient, elle non plus, d'aucune page de votre site, et Laravel la refuse de la même façon.

Pour reconnaître une requête venue de votre site, Laravel lit d'abord l'en-tête Sec-Fetch-Site. S'il vaut same-origin, la requête vient d'une page du même site, et Laravel l'accepte. Les navigateurs n'envoient cet en-tête que sur une connexion HTTPS, et votre projet est servi en HTTP. Dans vos formulaires HTML, vous ajouterez donc @csrf, une instruction qui insère un jeton secret que Laravel vérifie. Vous la verrez au chapitre Views. Sans elle, le formulaire reçoit la même réponse 419.

Pour tester une route POST avec curl, il suffit d'ajouter cet en-tête vous-même, avec l'option -H :

curl -s -X POST -H "Sec-Fetch-Site: same-origin" http://premier-test-laravel.test/livres
Livre enregistré

Que se passe-t-il dans l'autre sens, quand une requête GET arrive sur une route qui n'accepte que POST ? Laravel répond 405, comme pour DELETE /livres à la section 3. Pour le vérifier, ajoutez cette route à la fin du fichier :

Route::post('/contact', function () {
    return 'Message reçu';
});

Ouvrez ensuite http://premier-test-laravel.test/contact dans le navigateur, ou demandez la ligne de statut avec curl :

curl -s -i http://premier-test-laravel.test/contact
HTTP/1.1 405 Method Not Allowed
…

Dans le navigateur, la page d'erreur affiche ce message :

The GET method is not supported for route contact. Supported methods: POST.

7. Les autres méthodes de Route

Route::view : une page sans logique

Route::view relie un chemin directement à une vue, sans fonction anonyme ni contrôleur. Elle convient aux pages dont le contenu ne change jamais et qui n'ont besoin d'aucune donnée, comme une page « À propos ». Créez le fichier resources/views/about.blade.php avec ce contenu :

<h1>À propos</h1>
<p>La bibliothèque est ouverte du lundi au samedi.</p>

Ajoutez ensuite la route à la fin de routes/web.php :

Route::view('/a-propos', 'about');
curl -s http://premier-test-laravel.test/a-propos
<h1>À propos</h1>
<p>La bibliothèque est ouverte du lundi au samedi.</p>

Le premier argument de Route::view est le chemin de l'adresse, le second est le nom de la vue, écrit sans .blade.php, exactement comme vous l'écririez dans view('about').

Route::redirect : envoyer ailleurs

Une redirection est une réponse qui demande au navigateur d'ouvrir une autre adresse, donnée dans l'en-tête Location. Le navigateur envoie alors une seconde requête, vers cette adresse. Une redirection sert par exemple quand une page a changé d'adresse : les anciens liens continuent de fonctionner. Laravel propose deux méthodes pour cela, Route::redirect et Route::permanentRedirect. Ajoutez ces deux routes à la fin du fichier :

Route::redirect('/qui-sommes-nous', '/a-propos');

Route::permanentRedirect('/presentation', '/a-propos');
curl -s -I http://premier-test-laravel.test/qui-sommes-nous
curl -s -I http://premier-test-laravel.test/presentation
HTTP/1.1 302 Found
…
Location: /a-propos
…
HTTP/1.1 301 Moved Permanently
…
Location: /a-propos
…

L'option -I, en majuscule, n'affiche que la ligne de statut et les en-têtes, sans le contenu. La ligne Location donne l'adresse vers laquelle le navigateur est renvoyé.

Route::redirect répond par le code 302, qui annonce une redirection temporaire : l'ancienne adresse pourra resservir plus tard. Route::permanentRedirect répond par le code 301, qui annonce que la page a changé d'adresse pour de bon. Les moteurs de recherche remplacent alors l'ancienne adresse par la nouvelle dans leurs résultats.

Route::match et Route::any

Route::match accepte plusieurs verbes pour un même chemin, et Route::any les accepte tous. Ajoutez ces deux routes à la fin du fichier :

Route::match(['get', 'post'], '/recherche', function () {
    return 'Résultats de la recherche';
});

Route::any('/tout', function () {
    return 'Tous les verbes';
});
curl -s http://premier-test-laravel.test/recherche
curl -s -X POST -H "Sec-Fetch-Site: same-origin" http://premier-test-laravel.test/recherche
curl -s -X DELETE -H "Sec-Fetch-Site: same-origin" http://premier-test-laravel.test/tout
Résultats de la recherche
Résultats de la recherche
Tous les verbes

Une requête PUT vers /recherche reçoit un 405, car PUT ne figure pas dans la liste donnée à Route::match. Vous rencontrerez rarement ces deux méthodes. Avec une route par verbe, vous voyez en lisant routes/web.php ce que chaque requête déclenche.

Il existe aussi Route::resource(), qui déclare en une ligne les sept routes nécessaires pour afficher, créer, modifier et supprimer des livres, par exemple. Chacune de ces routes appelle une méthode différente du même contrôleur. Vous la découvrirez donc au chapitre Controllers, une fois ce contrôleur écrit.

Route::fallback : la route de repli

Une route de repli répond aux requêtes GET qu'aucune autre route n'a retenues. Elle remplace la page 404 de Laravel par la vôtre. Ajoutez-la à la fin du fichier :

Route::fallback(function () {
    return response("Cette page n'existe pas.", 404);
});
curl -s -i http://premier-test-laravel.test/nulle-part
HTTP/1.1 404 Not Found
…
Cette page n'existe pas.

La fonction response() fabrique une réponse à partir de deux éléments : le contenu, puis le code. Le second argument, 404, est important. Sans lui, Laravel enverrait le code 200, et un navigateur ou un moteur de recherche croirait que l'adresse /nulle-part existe vraiment.

Laravel essaie toujours la route de repli en dernier, même si vous la déclarez en haut de routes/web.php. Elle modifie aussi une réponse vue plus haut. Une requête GET vers /contact, qui n'accepte que POST, ne reçoit plus le 405, mais la page de repli avec un 404 :

curl -s -i http://premier-test-laravel.test/contact
HTTP/1.1 404 Not Found
…
Cette page n'existe pas.

Retirez cette route de repli avant de continuer : elle masquerait les 404 et les 405 des exercices.

8. Les routes nommées

Les pages d'un site contiennent des liens vers d'autres pages, et certaines actions renvoient le visiteur vers une autre adresse avec une redirection. Vous pouvez écrire ces adresses à la main, comme /livres/12. Laravel propose une autre façon de faire : vous donnez un nom à la route, puis vous lui demandez l'adresse qui correspond à ce nom.

Nommer une route

Pour nommer une route, ajoutez ->name() à la fin de sa déclaration, juste avant le point-virgule. Remplacez les routes GET /livres et GET /livres/{id} de la section 3 par celles-ci :

Route::get('/livres', function () {
    return 'Liste des livres';
})->name('books.index');

Route::post('/livres', function () {
    return 'Livre enregistré';
});

Route::get('/livres/{id}', function (string $id) {
    return "Fiche du livre $id";
})->name('books.show');

La route POST /livres, qui les sépare, ne change pas. Une route nommée est une route qui a reçu un nom avec ->name(), ici books.index pour la liste des livres et books.show pour la fiche d'un livre. route:list affiche ce nom avant l'action :

php artisan route:list --path=livres
  GET|HEAD   livres .............................................. books.index › routes/web.php:16
  POST       livres ............................................................ routes/web.php:20
  GET|HEAD   livres/{id} .......................................... books.show › routes/web.php:24

                                                                                Showing [3] routes

La route POST /livres n'a pas reçu de nom, et sa ligne n'indique que le fichier et le numéro de ligne.

Obtenir l'adresse à partir du nom

La fonction route() reçoit le nom d'une route et la valeur de ses paramètres, puis renvoie l'adresse complète. Ainsi, route('books.show', 12) renvoie l'adresse de la fiche du livre 12. Vous pouvez appeler cette fonction depuis le terminal, dans une vue ou dans l'action d'une route.

Dans le terminal. Tinker, que vous avez utilisé au chapitre MVC, exécute une instruction PHP dans votre projet :

php artisan tinker --execute="echo route('books.show', 12);"
http://premier-test-laravel.test/livres/12

Dans le terminal, aucun navigateur n'envoie de requête. Laravel prend donc le début de l'adresse dans la variable APP_URL du fichier .env. Si l'adresse affichée ne commence pas par http://premier-test-laravel.test, écrivez APP_URL=http://premier-test-laravel.test dans .env et relancez la commande.

Dans une vue Blade. Créez le dossier resources/views/books, puis, dans ce dossier, le fichier links.blade.php. Ce template contient deux liens vers la fiche du livre 12. Le premier donne l'adresse écrite à la main, le second la demande à route().

<h1>Le livre 12</h1>
<ul>
    <li><a href="/livres/12">Lien écrit à la main</a></li>
    <li><a href="{{ route('books.show', 12) }}">Lien écrit avec route()</a></li>
</ul>

Les doubles accolades insèrent dans le HTML le résultat de l'expression PHP qu'elles contiennent, comme {{ $post->title }} dans la vue du chapitre MVC. Ajoutez ensuite cette route à la fin de routes/web.php. Le nom de vue books.links désigne le fichier links.blade.php du dossier books, de la même façon que posts.index au chapitre MVC.

Route::view('/liens', 'books.links');
curl -s http://premier-test-laravel.test/liens
<h1>Le livre 12</h1>
<ul>
    <li><a href="/livres/12">Lien écrit à la main</a></li>
    <li><a href="http://premier-test-laravel.test/livres/12">Lien écrit avec route()</a></li>
</ul>

Les deux liens mènent à la même page. Le second contient l'adresse complète, car route() reprend le début de l'adresse de la requête reçue.

Dans l'action d'une route. Imaginez une page « dernier livre arrivé » qui renvoie le visiteur vers la fiche de ce livre, ici le livre 12. Ajoutez cette route à la fin du fichier :

Route::get('/dernier-livre', function () {
    return redirect()->route('books.show', 12);
});

Cette route n'a pas de nom. Sa fonction utilise le nom d'une autre route, books.show, pour construire l'adresse de la redirection. redirect()->route('books.show', 12) demande à route() l'adresse du livre 12, puis renvoie une réponse 302 qui contient cette adresse dans l'en-tête Location. La fonction to_route('books.show', 12) fait exactement la même chose, en plus court.

curl -s -I http://premier-test-laravel.test/dernier-livre
HTTP/1.1 302 Found
…
Location: http://premier-test-laravel.test/livres/12
…

À quoi sert le nom

Supposez que le site range désormais ses fiches à l'adresse /catalogue/livres/12. Dans routes/web.php, modifiez seulement le chemin de la route books.show :

Route::get('/catalogue/livres/{id}', function (string $id) {
    return "Fiche du livre $id";
})->name('books.show');

Ne touchez ni au nom de la route, ni au template links.blade.php, ni à la route /dernier-livre. Demandez de nouveau la page /liens :

curl -s http://premier-test-laravel.test/liens
<h1>Le livre 12</h1>
<ul>
    <li><a href="/livres/12">Lien écrit à la main</a></li>
    <li><a href="http://premier-test-laravel.test/catalogue/livres/12">Lien écrit avec route()</a></li>
</ul>

Le lien écrit avec route() pointe maintenant vers /catalogue/livres/12. Le lien écrit à la main pointe toujours vers /livres/12, une adresse qu'aucune route ne déclare plus :

curl -s -i http://premier-test-laravel.test/livres/12
HTTP/1.1 404 Not Found
…

La redirection de /dernier-livre a suivi, elle aussi :

curl -s -I http://premier-test-laravel.test/dernier-livre
HTTP/1.1 302 Found
…
Location: http://premier-test-laravel.test/catalogue/livres/12
…

Dans un vrai site, l'adresse de la fiche d'un livre figure dans beaucoup de vues et de contrôleurs. Si ces fichiers écrivent /livres/12 à la main, vous devez les ouvrir un par un pour corriger chaque lien, et un lien oublié envoie le visiteur sur une page 404. S'ils utilisent le nom books.show, seule la déclaration de la route change dans routes/web.php. Écrivez les liens et les redirections avec route() et le nom de la route.

Par convention, le nom d'une route réunit le nom de la ressource et celui de l'action, séparés par un point : books.index pour la liste des livres, books.show pour la fiche d'un livre. Remettez ensuite le chemin '/livres/{id}'.

À retenir

  • Laravel compare chaque requête aux routes de routes/web.php de haut en bas, et la première qui correspond répond.
  • Une adresse qu'aucune route ne déclare reçoit un 404. Une adresse déclarée, mais pour un autre verbe, reçoit un 405.
  • Un paramètre {id} arrive dans l'action sous forme de chaîne, et une contrainte comme whereNumber limite les valeurs qu'il accepte.
  • Une route fixe se déclare avant une route à paramètre qui commence de la même façon.
  • Une route qui interroge un modèle ou prépare une vue avec des données appelle un contrôleur.
  • route() produit l'adresse d'une route à partir de son nom, dans une vue comme dans une redirection. Quand le chemin change, ces adresses suivent, alors qu'un lien écrit à la main mène à un 404.

Exercices

Comptez 45 minutes pour les exercices, en séance ou à la maison.

Faites les exercices 1 à 9 dans routes/web.php de premier-test-laravel, sans contrôleur, en ajoutant chaque route à la fin du fichier. Après chaque route, vérifiez l'adresse dans le navigateur ou avec curl.

1. Une première route

Écrivez une route GET /hello qui affiche « Hello World ! Ceci est ma première route Laravel. ». La route /ping de la section 2 sert de modèle.

2. Un paramètre

Écrivez une route /profil/{firstName} qui affiche « Bienvenue sur votre profil, Alice ! » pour l'adresse /profil/Alice. Pour un autre prénom, la phrase reprend celui qui figure dans l'adresse.

curl -s http://premier-test-laravel.test/profil/Alice
curl -s http://premier-test-laravel.test/profil/Bob
Bienvenue sur votre profil, Alice !
Bienvenue sur votre profil, Bob !

3. Deux paramètres

Écrivez une route /calcul/{a}/{b} qui affiche « Le résultat est : 10 » pour /calcul/4/6. /calcul/4/six doit répondre 404. Inspirez-vous de l'agenda de la section 4.

4. Un paramètre optionnel

Écrivez une route /bienvenue/{lang?} :

  • sans langue, elle affiche « Bienvenue ! »,
  • avec en, elle affiche « Welcome! »,
  • avec es, elle affiche « ¡Bienvenido! »,
  • avec toute autre langue, elle affiche « Langue non supportée. ».

Les horaires de la section 4 utilisent le même mécanisme. Vérifiez les quatre cas :

curl -s http://premier-test-laravel.test/bienvenue
curl -s http://premier-test-laravel.test/bienvenue/en
curl -s http://premier-test-laravel.test/bienvenue/es
curl -s http://premier-test-laravel.test/bienvenue/de

5. Une route POST

Écrivez une route POST /formulaire qui renvoie « Formulaire envoyé avec succès ! ». Lancez ces deux commandes, puis ouvrez http://premier-test-laravel.test/formulaire dans le navigateur. Notez le code obtenu dans chacun des trois cas.

curl -s -i -X POST -H "Sec-Fetch-Site: same-origin" http://premier-test-laravel.test/formulaire
curl -s -i -X POST http://premier-test-laravel.test/formulaire

6. Une redirection

Écrivez une route /ici qui renvoie le navigateur vers /hello, la route de l'exercice 1. Vérifiez que la réponse porte le code 302 et l'en-tête Location: /hello :

curl -s -I http://premier-test-laravel.test/ici

7. L'ordre des routes

Une boutique déclare ces deux routes, dans cet ordre :

Route::get('/commandes/{number}', function (string $number) {
    return "Commande n° $number";
});

Route::get('/commandes/panier', function () {
    return 'Votre panier';
});
  1. Sans rien lancer, écrivez ce qu'affichent /commandes/15 et /commandes/panier.
  2. Ajoutez les deux routes à la fin de votre fichier, puis vérifiez votre prédiction.
  3. Corrigez de deux façons : d'abord en changeant l'ordre, puis en gardant l'ordre et en ajoutant une contrainte. Après chaque correction, /commandes/panier doit afficher « Votre panier ».

8. Des mentions légales

Créez la vue resources/views/legal.blade.php avec un titre <h1>Mentions légales</h1> et une phrase. Affichez-la à l'adresse /mentions-legales avec une seule ligne dans routes/web.php, sans fonction ni contrôleur.

9. Deux routes nommées

Donnez le nom hello à la route de l'exercice 1 et le nom profile à celle de l'exercice 2. Affichez ensuite leurs adresses avec Tinker, comme à la section 8 :

php artisan tinker --execute="echo route('hello');"
php artisan tinker --execute="echo route('profile', 'Alice');"

Remplacez ensuite la redirection /ici de l'exercice 6 par une route GET /ici dont la fonction renvoie le visiteur vers la route nommée hello, comme la route /dernier-livre de la section 8. Changez enfin le chemin /hello en /bonjour, sans toucher au nom, et vérifiez que l'en-tête Location suit ce changement :

curl -s -I http://premier-test-laravel.test/ici

Remettez ensuite le chemin /hello.

10. Quelle route répond ? (sur papier, 10 minutes)

Un musée déclare ces six routes, dans cet ordre :

Route::get('/expositions', function () {
    return 'Toutes les expositions';
});

Route::get('/expositions/{id}', function (string $id) {
    return "Exposition $id";
})->whereNumber('id');

Route::get('/expositions/prochaines', function () {
    return 'Prochaines expositions';
});

Route::post('/reservations', function () {
    return 'Réservation enregistrée';
});

Route::get('/visites/{lang?}', function (string $lang = 'fr') {
    return "Visite guidée en $lang";
});

Route::get('/billets/{price}/{count}', function (string $price, int $count) {
    return "$count billet(s) au tarif $price";
})->whereAlpha('price')->whereNumber('count');

Pour chaque requête GET, écrivez la réponse affichée, ou le code 404 ou 405 :

  1. /expositions
  2. /expositions/7
  3. /expositions/prochaines
  4. /reservations
  5. /visites
  6. /visites/nl
  7. /billets/reduit/3
  8. /billets/3/reduit

11. Six cartes

Deux routes correspondent à la même requête. Laquelle répond ?

La première déclarée dans routes/web.php. Laravel s'arrête à la première qui correspond.

Quelle différence entre un 404 et un 405 ?

404 : aucune route n'a ce chemin. 405 : une route a ce chemin, mais pas pour ce verbe.

Que faut-il ajouter à la fonction pour une route /horaires/{day?} ?

Une valeur par défaut : string $day = 'lundi'. Sans elle, /horaires provoque « Too few arguments ».

/recettes/{id} est déclarée avant /recettes/nouvelle. Comment corriger ?

Déclarer /recettes/nouvelle en premier, ou ajouter ->whereNumber('id') à /recettes/{id}.

Quand utiliser Route::view ?

Pour une page qui affiche une vue sans données, comme « À propos ».

Pourquoi nommer une route ?

Pour fabriquer son adresse avec route('books.show', 12). Si vous changez le chemin, les liens et les redirections écrits avec route() suivent, sans modifier les vues ni les contrôleurs.