Module 2Programmer avec Laravel
Chapitre 7 sur 17
Manipulation des requêtes HTTP
L'objet Request, les données d'entrée, la validation, les Form Requests, les fichiers, proxys et hôtes.
- ≈ 190 min
- 8 leçons
- 1 vidéo
- 4 exercices
Module 2Programmer avec Laravel
Chapitre 7 sur 17
L'objet Request, les données d'entrée, la validation, les Form Requests, les fichiers, proxys et hôtes.
Objectifs
Avec ce chapitre, tu sauras…
Request et lire les informations de la requête : chemin, URL, méthode, en-têtes, adresse IP, type de contenu attendu ;validate() et les Form Requests, et afficher les erreurs ;:::savoir Ajouté au résumé Le résumé ne traite pas la validation des données. C'est pourtant l'étape obligatoire de tout formulaire, et elle revient dans tous les examens de fin de module. La section 4 de ce chapitre la présente en détail, d'après la documentation de Laravel 13. :::
Leçon 1
La classe Illuminate\Http\Request représente la requête HTTP courante sous forme d'objet. Elle donne accès aux données envoyées (champs de formulaire, paramètres d'URL, JSON), aux fichiers, aux cookies, aux en-têtes et à des informations comme l'adresse IP du client. Elle étend la classe Request du composant Symfony HttpFoundation.
Leçon 2
Accéder à la requête
On type le paramètre Request dans une méthode de contrôleur ou une closure de route : le conteneur de services l'injecte automatiquement.
use Illuminate\Http\Request;
public function store(Request $request)
{
$nom = $request->input('nom');
// ...
}Route::get('/recherche', function (Request $request) {
return $request->query('q');
});Si la méthode reçoit aussi des paramètres de route, ils viennent après les dépendances : public function update(Request $request, string $id). On peut aussi obtenir la requête n'importe où avec l'assistant request() (par exemple dans une vue).
Le chemin, l'URL et l'hôte
Pour une requête vers https://kitab.ma/livres/le-petit-prince?ref=accueil :
| Méthode | Résultat |
|---|---|
$request->path() | livres/le-petit-prince |
$request->is('livres/*') | true (le * est un joker) |
$request->routeIs('livres.*') | true si la route a un nom qui commence par livres. |
$request->url() | https://kitab.ma/livres/le-petit-prince (sans la chaîne de requête) |
$request->fullUrl() | https://kitab.ma/livres/le-petit-prince?ref=accueil |
$request->fullUrlWithQuery(['page' => 2]) | …?ref=accueil&page=2 |
$request->fullUrlWithoutQuery(['ref']) | …/le-petit-prince |
$request->host() | kitab.ma |
$request->httpHost() | kitab.ma (avec le port s'il n'est pas standard) |
$request->schemeAndHttpHost() | https://kitab.ma |
Leçon 3
| Méthode | Exemple | Résultat |
|---|---|---|
string | $request->string('nom')->trim() | un objet Stringable (méthodes trim, upper, slug…) |
integer | $request->integer('page', 1) | un entier ('3' → 3) |
float | $request->float('prix') | un nombre décimal |
boolean |
Leçon 4
| Règle | Vérifie que la valeur… |
|---|---|
required | est présente et non vide |
nullable | peut être vide (null) ; à mettre pour un champ facultatif |
string, integer, numeric, boolean, array | est du bon type |
email | est une adresse e-mail valide |
min:3, max:255 | a une longueur (texte), une valeur (nombre) ou une taille en Ko (fichier) minimale ou maximale |
Leçon 5
| Disque | Dossier | Accessible depuis le web ? |
|---|---|---|
local (par défaut) | storage/app/private | non : factures, pièces d'identité |
public | storage/app/public | oui, après php artisan storage:link (URL /storage/...) |
s3 | un espace de stockage Amazon S3 (ou compatible) | selon la configuration |
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
1$request->query('tri') lit…
2$request->boolean('newsletter') avec la valeur "on" renvoie…
3$request->filled('nom') est vrai si le champ est…
4old('titre') sert à…
5Si validate() échoue sur un formulaire classique, Laravel…
6La règle qui vérifie qu'une catégorie existe est…
7Un formulaire d'envoi de fichier doit avoir…
8Pour qu'une image stockée soit visible sur le site, on utilise le disque…
L'essentiel du chapitre
Request injectée (ou request()) : path, is, routeIs, url, fullUrl, fullUrlWithQuery, host, method, isMethod, header, hasHeader, bearerToken, ip, accepts, prefers, expectsJson.all, collect, input (défaut, notation point, JSON), query, propriétés dynamiques ; typées : string, integer, float, boolean (1, true, on, yes), date, enum, array ; only, except ; présence : has, hasAny, filled, isNotFilled, missing, whenHas, whenFilled ; merge, mergeIfMissing ; ancienne saisie : flash, withInput, old() ; cookie() (chiffrés).TrimStrings, ConvertEmptyStringsToNull (globaux) ; trimStrings(except:) dans bootstrap/app.php.$request->validate([...]) → données validées ; échec = redirection + $errors + ancienne saisie (ou 422 JSON). Règles : required, nullable, types, min, max, in, unique, exists, confirmed, date, image, mimes ; Rule::enum, Rule::unique()->ignore(), Password::min(8). Vue : $errors->any(), @error('champ') {{ $message }} @enderror. Form Request : make:request, authorize(), rules(), messages(), attributes(), $request->validated(). Jamais create($request->all()).$request->boolean('en_stock') |
true pour 1, "1", true, "true", "on", "yes" ; false sinon |
date | $request->date('publie_le') | un objet date Carbon (ou null) |
enum | $request->enum('langue', Langue::class) | le cas de l'énumération (ou null) |
array | $request->array('categories') | toujours un tableau |
| Méthode | Renvoie true si… |
|---|---|
has('nom') | la clé est présente (même vide) |
has(['nom', 'email']) | toutes les clés sont présentes |
hasAny(['nom', 'email']) | au moins une est présente |
filled('nom') | la clé est présente et non vide |
isNotFilled('nom') | la clé est absente ou vide |
anyFilled(['nom', 'email']) | au moins une est remplie |
missing('nom') | la clé est absente |
between:1,5 | est comprise entre deux bornes |
in:ar,fr,en | fait partie d'une liste |
unique:livres,isbn | n'existe pas déjà dans la colonne isbn de la table livres |
exists:categories,id | existe dans la colonne id de la table categories (clé étrangère) |
confirmed | est identique au champ xxx_confirmation (mot de passe) |
date, before:today, after:publie_le | est une date, avant ou après une autre |
regex:/^[0-9]{13}$/ | respecte une expression régulière |
image, mimes:jpg,png,webp, file | est une image, a un de ces types, est un fichier |
url, alpha, alpha_dash, digits:10 | est une URL, des lettres, un identifiant, exactement 10 chiffres |
Le disque local a changé en Laravel 11
Écris le tableau de règles pour le formulaire d'inscription d'un client de Kitab : nom (obligatoire, 2 à 80 caractères), email (obligatoire, valide, unique dans users), téléphone (facultatif, exactement 10 chiffres et commence par 0), ville (obligatoire, parmi Casablanca, Rabat, Fès, Marrakech, Tanger), mot de passe (obligatoire, au moins 8 caractères avec lettres et chiffres, confirmé), conditions (doivent être acceptées : règle accepted).
Écris la vue Blade du formulaire de l'exercice 7.3 (champs nom, email, ville en liste déroulante) avec : le jeton CSRF, le réaffichage de la saisie, le message d'erreur sous chaque champ, et la ville précédemment choisie resélectionnée (directive @selected).
enctype="multipart/form-data" ; file, hasFile, isValid, extension, getClientOriginalName ; store('dossier', 'public'), storeAs ; disque local = storage/app/private (privé), public + php artisan storage:link ; asset('storage/'.$chemin).$middleware->trustProxies(at: [...]) ; hôtes : $middleware->trustHosts(at: [...]).La méthode HTTP
$methode = $request->method(); // "GET", "POST"...
if ($request->isMethod('post')) {
// ...
}Les en-têtes, le jeton et l'adresse IP
$valeur = $request->header('X-Magasin'); // null si absent
$valeur = $request->header('X-Magasin', 'casablanca'); // valeur par défaut
if ($request->hasHeader('X-Magasin')) { /* ... */ }
$jeton = $request->bearerToken(); // le jeton de l'en-tête "Authorization: Bearer xxx" ("" si absent)
$ip = $request->ip(); // l'adresse IP du client
$navigateur = $request->userAgent();La négociation de contenu
L'en-tête Accept indique le format de réponse souhaité par le client :
$types = $request->getAcceptableContentTypes(); // ['text/html', ...]
if ($request->accepts(['text/html', 'application/json'])) { /* ... */ }
$prefere = $request->prefers(['text/html', 'application/json']); // le type préféré ou null
if ($request->expectsJson()) {
return response()->json(['message' => 'Ajouté au panier']); // appel JavaScript
}
return back()->with('status', 'Ajouté au panier'); // formulaire classiqueexpectsJson() renvoie true pour une requête AJAX qui demande du JSON. C'est ainsi que Laravel décide de renvoyer les erreurs de validation en JSON ou par redirection.
Les requêtes PSR-7
PSR-7 est une norme qui définit des interfaces communes pour les messages HTTP en PHP. Pour obtenir une requête PSR-7 au lieu d'une requête Laravel (rarement utile, pour une bibliothèque qui l'exige), on installe deux paquets puis on type l'interface :
composer require symfony/psr-http-message-bridge nyholm/psr7use Psr\Http\Message\ServerRequestInterface;
Route::get('/psr', function (ServerRequestInterface $request) {
return $request->getUri()->getPath();
});Récupérer toutes les données
$donnees = $request->all(); // tableau de toutes les données (formulaire + chaîne de requête + JSON)
$donnees = $request->collect(); // la même chose sous forme de collection
$request->collect('livres')->each(function (array $livre) {
// parcourir un sous-ensemble
});
$donnees = $request->input(); // tableau associatif, comme all() mais sans les fichiersRécupérer une valeur
input lit une valeur quel que soit le verbe HTTP et l'endroit (formulaire, chaîne de requête, JSON) :
$titre = $request->input('titre');
$titre = $request->input('titre', 'Sans titre'); // valeur par défaut
$premier = $request->input('livres.0.titre'); // notation « point » pour les tableaux
$tous = $request->input('livres.*.titre'); // tous les titres
$ville = $request->input('client.adresse.ville'); // JSON imbriqué (Content-Type: application/json)query lit seulement la chaîne de requête (après le ?) :
$tri = $request->query('tri', 'titre'); // GET /livres?tri=prix → "prix"
$tout = $request->query(); // toute la chaîne de requête en tableauOn peut aussi lire une donnée comme une propriété : $request->titre. Laravel cherche d'abord dans les données envoyées, puis dans les paramètres de route.
Les méthodes typées
Les données HTTP arrivent toujours sous forme de chaînes. Les méthodes typées les convertissent :
On ne fait jamais confiance aux données envoyées par le client : un champ peut être vide, trop long, du mauvais type, ou volontairement malveillant. La validation vérifie chaque donnée avant de l'utiliser.
La méthode validate()
public function store(Request $request): RedirectResponse
{
$valide = $request->validate([
'nom' => ['required', 'string', 'max:120'],
'nationalite' => ['nullable', 'string', 'max:60'],
'email' => ['nullable', 'email', 'unique:auteurs,email'],
'date_naissance' => ['nullable', 'date', 'before:today'],
]);
Auteur::create($valide); // seulement les champs validés
return redirect()->route('admin.auteurs.index')->with('status', 'Auteur ajouté.');
}Si toutes les règles passent, validate() renvoie un tableau des données validées (uniquement les champs listés). Si une règle échoue, Laravel arrête le traitement et :
expectsJson()) : renvoie une réponse 422 avec les erreurs en JSON.Tableau: Les règles les plus utilisées
Le formulaire
Un formulaire qui envoie un fichier doit avoir l'attribut enctype="multipart/form-data" et la méthode POST :
<form method="POST" action="{{ route('admin.livres.store') }}" enctype="multipart/form-data">
@csrf
<input type="file" name="couverture" accept="image/*">
@error('couverture') <p class="message-erreur">{{ $message }}</p> @enderror
<button type="submit">Envoyer</button>
</form>Récupérer et inspecter le fichier
$fichier = $request->file('couverture'); // un objet Illuminate\Http\UploadedFile (ou null)
$fichier = $request->couverture; // même chose (propriété dynamique)
if ($request->hasFile('couverture') && $request->file('couverture')->isValid()) {
$chemin = $fichier->path(); // chemin temporaire sur le serveur
$extension = $fichier->extension(); // devinée d'après le CONTENU ("jpg")
$nomOriginal = $fichier->getClientOriginalName(); // nom donné par le client (à ne pas croire)
$taille = $fichier->getSize(); // en octets
$type = $fichier->getMimeType(); // "image/jpeg"
}extension() devine l'extension d'après le contenu du fichier : elle peut différer de l'extension donnée par le client. N'utilise jamais le nom original pour enregistrer un fichier (risque d'écraser un autre fichier ou d'injecter un nom dangereux).
Stocker le fichier
La méthode store déplace le fichier vers un disque configuré dans config/filesystems.php, sous un nom unique généré automatiquement, et renvoie le chemin relatif au disque :
$chemin = $request->file('couverture')->store('couvertures', 'public');
// "couvertures/Xk2b9Qm4...jpg" sur le disque public (storage/app/public)
$chemin = $request->file('couverture')->storeAs('couvertures', "{$livre->isbn}.jpg", 'public');
// nom choisi : couvertures/9782070612758.jpg
$chemin = $request->file('facture')->store('factures'); // disque par défaut "local" : storage/app/privatePour afficher une image du disque public, on crée une fois le lien symbolique, puis on génère l'URL :
php artisan storage:link # crée public/storage → storage/app/public<img src="{{ asset('storage/' . $livre->couverture) }}" alt="Couverture de {{ $livre->titre }}">
{{-- ou : Storage::url($livre->couverture) --}}Pour supprimer l'ancienne image lors d'une modification : Storage::disk('public')->delete($livre->couverture);.
Une application en production est souvent derrière un répartiteur de charge (load balancer) ou un proxy (Cloudflare, Nginx) qui gère le HTTPS et transmet la requête en HTTP. Sans configuration, Laravel croit que la requête est en HTTP et génère des liens http://, et $request->ip() renvoie l'adresse du proxy. On déclare les proxys de confiance dans bootstrap/app.php :
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: [
'192.168.1.1',
'10.0.0.0/8',
]);
// derrière un service cloud dont on ne connaît pas les adresses :
// $middleware->trustProxies(at: '*');
})Laravel lit alors les en-têtes X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Port et X-Forwarded-Proto envoyés par ces proxys.
Par défaut, Laravel répond à toutes les requêtes, quel que soit l'en-tête Host. Un attaquant peut envoyer un faux Host pour fausser les URL générées (par exemple dans un e-mail de réinitialisation de mot de passe). Le mieux est de configurer le serveur web pour qu'il n'accepte que le bon nom de domaine ; sinon, on active trustHosts :
$middleware->trustHosts(at: ['^kitab\.ma$']); // expression régulière
$middleware->trustHosts(at: ['^kitab\.ma$'], subdomains: false); // refuser aussi les sous-domainesLes requêtes avec un autre Host sont rejetées. Par défaut, les sous-domaines de l'adresse APP_URL sont aussi acceptés.
Le résumé configure les proxys et les hôtes en modifiant les classes App\Http\Middleware\TrustProxies (propriétés $proxies et $headers) et TrustHosts (méthode hosts()), à « décommenter » dans Kernel.php. Ces fichiers n'existent plus depuis Laravel 11 : on utilise $middleware->trustProxies(at: …) et $middleware->trustHosts(at: …) dans bootstrap/app.php.
Valider toutes les données reçues : validate(), règles et affichage des erreurs.
(1) Génère StoreLivreRequest et écris ses règles (isbn, titre, prix, stock, langue, couverture). (2) Dans Admin\LivreController@create, renvoie la vue admin.livres.create avec le formulaire (enctype, @csrf, old(), @error). (3) Dans store, type la Form Request, enregistre la couverture sur le disque public dans couvertures/ et renvoie pour l'instant dd($request->validated(), $chemin). (4) Crée le lien storage:link et vérifie que l'image est accessible à l'adresse /storage/couvertures/.... (5) Envoie le formulaire vide, puis un ISBN de 12 chiffres, puis un fichier PDF à la place de l'image : relève les messages d'erreur.
Pour une requête POST https://kitab.ma/panier?source=fiche dont le formulaire contient livre_id=12, quantite=2, cadeau=on, écris la méthode de $request qui donne : (1) panier ; (2) 12 sous forme d'entier ; (3) true pour l'option cadeau ; (4) fiche ; (5) ['livre_id' => '12', 'quantite' => '2'] ; (6) l'adresse IP du client ; (7) true si la requête vient d'un appel JavaScript qui attend du JSON ; (8) "POST".
$publie = $request->date('publie_le'); // null si absent
$heure = $request->date('ouverture', '!H:i', 'Africa/Casablanca'); // format et fuseau horaire
$langue = $request->enum('langue', Langue::class, Langue::Francais); // avec une valeur par défautRécupérer une partie des données
$identifiants = $request->only(['email', 'password']);
$identifiants = $request->only('email', 'password'); // même résultat
$sansCarte = $request->except(['numero_carte']);Tableau: Tester la présence d'une donnée
$request->whenHas('code_promo', function (string $code) {
// exécuté si code_promo est présent
}, function () {
// exécuté sinon
});
$request->whenFilled('recherche', function (string $texte) use (&$requete) {
// exécuté si recherche est remplie
});Il existe aussi whenMissing, sur le même modèle.
Fusionner des données
$request->merge(['quantite' => 1]); // ajoute ou ÉCRASE la valeur
$request->mergeIfMissing(['quantite' => 1]); // ajoute seulement si la clé n'existe pasL'ancienne saisie (old input)
Quand un formulaire contient une erreur, on renvoie l'utilisateur au formulaire avec ce qu'il avait tapé, pour qu'il n'ait pas tout à ressaisir. Laravel « flashe » la saisie dans la session : elle reste disponible pour la requête suivante seulement.
$request->flash(); // toute la saisie
$request->flashOnly(['nom', 'email']);
$request->flashExcept('password'); // jamais les mots de passe
return redirect('/inscription')->withInput(); // flasher puis rediriger
return redirect()->route('inscription')->withInput($request->except('password'));
return back()->withInput(); // revenir à la page précédentePour relire l'ancienne saisie : $request->old('nom') dans un contrôleur, ou l'assistant old() dans une vue :
<input type="text" name="titre" value="{{ old('titre') }}">
<input type="text" name="titre" value="{{ old('titre', $livre->titre) }}"> {{-- formulaire de modification --}}La validation de Laravel flashe automatiquement la saisie en cas d'erreur : en pratique, on écrit rarement flash() soi-même, mais on utilise toujours old() dans les formulaires.
Les cookies
Tous les cookies créés par Laravel sont chiffrés et signés : un cookie modifié par le client est considéré comme invalide.
$valeur = $request->cookie('derniere_categorie');La normalisation des données
Deux middlewares globaux nettoient automatiquement les données : TrimStrings supprime les espaces au début et à la fin de chaque champ texte, et ConvertEmptyStringsToNull transforme un champ vide en null. Pour les désactiver sur certaines requêtes :
use Illuminate\Http\Request;
->withMiddleware(function (Middleware $middleware): void {
$middleware->trimStrings(except: [
fn (Request $request) => $request->is('admin/editeur/*'), // garder les espaces ici
]);
$middleware->convertEmptyStringsToNull(except: [
fn (Request $request) => $request->is('api/*'),
]);
})Les règles s'écrivent en tableau (recommandé) ou en chaîne séparée par des barres : 'nom' => 'required|string|max:120'. Pour des règles plus complexes, on utilise la classe Rule et les objets de règle :
use App\Enums\Langue;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
$request->validate([
'langue' => ['required', Rule::enum(Langue::class)],
'isbn' => ['required', Rule::unique('livres', 'isbn')->ignore($livre->id)], // modification : ignorer le livre lui-même
'password' => ['required', 'confirmed', Password::min(8)->letters()->numbers()],
'categorie_id' => ['required', Rule::exists('categories', 'id')],
]);Afficher les erreurs dans la vue
Après un échec, la vue dispose de la variable $errors (grâce au middleware ShareErrorsFromSession du groupe web). La directive @error affiche le message d'un champ :
@if ($errors->any())
<div class="alerte erreur">
<p>Le formulaire contient {{ $errors->count() }} erreur(s) :</p>
<ul>
@foreach ($errors->all() as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.auteurs.store') }}">
@csrf
<label for="nom">Nom</label>
<input id="nom" type="text" name="nom" value="{{ old('nom') }}" @class(['invalide' => $errors->has('nom')])>
@error('nom')
<p class="message-erreur">{{ $message }}</p>
@enderror
<button type="submit">Enregistrer</button>
</form>Les messages en français
Les messages par défaut sont en anglais (« The nom field is required. »). Pour les avoir en français :
APP_LOCALE=fr dans le .env ;php artisan lang:publish, qui crée lang/en), puis créer lang/fr/validation.php avec les traductions (le paquet communautaire laravel-lang/common fournit des traductions complètes : composer require laravel-lang/common --dev puis php artisan lang:add fr) ;validate() :$request->validate(
['nom' => ['required', 'max:120']],
['nom.required' => 'Le nom de l\'auteur est obligatoire.', 'max' => 'Le champ :attribute dépasse :max caractères.'],
['nom' => 'nom de l\'auteur'],
);Les Form Requests
Quand la validation devient longue, on la déplace dans une classe dédiée, une Form Request :
php artisan make:request StoreLivreRequest<?php
namespace App\Http\Requests;
use App\Enums\Langue;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreLivreRequest extends FormRequest
{
/** L'utilisateur a-t-il le droit d'envoyer ce formulaire ? */
public function authorize(): bool
{
return in_array($this->user()?->role, ['admin', 'libraire'], true);
}
/** Les règles de validation. */
public function rules(): array
{
return [
'isbn' => ['required', 'regex:/^97[89][0-9]{10}$/', 'unique:livres,isbn'],
'titre' => ['required', 'string', 'max:200'],
'resume' => ['nullable', 'string', 'max:5000'],
'prix' => ['required', 'numeric', 'min:0', 'max:9999.99'],
'stock' => ['required', 'integer', 'min:0'],
'langue' => ['required', Rule::enum(Langue::class)],
'categorie_id' => ['required', 'exists:categories,id'],
'auteurs' => ['required', 'array', 'min:1'],
'auteurs.*' => ['integer', 'exists:auteurs,id'],
'couverture' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
];
}
/** Les messages personnalisés. */
public function messages(): array
{
return [
'isbn.regex' => 'L\'ISBN doit compter 13 chiffres et commencer par 978 ou 979.',
'auteurs.required' => 'Choisis au moins un auteur.',
];
}
/** Les noms des champs dans les messages. */
public function attributes(): array
{
return ['categorie_id' => 'catégorie', 'couverture' => 'image de couverture'];
}
}Il suffit ensuite de typer la Form Request à la place de Request : la validation s'exécute avant même d'entrer dans la méthode.
use App\Http\Requests\StoreLivreRequest;
public function store(StoreLivreRequest $request): RedirectResponse
{
$donnees = $request->validated(); // seulement les champs validés
// ... enregistrer (chapitre 11)
return redirect()->route('admin.livres.index')->with('status', 'Livre ajouté.');
}Si authorize() renvoie false, Laravel répond 403 ; si une règle échoue, il redirige avec les erreurs. La méthode store ne s'exécute que si tout est valide.
Depuis Laravel 11, le disque local enregistre dans storage/app/private (et non plus storage/app). Un fichier envoyé avec store('couvertures') sans préciser de disque n'est donc pas accessible depuis le web, même avec storage:link. Pour une image publique, précise toujours le disque 'public'.