Retour au cours

backend / php-laravel

Contrôleurs et requêtes HTTP

Leçon 91 exercice

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 validationVérifie
requiredLe champ est présent et non vide
numericLe champ est un nombre
exists:categories,idLa valeur existe dans la table categories
max:120La 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
<?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 les if imbriqué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 toujours 200.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →