backend / php-laravel
Validation des formulaires et Form Requests
Explication
Ce que vous allez apprendre
- Créer un FormRequest dédié pour isoler la validation ET l'autorisation hors des contrôleurs
- Valider chaque élément d'un tableau individuellement avec la notation
tags.* - Personnaliser les messages d'erreur de validation en français
- Ajouter des règles de validation croisées entre plusieurs champs avec
withValidator - Personnaliser la réponse en cas d'échec de validation pour une API pure
Dans quel contexte ?
Un contrôleur ProduitController::store() devenait de plus en plus difficile à lire : trente lignes de règles de validation mélangées avec la logique de création du produit. Extraire cette validation dans une classe StoreProduitRequest dédiée rend le contrôleur à nouveau lisible en quelques lignes, tout en centralisant aussi la question "qui a le droit de créer un produit ?" au même endroit.
D'abord, pourquoi sortir la validation du contrôleur ?
Un FormRequest est une classe qui se glisse à la place de Request dans la signature d'une méthode de contrôleur. Laravel exécute automatiquement sa validation (méthode rules()) et son autorisation (méthode authorize()) AVANT même d'entrer dans le corps de la méthode du contrôleur — si l'une des deux échoue, le contrôleur n'est jamais exécuté.
Ce découplage suit le principe de responsabilité unique : le contrôleur orchestre l'action métier, le FormRequest se charge uniquement de vérifier que les données et les droits sont valides.
Prérequis
Il faut connaître les migrations (leçon précédente), car les règles de validation référencent souvent directement des tables et colonnes (exists:categories,id).
Ensuite, valider chaque élément d'une liste, pas juste la liste elle-même
Une règle comme 'tags' => ['array'] vérifie seulement que tags est bien un tableau, pas que chaque valeur qu'il contient est valide. La notation 'tags.*' => ['integer', 'exists:tags,id'] applique la règle à chaque élément individuellement — indispensable dès qu'on valide des tableaux d'identifiants ou de sous-objets.
| Règle | Vérifie |
|---|---|
'tags' => 'array' | tags est un tableau (structure globale) |
'tags.*' => 'integer' | Chaque élément du tableau est un entier |
'tags.*' => 'exists:tags,id' | Chaque élément correspond à un id existant |
Il reste un cas que les règles simples ne couvrent pas : la validation croisée
Certaines règles métier dépendent de plusieurs champs à la fois, comme "le prix ne peut pas être inférieur à 1 pour la catégorie premium". withValidator() permet d'ajouter cette logique après la validation standard, avec un accès complet à toutes les données de la requête.
Piège fréquent
Essayer de représenter une validation croisée complexe uniquement avec les règles déclaratives standard mène souvent à des expressions illisibles. Dès que la logique dépend de plusieurs champs en même temps, withValidator() reste plus clair et plus facile à tester.
Maintenant, adapter la réponse d'erreur au type de client
Par défaut, un FormRequest redirige vers la page précédente avec les erreurs en session — un comportement pensé pour une application web classique. Pour une API pure qui doit toujours répondre en JSON, failedValidation() peut être surchargée pour lancer une HttpResponseException avec une réponse JSON structurée (422) au lieu d'une redirection.
Astuce
$request->validated() ne renvoie QUE les champs déclarés dans rules(), jamais de champs supplémentaires envoyés par erreur ou malveillance. C'est une protection complémentaire à $fillable : même si un attaquant ajoute un champ est_admin au payload, il n'apparaîtra jamais dans validated() s'il n'est pas dans les règles.
Maintenant que les données entrantes sont validées de façon fiable, la prochaine leçon aborde l'autre bout de la chaîne : comment ces données sont affichées dans une interface, avec le moteur de templates Blade.
Commandes & code
Validation des formulaires et Form Requests
<?php
// app/Http/Requests/StoreProduitRequest.php
// Un FormRequest isole la validation ET l'autorisation hors du contrôleur
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class StoreProduitRequest extends FormRequest {
public function authorize(): bool {
// Vérifie que l'utilisateur a le droit d'effectuer cette action
return $this->user()->can('create', \App\Models\Produit::class);
}
public function rules(): array {
return [
'nom' => ['required', 'string', 'min:3', 'max:120'],
'sku' => ['required', 'string', 'unique:produits,sku'],
'prix' => ['required', 'numeric', 'min:0', 'max:1000000'],
'categorie_id' => ['required', 'integer', 'exists:categories,id'],
'description' => ['nullable', 'string', 'max:5000'],
'tags' => ['array'],
'tags.*' => ['integer', 'exists:tags,id'], // valide chaque élément du tableau
'image' => ['nullable', 'image', 'max:2048'], // 2 Mo max, doit être une image
];
}
// Messages d'erreur personnalisés en français
public function messages(): array {
return [
'nom.required' => "Le nom du produit est obligatoire.",
'prix.min' => "Le prix ne peut pas être négatif.",
'sku.unique' => "Ce SKU existe déjà.",
];
}
// Validation conditionnelle avancée après les règles de base
public function withValidator(Validator $validator): void {
$validator->after(function ($validator) {
if ($this->input('prix') < 1 && $this->input('categorie_id') === 1) {
$validator->errors()->add('prix', "Prix trop bas pour cette catégorie premium.");
}
});
}
// Réponse personnalisée en cas d'échec (utile pour une API pure)
protected function failedValidation(Validator $validator): void {
throw new HttpResponseException(
response()->json(['erreurs' => $validator->errors()], 422)
);
}
}
// Utilisation dans le contrôleur : la validation se fait avant même d'entrer dans la méthode
class ProduitController extends Controller {
public function store(StoreProduitRequest $request): \Illuminate\Http\JsonResponse {
$produit = \App\Models\Produit::create($request->validated());
return response()->json($produit, 201);
}
}<?php
// Validation manuelle (hors FormRequest), utile pour de la validation ad hoc
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($request->all(), [
'email' => ['required', 'email'],
'age' => ['required', 'integer', 'between:0,120'],
]);
if ($validator->fails()) {
return response()->json(['erreurs' => $validator->errors()], 422);
}
$donneesValidees = $validator->validated();Résumé
FormRequestsépare la validation et l'autorisation du contrôleur (Single Responsibility).tags.*valide chaque élément d'un tableau individuellement.withValidator()permet des règles de validation croisées entre plusieurs champs.$request->validated()ne retourne que les champs passés dansrules(), jamais de champs non attendus.
Exercices pratiques
Mission : valider une liste de tags et une regle de prix premium
Objectif : Corriger un FormRequest qui valide mal un tableau de tags et lui ajouter une regle de validation croisee entre deux champs.
Contexte
Le StoreProduitRequest d'une boutique declare la regle 'tags' => ['array'] pour valider les tags envoyes a la creation d'un produit. Un client a envoye tags: [1, 2, 'admin'], et le produit a ete cree avec un tag totalement invalide ('admin' n'est ni un entier ni un id existant dans la table tags) sans qu'aucune erreur de validation ne soit remontee. Par ailleurs, l'equipe metier veut desormais interdire un prix inferieur a 1 euro specifiquement pour la categorie premium (categorie_id = 1), une regle qui ne correspond a aucune regle de validation declarative simple.