Module 2Programmer avec Laravel
Chapitre 3 sur 17
Gestion du routage
Routes, paramètres, routes nommées, groupes, liaison de modèle, repli, CORS, API et cache des routes.
- ≈ 250 min
- 12 leçons
- 3 vidéos
- 4 exercices
Module 2Programmer avec Laravel
Chapitre 3 sur 17
Routes, paramètres, routes nommées, groupes, liaison de modèle, repli, CORS, API et cache des routes.
Objectifs
Avec ce chapitre, tu sauras…
route() ;Leçon 1
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome'); // renvoie une vue
});
Route::get('/bonjour', function () {
return 'Bonjour Kitab !'; // une chaîne devient une réponse HTML
});
Route::get('/api-test', function () {
return ['nom' => 'Kitab', 'livres' => 3]; // un tableau devient du JSON
});Tableau: Les fichiers de routes
| Fichier | Contenu | Middlewares appliqués |
|---|---|---|
routes/web.php | les pages du site, vues dans un navigateur | groupe web : session, cookies chiffrés, protection CSRF, erreurs de validation |
routes/api.php (après php artisan install:api) | une API sans état, appelée par JavaScript ou une application mobile | groupe api ; préfixe /api ajouté automatiquement |
routes/console.php | des commandes Artisan et des tâches planifiées | aucun (pas de requête HTTP) |
| Verbe | Usage habituel | Exemple Kitab |
|---|---|---|
| GET | lire, afficher | GET /livres : le catalogue |
| POST | créer | POST /panier : ajouter un livre au panier |
| PUT / PATCH | modifier | PATCH /admin/livres/12 : changer le prix |
| DELETE | supprimer | DELETE /admin/livres/12 |
Leçon 2
Leçon 3
Leçon 4
| Méthode de groupe | Effet |
|---|---|
prefix('admin') | ajoute admin/ au début de chaque URI |
name('admin.') | ajoute admin. au début de chaque nom (n'oublie pas le point) |
middleware([...]) | applique des middlewares à toutes les routes (chapitre 4) |
controller(X::class) | toutes les routes appellent une méthode du contrôleur X |
domain('{magasin}.kitab.ma') | routes d'un sous-domaine |
Leçon 5
Leçon 6
Leçon 7
Leçon 8
Leçon 9
Leçon 10
Leçon 11
Leçon 12
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
1Une route qui ne correspond à aucune requête provoque l'erreur…
2Route::redirect('/a', '/b') renvoie par défaut le code…
3Un paramètre facultatif s'écrit…
4route('livres.show', ['slug' => 'x', 'page' => 2]) produit…
5Dans un groupe, name('admin.') sert à…
6Avec function (Livre $livre) sur /livres/{livre}, un id inexistant donne…
7Pour envoyer un formulaire HTML vers une route DELETE, on ajoute…
8En Laravel 13, le fichier config/cors.php apparaît avec…
L'essentiel du chapitre
[Controleur::class, 'methode']). Fichiers : routes/web.php (groupe web : session, CSRF), routes/api.php (install:api, préfixe /api), routes/console.php. Chargés par bootstrap/app.php.get, post, put, patch, delete, options, match, any ; raccourcis Route::redirect (302), permanentRedirect (301), Route::view. Vérifier : php artisan route:list.{slug} (obligatoire), {nom?} + valeur par défaut (facultatif) ; contraintes where, whereNumber, whereAlpha, whereIn ; ordre des routes : fixes d'abord.->name('livres.show')) ; URL avec route('livres.show', $param), redirection avec redirect()->route() ou to_route().prefix, name (avec le point), middleware, controller, imbricables.{livre} + Livre $livre, 404 si absent), clé personnalisée {livre:slug} ou getRouteKeyName(), délimitée ; explicite Route::model / Route::bind dans AppServiceProvider::boot() ; énumérations.Route::fallback (à la fin) ; @method('DELETE') + @csrf pour PUT/PATCH/DELETE ; Route::currentRouteName(), request()->routeIs().Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
HandleCors global, php artisan config:publish cors. Cache : route:cache au déploiement, route:clear sinon. throttle:60,1 limite le débit (429).Ces fichiers sont chargés par bootstrap/app.php (bloc withRouting).
Le résumé indique que les fichiers de routes sont chargés par App\Providers\RouteServiceProvider et qu'un projet contient quatre fichiers (web.php, api.php, console.php, channels.php). C'était vrai jusqu'à Laravel 10. Depuis Laravel 11, ce fournisseur n'existe plus, les routes sont déclarées dans bootstrap/app.php, et seuls web.php et console.php existent au départ.
Les méthodes du routeur
Le routeur propose une méthode par verbe HTTP :
Route::get($uri, $action); // lire (afficher une page)
Route::post($uri, $action); // créer (envoyer un formulaire)
Route::put($uri, $action); // remplacer
Route::patch($uri, $action); // modifier en partie
Route::delete($uri, $action); // supprimer
Route::options($uri, $action); // demander les méthodes permises
Route::match(['get', 'post'], '/contact', function () { /* ... */ }); // plusieurs verbes
Route::any('/tout', function () { /* ... */ }); // tous les verbesL'injection de dépendances dans une route
Toute classe indiquée dans la signature d'une route est fournie automatiquement par le conteneur de services de Laravel. L'exemple le plus courant : la requête HTTP courante.
use Illuminate\Http\Request;
Route::get('/recherche', function (Request $request) {
return 'Vous cherchez : ' . $request->query('q', 'rien');
});
// GET /recherche?q=tolstoi → « Vous cherchez : tolstoi »Les redirections et les routes de vue
Route::redirect('/accueil', '/'); // redirection 302 (temporaire)
Route::redirect('/ancien-catalogue', '/livres', 301);
Route::permanentRedirect('/boutique', '/livres'); // redirection 301 (définitive)
Route::view('/a-propos', 'pages.a-propos'); // affiche une vue
Route::view('/contact', 'pages.contact', ['email' => 'contact@kitab.ma']);Route::redirect évite d'écrire une closure pour une simple redirection. Le code 301 dit aux navigateurs et aux moteurs de recherche que l'adresse a changé définitivement ; le code 302 indique un changement temporaire. Route::view affiche directement une vue, avec éventuellement des données, sans contrôleur.
La liste des routes
php artisan route:list # toutes les routes
php artisan route:list -v # avec les middlewares de chaque route
php artisan route:list --path=admin # seulement les URI qui commencent par admin
php artisan route:list --name=livres # seulement les noms qui contiennent livres
php artisan route:list --except-vendor # sans les routes des paquets installés
php artisan route:list --only-vendor # seulement les routes des paquets GET|HEAD / ......................................................
GET|HEAD a-propos ...................................................
GET|HEAD livres ............................. livres.index
GET|HEAD livres/{slug} ....................... livres.show
POST panier ............................. panier.ajouter
GET|HEAD up .....................................................Les paramètres obligatoires
Un segment entre accolades capture une partie de l'URI et la passe à l'action :
Route::get('/livres/{slug}', function (string $slug) {
return "Fiche du livre : $slug";
});
Route::get('/categories/{categorie}/livres/{page}', function (string $categorie, int $page) {
return "Catégorie $categorie, page $page";
});Les paramètres sont transmis dans l'ordre des segments : le nom des variables PHP n'a pas d'importance pour l'ordre. Un nom de paramètre contient des lettres et éventuellement _ ({livre_id}), jamais de tiret.
Si l'action a aussi des dépendances (comme Request), on les place avant les paramètres de route :
Route::get('/livres/{slug}', function (Request $request, string $slug) {
return "$slug demandé depuis " . $request->ip();
});Les paramètres facultatifs
On ajoute ? après le nom et on donne une valeur par défaut à la variable :
Route::get('/auteurs/{nom?}', function (?string $nom = null) {
return $nom ? "Livres de $nom" : 'Tous les auteurs';
});
Route::get('/langue/{code?}', function (string $code = 'fr') {
return "Langue choisie : $code";
});Contraindre un paramètre
La méthode where impose une expression régulière. Si la valeur ne correspond pas, la route ne correspond pas (404) :
Route::get('/commandes/{id}', function (string $id) {
return "Commande n° $id";
})->where('id', '[0-9]+');
Route::get('/auteurs/{id}/{nom}', function (string $id, string $nom) {
return "$id : $nom";
})->whereNumber('id')->whereAlpha('nom');
Route::get('/livres/{slug}', function (string $slug) {
return $slug;
})->where('slug', '[a-z0-9-]+');
Route::get('/livres/langue/{langue}', function (string $langue) {
return $langue;
})->whereIn('langue', ['ar', 'fr', 'en']);Assistants disponibles : whereNumber, whereAlpha, whereAlphaNumeric, whereUuid, whereUlid, whereIn. Pour imposer une contrainte à tous les paramètres d'un même nom, on écrit Route::pattern('id', '[0-9]+'); dans la méthode boot de App\Providers\AppServiceProvider.
Le routeur prend la première route qui correspond. Avec /livres/{slug} déclarée avant /livres/nouveautes, l'URL /livres/nouveautes est capturée par la première route (avec $slug = 'nouveautes'). Déclare toujours les routes fixes avant les routes à paramètre, ou contrains le paramètre.
Une route nommée permet de générer son URL sans écrire l'adresse en dur. Si l'URI change un jour, il suffit de la modifier à un seul endroit.
Route::get('/livres', function () { /* ... */ })->name('livres.index');
Route::get('/livres/{slug}', function (string $slug) { /* ... */ })->name('livres.show');
Route::get('/mon-compte/profil', [ProfilController::class, 'show'])->name('profil');$url = route('livres.index'); // http://localhost:8000/livres
$url = route('livres.show', ['slug' => 'le-petit-prince']); // .../livres/le-petit-prince
$url = route('livres.show', 'le-petit-prince'); // même résultat (un seul paramètre)
$url = route('livres.index', ['page' => 2, 'tri' => 'prix']); // .../livres?page=2&tri=prix
return redirect()->route('livres.index'); // redirection vers une route nommée
return to_route('livres.show', ['slug' => 'le-petit-prince']); // raccourci équivalentLes clés du tableau qui ne correspondent à aucun paramètre de la route sont ajoutées à la chaîne de requête (?page=2&tri=prix).
Dans une vue Blade :
<a href="{{ route('livres.show', $livre->slug) }}">{{ $livre->titre }}</a>La convention est ressource.action : livres.index, livres.show, livres.store, admin.livres.edit. Ce sont aussi les noms que génèrent les routes de ressources (chapitre 6). Toute route utilisée dans un lien ou une redirection doit avoir un nom.
Un groupe partage des attributs (préfixe d'URI, préfixe de nom, middlewares, contrôleur) entre plusieurs routes, sans les répéter.
use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\CommandeController;
Route::prefix('admin') // URI : /admin/...
->name('admin.') // noms : admin....
->middleware(['auth']) // il faut être connecté
->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); // /admin → admin.dashboard
Route::get('/stats', [DashboardController::class, 'stats'])->name('stats'); // /admin/stats → admin.stats
});
// Plusieurs routes vers le même contrôleur
Route::controller(CommandeController::class)->group(function () {
Route::get('/commandes', 'index')->name('commandes.index');
Route::get('/commandes/{id}', 'show')->name('commandes.show');
Route::post('/commandes', 'store')->name('commandes.store');
});Le plus souvent, un paramètre de route sert à retrouver un enregistrement en base. Laravel peut le faire à ta place.
La liaison implicite
Si le nom de la variable typée par un modèle Eloquent est identique au nom du segment, Laravel cherche automatiquement l'enregistrement dont la clé primaire vaut la valeur de l'URI. S'il n'existe pas, il renvoie une 404.
use App\Models\Livre;
Route::get('/livres/{livre}', function (Livre $livre) {
return $livre->titre; // GET /livres/12 : le livre dont l'id vaut 12
});Pour chercher sur une autre colonne que l'id, on l'indique dans le segment :
Route::get('/livres/{livre:slug}', function (Livre $livre) {
return $livre->titre; // GET /livres/le-petit-prince : WHERE slug = 'le-petit-prince'
});Pour que toutes les routes d'un modèle utilisent cette colonne, on redéfinit getRouteKeyName() dans le modèle :
public function getRouteKeyName(): string
{
return 'slug';
}Avec deux modèles liés, Laravel peut vérifier que le second appartient au premier (liaison délimitée, scoped) :
use App\Models\Categorie;
Route::get('/categories/{categorie}/livres/{livre:slug}', function (Categorie $categorie, Livre $livre) {
return $livre->titre; // 404 si le livre n'appartient pas à cette catégorie
});Quand une clé personnalisée est utilisée sur le paramètre enfant ({livre:slug}), Laravel délimite automatiquement : il cherche le livre parmi les livres de la catégorie (relation livres() du modèle Categorie, chapitre 11). Sans clé personnalisée, on ajoute ->scopeBindings().
La liaison explicite
On peut déclarer soi-même la liaison dans la méthode boot de App\Providers\AppServiceProvider :
use App\Models\Livre;
use Illuminate\Support\Facades\Route;
public function boot(): void
{
Route::model('ouvrage', Livre::class); // {ouvrage} → un Livre par son id
Route::bind('isbn', function (string $valeur) { // logique de recherche personnalisée
return Livre::where('isbn', $valeur)->firstOrFail();
});
}Route::get('/ouvrages/{ouvrage}', fn (Livre $ouvrage) => $ouvrage->titre);
Route::get('/isbn/{isbn}', fn (Livre $isbn) => $isbn->titre);La liaison d'une énumération
Une énumération PHP « adossée » (backed enum) peut typer un paramètre : la route ne s'exécute que si la valeur existe dans l'énumération, sinon 404.
use App\Enums\Langue; // enum Langue: string { case Arabe = 'ar'; case Francais = 'fr'; case Anglais = 'en'; }
Route::get('/livres/langue/{langue}', function (Langue $langue) {
return "Livres en {$langue->value}";
});Le résumé utilise le modèle App\User : depuis Laravel 8, les modèles sont dans App\Models (App\Models\User). Il place la liaison explicite dans RouteServiceProvider::boot() avec parent::boot() : depuis Laravel 11, on l'écrit dans AppServiceProvider::boot(), sans parent::boot().
Route::fallback définit la route exécutée quand aucune autre ne correspond. Elle se place à la fin de routes/web.php. Elle bénéficie des middlewares du groupe web (session, utilisateur connecté), ce qui permet d'afficher une page 404 personnalisée avec le menu du site.
Route::fallback(function () {
return response()->view('erreurs.introuvable', [], 404);
});Pour une page 404 simple, il suffit aussi de créer la vue resources/views/errors/404.blade.php : Laravel l'utilise automatiquement pour toute erreur 404.
Un formulaire HTML ne connaît que GET et POST. Pour appeler une route PUT, PATCH ou DELETE, on envoie le formulaire en POST avec un champ caché _method :
<form action="{{ route('admin.livres.destroy', $livre) }}" method="POST">
@csrf
@method('DELETE') {{-- génère <input type="hidden" name="_method" value="DELETE"> --}}
<button type="submit">Supprimer</button>
</form>@csrf ajoute le jeton de protection (chapitre 5) ; @method('DELETE') ajoute le champ _method. Laravel lit ce champ et traite la requête comme un DELETE.
use Illuminate\Support\Facades\Route;
$route = Route::current(); // l'objet Illuminate\Routing\Route
$nom = Route::currentRouteName(); // "livres.show"
$action = Route::currentRouteAction(); // "App\Http\Controllers\LivreController@show"Dans une vue, on teste souvent le nom de la route pour mettre en valeur le lien actif du menu :
<a href="{{ route('livres.index') }}" @class(['actif' => request()->routeIs('livres.*')])>Catalogue</a>Par sécurité, un navigateur interdit à un script JavaScript chargé depuis une origine (protocole + domaine + port, par exemple http://localhost:5173) de lire la réponse d'une requête vers une autre origine (http://localhost:8000). Le CORS (Cross-Origin Resource Sharing) est le mécanisme par lequel le serveur autorise certaines origines, grâce à des en-têtes comme Access-Control-Allow-Origin. Avant certaines requêtes, le navigateur envoie une requête de vérification OPTIONS (preflight).
Laravel répond automatiquement aux requêtes CORS grâce au middleware HandleCors, inclus dans la pile de middlewares globale. Pour changer la configuration, on publie le fichier :
php artisan config:publish corsreturn [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:5173', 'https://app.kitab.ma'],
'allowed_headers' => ['*'],
'supports_credentials' => false,
];Le CORS ne concerne que les appels JavaScript entre origines différentes : une application React ou Vue séparée, ou une application mobile hybride. Un site Blade classique, servi par Laravel lui-même, n'en a pas besoin.
Le résumé indique que le middleware HandleCors se trouve dans App\Http\Kernel et que config/cors.php existe par défaut. Depuis Laravel 11, il n'y a plus de Kernel.php, et config/cors.php n'est créé que par php artisan config:publish cors.
php artisan install:apiCette commande crée routes/api.php, installe Laravel Sanctum (authentification par jeton, chapitre 12) et déclare le fichier dans bootstrap/app.php. Les routes de ce fichier reçoivent automatiquement le préfixe /api et le groupe de middlewares api (sans session ni CSRF).
use App\Models\Livre;
use Illuminate\Support\Facades\Route;
Route::get('/livres', function () {
return Livre::select('id', 'titre', 'prix')->get(); // GET /api/livres → JSON
});Le middleware throttle limite le nombre de requêtes : ->middleware('throttle:60,1') autorise 60 requêtes par minute pour un même utilisateur (ou une même adresse IP). Au-delà, Laravel répond 429 Too Many Requests. On peut aussi définir des limiteurs nommés avec RateLimiter::for() dans AppServiceProvider (chapitre 12).
En production, Laravel peut compiler toutes les routes dans un seul fichier, ce qui accélère l'enregistrement des routes à chaque requête :
php artisan route:cache # génère le cache (bootstrap/cache/routes-v7.php)
php artisan route:clear # le supprimeUne fois le cache créé, Laravel n'utilise plus routes/web.php : une nouvelle route semble « ne pas exister ». On ne lance donc route:cache qu'au déploiement. Si une route reste introuvable en local, lance php artisan route:clear.
La première route et la première vue.
Dans le projet Kitab, sans base de données pour l'instant : (1) déclare en haut de routes/web.php un tableau $livres de trois livres (slug, titre, auteur, prix) ; (2) crée GET /livres (livres.index) qui renvoie le tableau en JSON ; (3) crée GET /livres/{slug} (livres.show) qui renvoie le livre correspondant ou une erreur 404 avec abort(404) ; (4) crée un groupe admin (préfixe d'URI et de nom) avec GET /admin (admin.dashboard) ; (5) crée la redirection /boutique → /livres ; (6) vérifie le tout avec php artisan route:list.
Écris dans routes/web.php les routes (avec des closures qui renvoient un texte) : (1) GET / nommée accueil ; (2) GET /livres nommée livres.index, qui affiche « Page 1 » ou « Page N » selon le paramètre de requête page ; (3) GET /livres/{slug} nommée livres.show, où le slug ne contient que des minuscules, des chiffres et des tirets ; (4) GET /auteurs/{nom?} ; (5) une redirection définitive de /catalogue vers /livres ; (6) GET /a-propos qui affiche la vue pages.a-propos ; (7) une route de repli qui renvoie « Page introuvable » avec le code 404.
Les groupes peuvent s'imbriquer : un groupe admin peut contenir un sous-groupe prefix('catalogue').
L'exemple de groupe du résumé utilise une syntaxe de Laravel 7 : la clé 'namespace' => 'Admin' et les actions écrites en chaîne ('IndexController@index'). Depuis Laravel 8, le préfixe d'espace de noms n'est plus appliqué : on importe la classe avec use et on écrit [IndexController::class, 'index']. Cette écriture est vérifiée par l'éditeur et fonctionne avec « Aller à la définition ».
Passer des données aux vues et capturer des paramètres de route (wildcards).
Six astuces de routage : liaison de modèle, Route::view, contrôleurs, routes de ressources.
Chaque extrait contient une erreur. Trouve-la et corrige-la. (1) Route::get('/livres/{slug}', fn ($slug) => $slug); Route::get('/livres/nouveautes', fn () => 'Nouveautés'); : la page des nouveautés n'est jamais affichée. (2) Route::get('/user/{id}', function (Request $request) { return $id; }); (3) Route::prefix('admin')->name('admin')->group(function () { Route::get('/stats', ...)->name('stats'); }); : route('admin.stats') provoque une erreur. (4) Route::get('/auteurs/{nom?}', function (string $nom) { return $nom; }); : /auteurs provoque une erreur.
Voici un extrait de php artisan route:list -v :
GET|HEAD admin/livres ............ admin.livres.index › Admin\LivreController@index
⇂ web
⇂ auth
DELETE admin/livres/{livre} .... admin.livres.destroy › Admin\LivreController@destroy
⇂ web
⇂ auth
GET|HEAD livres/{livre:slug} ..... livres.show › CatalogueController@show
⇂ web(1) Quelle URL affiche la liste de l'administration, et quelle méthode s'exécute ? (2) Un visiteur non connecté peut-il supprimer un livre ? Pourquoi ? (3) Écris le lien Blade vers la fiche du livre $livre. (4) Écris le formulaire Blade qui supprime $livre.