Module 2Programmer avec Laravel
Chapitre 8 sur 17
Manipulation des réponses HTTP
Réponses, codes de statut, cookies, redirections avec messages flashés, JSON, téléchargements et macros.
- ≈ 145 min
- 5 leçons
- 4 exercices
- TP
Module 2Programmer avec Laravel
Chapitre 8 sur 17
Réponses, codes de statut, cookies, redirections avec messages flashés, JSON, téléchargements et macros.
Objectifs
Avec ce chapitre, tu sauras…
Response ;Leçon 1
Chaînes et tableaux
Toute route et toute action de contrôleur doit renvoyer une réponse. La plus simple est une chaîne : Laravel la transforme en réponse HTTP complète (code 200, type text/html). Un tableau est converti automatiquement en JSON :
Route::get('/', function () {
return 'Bienvenue sur Kitab'; // text/html
});
Route::get('/infos', function () {
return ['nom' => 'Kitab', 'villes' => ['Casablanca', 'Rabat']]; // application/json
});Les objets Response
Pour choisir le code de statut et les en-têtes, on renvoie un objet Illuminate\Http\Response, créé avec l'assistant response() :
Route::get('/robots.txt', function () {
return response("User-agent: *\nDisallow: /admin", 200)
->header('Content-Type', 'text/plain');
});La classe Response hérite de Symfony\Component\HttpFoundation\Response : elle dispose de nombreuses méthodes pour construire la réponse.
Tableau: Les codes de statut à connaître
| Code | Signification | Quand Laravel l'utilise |
|---|---|---|
| 200 OK | succès | réponse normale |
| 201 Created | ressource créée | à renvoyer après un store d'API |
| 204 No Content | succès sans contenu | après un destroy d'API |
| 301 / 302 | redirection définitive / temporaire | redirect() renvoie 302 |
| 401 Unauthorized | non authentifié | requête JSON sans être connecté |
| 403 Forbidden | authentifié mais non autorisé | abort(403), autorisation refusée |
| 404 Not Found | introuvable | route absente, findOrFail, liaison de modèle |
| 419 Page Expired | jeton CSRF invalide | propre à Laravel (chapitre 5) |
| 422 Unprocessable Content | données invalides | échec de validation en JSON |
| 429 Too Many Requests | trop de requêtes | middleware throttle |
| 500 Internal Server Error | erreur du serveur | exception non gérée |
| 503 Service Unavailable | indisponible | mode maintenance (php artisan down) |
Leçon 2
Leçon 3
| Méthode de la redirection | Effet |
|---|---|
->with('status', '...') | flashe une donnée (souvent un message) |
->withInput() | flashe la saisie (relue avec old()) |
->withErrors(['email' => 'Identifiants incorrects.']) | flashe des messages d'erreur (relus avec $errors et @error) |
->withFragment('avis') | ajoute #avis à l'URL |
Leçon 4
| Méthode | Résultat |
|---|---|
response()->view(...) | une page HTML avec un code ou des en-têtes choisis |
response()->json(...) | du JSON |
response()->download(...) | un téléchargement d'un fichier existant |
response()->streamDownload(...) | un téléchargement d'un contenu généré |
response()->file(...) | un fichier affiché dans le navigateur |
Leçon 5
1Une route qui renvoie un tableau PHP produit…
2redirect() renvoie par défaut le code…
3Un utilisateur connecté qui n'a pas le droit d'agir reçoit…
4Une erreur de validation en JSON renvoie…
5->with('status', 'OK') sur une redirection…
6Pour afficher un PDF dans le navigateur sans le télécharger, on utilise…
7Après un POST réussi, on doit…
8Cookie::queue() sert à…
L'essentiel du chapitre
$hidden exclus), ou un objet response($contenu, $code) avec header() / withHeaders().abort(404), abort_if, abort_unless ; pages resources/views/errors/404.blade.php.cache.headers:public;max_age=86400;etag.->cookie(nom, valeur, minutes), Cookie::queue, cookie(), withoutCookie, Cookie::expire ; chiffrés par défaut ; exclusion : $middleware->encryptCookies(except: [...]).redirect('/uri'), back(), redirect()->route('nom', $modele), to_route(), action([...]), away() (jamais avec une URL non vérifiée), intended() ; ->with(), ->withInput(), ->withErrors() ; lire session('status'). Motif Post/Redirect/Get.response()->view(), json() (+ code), download() (nom ASCII), streamDownload(), file() (affiché), noContent().Response::macro('nom', fn …) dans AppServiceProvider::boot(), puis response()->nom(...).response()->noContent() | une réponse vide 204 |
La fonction abort() interrompt la requête avec un code d'erreur : abort(404), abort(403, 'Action non autorisée.'). Ses variantes abort_if($condition, 404) et abort_unless($condition, 403) évitent d'écrire un if. Pour personnaliser la page d'une erreur, on crée la vue resources/views/errors/404.blade.php (ou 403, 500…).
Les modèles et collections Eloquent
Un modèle ou une collection Eloquent renvoyé directement est converti en JSON, en respectant les attributs cachés du modèle (le mot de passe d'un utilisateur, par exemple, grâce à sa propriété $hidden) :
use App\Models\Livre;
Route::get('/api/livres/{livre}', function (Livre $livre) {
return $livre; // {"id":1,"titre":"Le Petit Prince",...}
});Ajouter des en-têtes
Les méthodes de réponse sont chaînables :
return response($contenu)
->header('Content-Type', 'text/csv')
->header('X-Kitab-Version', '1.0');
return response($contenu)->withHeaders([
'Content-Type' => 'text/csv',
'X-Kitab-Version' => '1.0',
]);Le middleware de contrôle du cache
Le middleware cache.headers règle l'en-tête Cache-Control d'un groupe de routes. Les directives s'écrivent en snake case, séparées par des points-virgules ; etag ajoute un identifiant (hachage du contenu) qui permet au navigateur de ne pas retélécharger une page inchangée :
Route::middleware('cache.headers:public;max_age=86400;etag')->group(function () {
Route::view('/conditions-generales', 'pages.cgv');
Route::view('/mentions-legales', 'pages.mentions');
});max_age=86400 autorise le navigateur à garder la page un jour (86 400 secondes) sans la redemander.
Attacher un cookie à une réponse
La méthode cookie ajoute un cookie : nom, valeur et durée de validité en minutes :
return response('Préférence enregistrée')->cookie('affichage', 'grille', 60 * 24 * 30); // 30 joursD'autres arguments, moins utilisés, ont le même sens que ceux de la fonction PHP setcookie :
return response('OK')->cookie('affichage', 'grille', $minutes, $chemin, $domaine, $secure, $httpOnly);Quand on n'a pas encore l'objet réponse (dans un service, par exemple), la façade Cookie met le cookie « en file d'attente » : il sera joint à la réponse au moment de l'envoi.
use Illuminate\Support\Facades\Cookie;
Cookie::queue('derniere_categorie', 'romans', 60);L'assistant cookie() crée un objet cookie, à attacher plus tard :
$cookie = cookie('affichage', 'liste', 60);
return response('OK')->cookie($cookie);Supprimer un cookie
return response('Préférence effacée')->withoutCookie('affichage');
Cookie::expire('affichage'); // sans objet réponseCookies et chiffrement
Par défaut, tous les cookies créés par Laravel sont chiffrés et signés : le client ne peut ni les lire ni les modifier. Pour qu'un cookie reste lisible par JavaScript (une préférence d'affichage, par exemple), on l'exclut du chiffrement dans bootstrap/app.php :
->withMiddleware(function (Middleware $middleware): void {
$middleware->encryptCookies(except: [
'affichage',
]);
})Le résumé exclut les cookies avec la propriété $except de la classe App\Http\Middleware\EncryptCookies. Depuis Laravel 11, cette classe n'est plus dans le projet : on écrit $middleware->encryptCookies(except: [...]) dans bootstrap/app.php.
Une redirection est une instance de Illuminate\Http\RedirectResponse. Elle répond au navigateur avec un code 302 et l'en-tête Location : le navigateur charge alors la nouvelle adresse.
return redirect('/livres'); // vers une URI
return back(); // vers la page précédente (formulaire invalide, par exemple)
return back()->withInput(); // en gardant la saisieback() utilise la session : la route doit faire partie du groupe web.
Vers une route nommée
Appelé sans argument, redirect() renvoie un objet Redirector qui propose plusieurs méthodes :
return redirect()->route('livres.index');
return redirect()->route('livres.show', ['slug' => 'le-petit-prince']);
return to_route('livres.index'); // raccourci (Laravel 9+)Si le paramètre de la route est rempli à partir d'un modèle Eloquent, on peut passer le modèle lui-même : Laravel en extrait la clé.
return redirect()->route('admin.livres.edit', [$livre]); // /admin/livres/12/edit
return redirect()->route('admin.livres.edit', $livre); // même résultatLa valeur extraite est la clé primaire, ou la colonne indiquée dans la route ({livre:slug}), ou celle renvoyée par la méthode getRouteKeyName() du modèle.
Vers une action de contrôleur
use App\Http\Controllers\CatalogueController;
return redirect()->action([CatalogueController::class, 'index']);
return redirect()->action([CatalogueController::class, 'show'], ['slug' => 'le-petit-prince']);Vers un site externe
away redirige vers une autre application, sans aucune vérification de l'URL :
return redirect()->away('https://www.cmi.co.ma/paiement?ref=' . $commande->numero);Ne redirige jamais vers une URL fournie par l'utilisateur sans la vérifier (redirect()->away($request->input('retour'))) : un pirate enverrait des liens kitab.ma/connexion?retour=https://site-pirate.com qui ont l'air sûrs mais renvoient vers son site (OWASP, module M108). Pour revenir à la page demandée avant la connexion, Laravel fournit redirect()->intended(route('accueil')), qui ne redirige que vers ton application.
Rediriger avec des données flashées
Après une action réussie, on redirige presque toujours en flashant un message dans la session : il n'est disponible que pour la requête suivante.
public function destroy(Livre $livre): RedirectResponse
{
$livre->delete();
return redirect()->route('admin.livres.index')
->with('status', "Le livre « {$livre->titre} » a été supprimé.");
}@if (session('status'))
<div class="alerte succes">{{ session('status') }}</div>
@endifAppelé sans argument, response() renvoie une fabrique de réponses (ResponseFactory) qui propose les méthodes suivantes.
Réponse de vue
Quand on veut une vue et un code ou des en-têtes particuliers :
return response()
->view('erreurs.rupture', ['livre' => $livre], 410)
->header('X-Raison', 'Livre retiré du catalogue');Sinon, l'assistant view('nom', $donnees) suffit (code 200).
Réponse JSON
json convertit le tableau en JSON (avec json_encode) et règle l'en-tête Content-Type: application/json :
return response()->json([
'resultats' => $livres->count(),
'livres' => $livres,
]);
return response()->json(['message' => 'Livre créé', 'id' => $livre->id], 201); // avec un codeJSONP (ancienne technique pour contourner l'interdiction des requêtes entre origines, remplacée aujourd'hui par le CORS) :
return response()->json(['nom' => 'Kitab'])->withCallback($request->input('callback'));Téléchargement de fichier
download force le navigateur à télécharger le fichier. Le deuxième argument est le nom vu par l'utilisateur ; le troisième, des en-têtes :
return response()->download(storage_path('app/private/factures/F-2026-0042.pdf'));
return response()->download($chemin, 'facture-kitab-0042.pdf', ['Content-Type' => 'application/pdf']);Le nom de fichier proposé doit être en caractères ASCII (exigence du composant Symfony qui gère les téléchargements). Pour un fichier d'un disque : Storage::download('factures/F-2026-0042.pdf').
Téléchargement en flux (streaming)
streamDownload propose au téléchargement un contenu généré à la volée, sans l'écrire sur le disque :
return response()->streamDownload(function () use ($commande) {
echo "Commande {$commande->numero}\n";
foreach ($commande->lignes as $ligne) {
echo "{$ligne->livre->titre} x {$ligne->quantite}\n";
}
}, "commande-{$commande->numero}.txt");Fichier affiché dans le navigateur
file affiche le fichier (image, PDF) dans le navigateur au lieu de le télécharger :
return response()->file(storage_path('app/private/catalogues/catalogue-2026.pdf'));
return response()->file($chemin, ['Content-Type' => 'application/pdf']);Une macro ajoute une méthode personnalisée à la fabrique de réponses, réutilisable dans toute l'application. On la déclare dans la méthode boot d'un fournisseur de services, par exemple AppServiceProvider :
use Illuminate\Support\Facades\Response;
public function boot(): void
{
Response::macro('succes', function (mixed $donnees, string $message = 'OK', int $code = 200) {
return Response::json([
'succes' => true,
'message' => $message,
'donnees' => $donnees,
], $code);
});
}return response()->succes($livre, 'Livre créé', 201);
// {"succes":true,"message":"Livre créé","donnees":{...}}Le premier argument de macro est le nom de la méthode ; le second, une closure exécutée à chaque appel.
Le résumé intitule cette section « Réponse Marco » : il s'agit des réponses macro. Son exemple (Response::macro('caps', …) qui renvoie le texte en majuscules) fonctionne toujours.
Dans Admin\LivreController : (1) store, update et destroy redirigent vers admin.livres.index avec un message flashé ; (2) le layout d'administration affiche ce message ; (3) une nouvelle route GET /admin/livres/export (déclarée avant la ressource) télécharge un CSV des livres généré avec streamDownload ; (4) crée la vue errors/404.blade.php avec un lien vers le catalogue ; (5) ajoute aux pages légales le middleware cache.headers avec une durée d'une semaine.
Pour chaque besoin de Kitab, écris l'instruction return : (1) après l'ajout d'un auteur, revenir à la liste des auteurs avec le message « Auteur ajouté » ; (2) la recherche instantanée renvoie au JavaScript la liste $livres et leur nombre ; (3) la facture factures/F-0042.pdf du disque privé est téléchargée sous le nom facture-0042.pdf ; (4) la page /conditions affiche la vue pages.cgv ; (5) l'identifiant reçu ne correspond à aucune commande de l'utilisateur ; (6) un formulaire de connexion échoue : revenir au formulaire avec l'e-mail saisi et l'erreur « Identifiants incorrects. » sur le champ email.
Crée deux routes : GET /affichage/{mode} (le mode vaut grille ou liste, sinon 404) qui enregistre le mode dans un cookie de 30 jours puis redirige vers le catalogue ; et GET /livres qui lit ce cookie (valeur par défaut grille) et renvoie « Affichage : grille » ou « Affichage : liste ». Le cookie doit rester lisible par JavaScript.
Après un formulaire POST réussi, on redirige toujours vers une page GET, au lieu d'afficher une vue directement. Sinon, si l'utilisateur rafraîchit la page, le navigateur renvoie le formulaire et l'action s'exécute deux fois (deux commandes, deux livres identiques). C'est le motif Post/Redirect/Get.
Crée deux macros : succes($donnees, $message, $code) et echec($message, $code = 400, $erreurs = []) qui renvoient un JSON de la forme {"succes": false, "message": "...", "erreurs": {...}}. Utilise-les dans une route d'API GET /api/livres/{isbn} qui renvoie le livre ou un échec 404 « Livre introuvable ».