Module 2Programmer avec Laravel
Chapitre 4 sur 17
Utilisation des middlewares
Créer, enregistrer et paramétrer des middlewares dans bootstrap/app.php, groupes, alias et terminate.
- ≈ 190 min
- 8 leçons
- 1 vidéo
- 4 exercices
Module 2Programmer avec Laravel
Chapitre 4 sur 17
Créer, enregistrer et paramétrer des middlewares dans bootstrap/app.php, groupes, alias et terminate.
Objectifs
Avec ce chapitre, tu sauras…
handle ;terminate.Leçon 1
On peut se représenter les middlewares comme des couches d'oignon : la requête traverse chaque couche pour atteindre le contrôleur, puis la réponse retraverse les couches dans l'autre sens.
requête ─►┌────────────── EncryptCookies ──────────────┐
│ ┌──────────── StartSession ─────────────┐ │
│ │ ┌───────── PreventRequestForgery ───┐ │ │
│ │ │ ┌──────── auth ──────────────────┐ │ │ │
│ │ │ │ Contrôleur → réponse │ │ │ │
│ │ │ └────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
réponse ◄─└─────────────────────────────────────────────┘Usages typiques : vérifier l'authentification ou un rôle, limiter le débit, journaliser les requêtes, forcer la langue, ajouter des en-têtes de sécurité, mettre le site en maintenance.
Leçon 2
php artisan make:middleware EnsureUserIsAdminLa commande crée le fichier app/Http/Middleware/EnsureUserIsAdmin.php (le dossier est créé s'il n'existe pas) :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user()?->role !== 'admin') {
abort(403, 'Accès réservé aux administrateurs.');
}
return $next($request); // laisse passer la requête vers la couche suivante
}
}La méthode handle reçoit la requête et $next, une fonction qui représente « la suite » (les couches suivantes puis le contrôleur). Deux possibilités :
redirect(...), abort(403)) sans appeler $next ;$next($request).Middleware avant et après
Le moment où le middleware agit dépend de l'endroit où on appelle $next :
class BeforeMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// action AVANT le contrôleur (vérifier, modifier la requête)
return $next($request);
}
}
class AfterMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request); // le contrôleur s'exécute ici
// action APRÈS le contrôleur (modifier la réponse)
return $response;
}
}<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AddSecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'SAMEORIGIN'); // contre le clickjacking
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
return $response;
}
}Leçon 3
Un middleware créé n'est actif nulle part tant qu'on ne l'a pas enregistré. Depuis Laravel 11, tout se passe dans bootstrap/app.php, dans le bloc withMiddleware.
Middleware global
Un middleware global s'exécute à chaque requête de l'application :
use App\Http\Middleware\AddSecurityHeaders;
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware): void {
$middleware->append(AddSecurityHeaders::class); // à la fin de la pile globale
// $middleware->prepend(...); // au début de la pile globale
})La pile globale par défaut contient notamment TrustProxies, HandleCors, PreventRequestsDuringMaintenance, ValidatePostSize, TrimStrings (supprime les espaces au début et à la fin des champs) et ConvertEmptyStringsToNull (transforme un champ vide en null).
Middleware de route
On l'affecte à une route (ou à un groupe) avec la méthode middleware, en donnant le nom de la classe :
use App\Http\Middleware\EnsureUserIsAdmin;
Route::get('/admin', function () {
return 'Administration';
})->middleware(EnsureUserIsAdmin::class);
Route::get('/admin/stats', function () {
return 'Statistiques';
})->middleware(['auth', EnsureUserIsAdmin::class]); // plusieurs middlewares, dans l'ordreLes alias
Un alias est un nom court pour un middleware. On le déclare dans bootstrap/app.php :
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
]);
})Route::get('/admin', fn () => 'Administration')->middleware(['auth', 'admin']);Laravel fournit déjà ces alias :
| Alias | Rôle |
|---|---|
auth | l'utilisateur doit être connecté ; sinon, redirection vers la route login |
guest | l'utilisateur doit être non connecté (pages de connexion et d'inscription) |
verified | l'adresse e-mail doit être vérifiée |
can | l'utilisateur doit avoir une autorisation (can:update,livre, chapitre 12) |
throttle | limite le débit (throttle:60,1) |
signed | l'URL doit avoir une signature valide (liens de confirmation) |
password.confirm | demande de retaper le mot de passe avant une action sensible |
auth.basic | authentification HTTP « basique » |
cache.headers | ajoute des en-têtes de cache (cache.headers:public;max_age=3600) |
Leçon 4
Groupe web (routes/web.php) | Groupe api (routes/api.php) |
|---|---|
EncryptCookies (chiffre les cookies) | SubstituteBindings (liaison de modèle) |
AddQueuedCookiesToResponse | |
StartSession (démarre la session) | |
ShareErrorsFromSession (rend $errors disponible dans les vues) | |
PreventRequestForgery (protection CSRF, chapitre 5) | |
Leçon 5
Leçon 6
Leçon 7
Leçon 8
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
1Pour laisser passer une requête, un middleware renvoie…
2Un middleware qui modifie la réponse appelle $next …
3En Laravel 13, un middleware global se déclare…
4L'alias guest exige que l'utilisateur soit…
5Le groupe web contient la protection CSRF, qui s'appelle en Laravel 13…
6->middleware('role:admin,libraire') transmet au middleware…
7withoutMiddleware peut retirer…
8La méthode terminate s'exécute…
L'essentiel du chapitre
php artisan make:middleware Nom (app/Http/Middleware).handle(Request $request, Closure $next): Response : rejeter (renvoyer une réponse, abort(403)) ou laisser passer (return $next($request)). Avant : agir puis $next ; après : $response = $next($request), agir, renvoyer $response.bootstrap/app.php (withMiddleware) : global append / prepend ; alias alias([...]) ; groupes appendToGroup, web(append:), api(prepend:) ; priorité priority, appendToPriorityList. Sur une route : ->middleware(Classe::class) ou ->middleware('alias') ; exclure : withoutMiddleware (pas les globaux).auth, guest, verified, can, throttle, signed, password.confirm, cache.headers. Groupe web : cookies chiffrés, session, erreurs, PreventRequestForgery (CSRF), liaisons.$next (string ...$roles) ; sur la route role:admin,libraire.terminate(Request, Response) : après l'envoi de la réponse (FastCGI) ; singleton pour garder la même instance.SubstituteBindings (liaison de modèle) |
app/Http/Kernel.php, qui n'existe plus.Exclure un middleware
Dans un groupe protégé, withoutMiddleware retire un middleware pour une route précise :
use App\Http\Middleware\EnsureUserIsAdmin;
Route::middleware([EnsureUserIsAdmin::class])->group(function () {
Route::get('/admin', fn () => 'Tableau de bord');
Route::get('/admin/aide', fn () => 'Aide')
->withoutMiddleware([EnsureUserIsAdmin::class]); // page d'aide accessible à tous
});withoutMiddleware ne retire que des middlewares de route, jamais un middleware global.
Un groupe de middlewares réunit plusieurs middlewares sous une seule clé. Laravel en définit deux, appliqués automatiquement aux fichiers de routes :
Un middleware peut recevoir des paramètres supplémentaires, placés après $next dans la signature de handle. Exemple : un seul middleware qui vérifie n'importe quel rôle.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserHasRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
$user = $request->user();
if (! $user) {
return redirect()->route('login');
}
if (! in_array($user->role, $roles, true)) {
abort(403);
}
return $next($request);
}
}Sur la route, on sépare le nom du middleware et les paramètres par :, et plusieurs paramètres par des virgules :
use App\Http\Middleware\EnsureUserHasRole;
Route::get('/admin/livres', fn () => 'Gestion des livres')
->middleware(EnsureUserHasRole::class . ':admin,libraire'); // admin OU libraire
// avec un alias 'role' déclaré dans bootstrap/app.php :
Route::get('/admin/utilisateurs', fn () => 'Gestion des comptes')
->middleware('role:admin');Avec string ...$roles (paramètre variadique), le middleware reçoit tous les paramètres dans un tableau : ['admin', 'libraire'].
Parfois, un middleware doit travailler après l'envoi de la réponse au navigateur (journaliser, envoyer des statistiques), pour ne pas ralentir l'utilisateur. On lui ajoute une méthode terminate, appelée automatiquement après l'envoi de la réponse quand le serveur utilise FastCGI (PHP-FPM) :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class LogVisite
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
Log::info('Visite', [
'url' => $request->fullUrl(),
'statut' => $response->getStatusCode(),
'duree_ms' => round((microtime(true) - LARAVEL_START) * 1000),
]);
}
}Pour être appelé, le middleware terminable doit être enregistré (global ou sur des routes). Par défaut, Laravel crée une nouvelle instance du middleware pour appeler terminate. Pour garder la même instance entre handle et terminate, on l'enregistre comme singleton dans AppServiceProvider :
use App\Http\Middleware\LogVisite;
public function register(): void
{
$this->app->singleton(LogVisite::class);
}LARAVEL_START est une constante définie dans public/index.php : le moment où la requête est arrivée.
On peut aussi déclarer les middlewares dans le contrôleur lui-même (chapitre 6). En Laravel 11 à 13, le contrôleur implémente l'interface HasMiddleware ; en Laravel 13, on peut aussi utiliser l'attribut #[Middleware] :
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
class CommandeController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
'auth',
new Middleware('role:admin', only: ['destroy']),
];
}
}Les kits de démarrage et les middlewares auth et guest. La vidéo montre Breeze (Laravel 11) : le principe des middlewares est identique.
(1) Crée le middleware EnsureUserHasRole (paramètre variadique) et déclare l'alias role. (2) Protège le groupe admin de routes/web.php avec auth et role:admin,libraire. (3) Ajoute une route GET /admin/utilisateurs réservée à role:admin. (4) Crée le middleware « après » AddSecurityHeaders et rends-le global. (5) Vérifie avec php artisan route:list -v que les middlewares apparaissent sur les bonnes routes. (6) En visitant /admin sans être connecté, que se passe-t-il ? (Il n'y a pas encore de route login : note l'erreur, elle disparaîtra au chapitre 12.)
Pour chaque besoin, dis si le middleware agit avant ou après le contrôleur, et s'il doit être global, de groupe ou de route : (1) refuser l'accès à l'administration aux non-administrateurs ; (2) ajouter l'en-tête X-Frame-Options à toutes les pages ; (3) forcer la langue du site selon la session ; (4) mesurer le temps de traitement de chaque requête ; (5) bloquer un utilisateur suspendu (colonne suspendu) sur tout l'espace client.
On peut créer son propre groupe, ou compléter les groupes existants :
->withMiddleware(function (Middleware $middleware): void {
// créer (ou compléter) un groupe
$middleware->appendToGroup('backoffice', [
'auth',
\App\Http\Middleware\EnsureUserIsAdmin::class,
]);
// ajouter un middleware à TOUTES les routes web
$middleware->web(append: [
\App\Http\Middleware\SetLocale::class,
]);
})Route::middleware('backoffice')->prefix('admin')->group(function () {
// toutes ces routes exigent d'être connecté ET administrateur
});La priorité des middlewares
Rarement, un middleware doit s'exécuter avant un autre, quel que soit l'ordre dans lequel on les a affectés à la route. On le règle avec $middleware->priority([...]) (liste complète) ou, plus simplement, avec prependToPriorityList / appendToPriorityList :
$middleware->appendToPriorityList(
after: \Illuminate\Routing\Middleware\SubstituteBindings::class,
append: \App\Http\Middleware\EnsureUserIsAdmin::class,
);Tout le chapitre « Enregistrement » du résumé utilise le fichier app/Http/Kernel.php et ses propriétés $middleware, $routeMiddleware, $middlewareGroups et $middlewarePriority. Ce fichier n'existe plus depuis Laravel 11. Les équivalents dans bootstrap/app.php : $middleware->append() (global), $middleware->alias() (alias), $middleware->appendToGroup(), web(), api() (groupes), $middleware->priority() (priorité). Le middleware CSRF s'appelait VerifyCsrfToken, puis ValidateCsrfToken ; depuis Laravel 13, c'est PreventRequestForgery.
Écris un middleware CheckBoutiqueOuverte qui renvoie une page « La boutique est fermée pour inventaire » (code 503) si la variable de configuration kitab.boutique_ouverte vaut false, sauf pour les administrateurs (role = admin). Crée le fichier config/kitab.php qui lit la variable BOUTIQUE_OUVERTE du .env. Enregistre le middleware pour toutes les routes web.
Un ancien projet (Laravel 9) contient dans app/Http/Kernel.php : 'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class, dans $routeMiddleware, et \App\Http\Middleware\AddSecurityHeaders::class, dans $middleware. Écris le bloc withMiddleware équivalent pour Laravel 13.