backend / php-laravel
Contrôleurs et requêtes HTTP
Explication
Ce que vous allez apprendre
- Écrire les cinq méthodes CRUD standard d'un contrôleur Laravel (index, show, store, update, destroy)
- Valider directement les données d'une requête avec
$request->validate() - Construire des requêtes conditionnelles lisibles avec
when()sur le query builder - Utiliser les accesseurs typés de la requête (
->string(),->integer(),->boolean()) - Renvoyer des codes HTTP explicites et cohérents avec l'action réalisée
Dans quel contexte ?
Une API doit exposer une liste de produits filtrable par catégorie et par mot-clé de recherche, paginée, tout en gérant proprement la création avec validation des données envoyées. Sans les bons outils, ce contrôleur se transforme vite en une cascade de if imbriqués difficile à relire. Cette leçon montre comment Laravel simplifie chacune de ces étapes.
D'abord, un contrôleur est avant tout une classe organisée par convention
Un contrôleur Laravel regroupe les actions liées à une même ressource : index() pour lister, show() pour afficher un élément, store() pour créer, update() pour modifier, destroy() pour supprimer. Cette convention de nommage n'est pas obligatoire techniquement, mais elle est tellement répandue que s'en écarter surprendrait n'importe quel autre développeur Laravel reprenant le code.
Chaque méthode reçoit un objet Request qui représente toute la requête HTTP entrante : ses paramètres, ses en-têtes, ses fichiers uploadés.
Prérequis
Il faut connaître le routing Laravel (leçon précédente), puisque chaque méthode de contrôleur est reliée à une route précise.
Ensuite, valider avant de faire confiance à une seule donnée
Aucune donnée envoyée par un client (navigateur, application mobile, script externe) ne doit jamais être utilisée telle quelle. $request->validate([...]) vérifie chaque champ contre des règles (required, numeric, exists:categories,id...) et renvoie automatiquement une erreur 422 avec le détail si une règle échoue — sans qu'il faille écrire ce comportement à la main.
| Règle de validation | Vérifie |
|---|---|
required | Le champ est présent et non vide |
numeric | Le champ est un nombre |
exists:categories,id | La valeur existe dans la table categories |
max:120 | La longueur ou la valeur ne dépasse pas 120 |
Il reste à construire des requêtes qui s'adaptent aux filtres reçus
Plutôt que d'empiler des if ($request->has('categorie')) { $query->where(...); }, le query builder Eloquent propose when() : ->when($request->filled('categorie'), fn ($q) => $q->where(...)) n'applique la condition que si elle est pertinente, en gardant le code lisible en chaîne fluide.
Piège fréquent
$request->has('categorie') renvoie true même si le champ est présent mais vide (chaîne vide). $request->filled('categorie') est presque toujours le bon choix pour vérifier qu'une valeur exploitable a réellement été envoyée.
Maintenant, des accesseurs qui évitent des conversions manuelles fragiles
Lire $request->input('page') renvoie toujours une chaîne de caractères, même si elle "ressemble" à un nombre. Les accesseurs typés (->integer('page', 1), ->boolean('archives'), ->string('q')) effectuent la conversion correcte et gèrent une valeur par défaut, évitant des bugs subtils de comparaison de types comme vus en leçon 1.
Astuce
$request->boolean('archives') interprète correctement '1', 'true', 'on' et 'yes' comme true — exactement ce qu'envoient différents types de formulaires HTML ou de clients JSON, sans qu'il faille écrire cette logique de conversion soi-même.
Enfin, un code HTTP précis vaut mieux qu'un 200 générique
Renvoyer systématiquement un code 200, même pour une création réussie, prive le client d'une information utile. response()->json($produit, 201) signale explicitement "ressource créée", et response()->noContent() (204) signale une suppression réussie sans contenu à retourner.
Maintenant que tu sais recevoir et valider une requête au niveau du contrôleur, il est temps de voir comment ces données sont réellement stockées et récupérées en base : la prochaine leçon plonge dans Eloquent, l'ORM de Laravel.
Commandes & code
Contrôleurs et requêtes HTTP
<?php
namespace App\Http\Controllers;
use App\Models\Produit;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
class ProduitController extends Controller {
// index : liste avec filtres, pagination, tri -- lit la Request directement
public function index(Request $request): JsonResponse {
$produits = Produit::query()
->when($request->filled('categorie'), fn ($q) =>
$q->where('categorie_id', $request->integer('categorie')))
->when($request->filled('recherche'), fn ($q) =>
$q->where('nom', 'like', '%' . $request->string('recherche') . '%'))
->orderBy($request->string('tri', 'nom'))
->paginate($request->integer('par_page', 20));
return response()->json($produits);
}
// show : injection automatique du modèle via route model binding
public function show(Produit $produit): JsonResponse {
return response()->json($produit->load('categorie', 'avis'));
}
// store : création avec FormRequest dédié pour la validation (voir leçon suivante)
public function store(Request $request): JsonResponse {
$donnees = $request->validate([
'nom' => ['required', 'string', 'max:120'],
'prix' => ['required', 'numeric', 'min:0'],
'categorie_id' => ['required', 'exists:categories,id'],
]);
$produit = Produit::create($donnees);
return response()->json($produit, 201);
}
public function update(Request $request, Produit $produit): JsonResponse {
$produit->update($request->validate([
'nom' => ['sometimes', 'string', 'max:120'],
'prix' => ['sometimes', 'numeric', 'min:0'],
]));
return response()->json($produit);
}
public function destroy(Produit $produit): \Illuminate\Http\Response {
$produit->delete();
return response()->noContent();
}
}
// Accès typé aux données de la requête
class RechercheController extends Controller {
public function recherche(Request $request): JsonResponse {
$q = $request->string('q')->trim(); // Stringable typé
$page = $request->integer('page', 1); // cast int avec défaut
$inclureArchives = $request->boolean('archives'); // cast bool ('1', 'true', 'on' -> true)
$tags = $request->array('tags'); // cast tableau
// Upload de fichier
if ($request->hasFile('image')) {
$chemin = $request->file('image')->store('produits', 'public');
}
return response()->json(compact('q', 'page', 'inclureArchives', 'tags'));
}
}Résumé
$request->validate([...])valide et renvoie directement les données propres.when()sur un query builder évite lesifimbriqués pour construire une requête conditionnelle.- Les accesseurs typés (
->string(),->integer(),->boolean()) évitent les casts manuels fragiles. - Retourner des codes HTTP explicites (
201,noContent()= 204) plutôt que toujours200.
Exercices pratiques
Mission : reparer un filtre de recherche qui ignore les champs vides
Objectif : Corriger un controleur d'index qui traite incorrectement les filtres vides et renvoie des codes HTTP incoherents.
Contexte
Le controleur ProduitController::index() d'une API utilise $request->has('categorie') pour decider d'ajouter un filtre where('categorie_id', ...). Un client mobile envoie systematiquement le champ categorie meme vide (categorie=) dans son formulaire de recherche, ce qui declenche un filtre sur une categorie vide et renvoie zero resultat au lieu de tous les produits. Par ailleurs, store() et destroy() renvoient tous deux un code 200, ce qui empeche le client mobile de distinguer une creation reussie d'une simple confirmation.