Module 2Programmer avec Laravel
Chapitre 6 sur 17
Manipulation des contrôleurs
Contrôleurs de base, à action unique, de ressources, middlewares et injection de dépendances.
- ≈ 160 min
- 6 leçons
- 1 vidéo
- 4 exercices
Module 2Programmer avec Laravel
Chapitre 6 sur 17
Contrôleurs de base, à action unique, de ressources, middlewares et injection de dépendances.
Objectifs
Avec ce chapitre, tu sauras…
Leçon 1
Écrire toute la logique dans des closures de routes/web.php devient vite illisible : le fichier grossit, rien n'est réutilisable, et le cache des routes est moins efficace. Un contrôleur est une classe qui regroupe la logique de traitement des requêtes liées à une même ressource. Par exemple, un LivreController gère l'affichage, la création, la modification et la suppression des livres.
| Closure dans la route | Contrôleur |
|---|---|
| pratique pour un test ou une page très simple | adapté à toute vraie fonctionnalité |
tout dans routes/web.php | une classe par ressource dans app/Http/Controllers |
| pas de réutilisation | méthodes, dépendances et middlewares partagés |
Le fichier de routes ne fait plus que l'aiguillage ; le contrôleur fait le traitement (rôle C de MVC).
Leçon 2
Un contrôleur de base
php artisan make:controller CatalogueController<?php
namespace App\Http\Controllers;
use App\Models\Livre;
use Illuminate\View\View;
class CatalogueController extends Controller
{
/**
* Affiche la liste des livres.
*/
public function index(): View
{
return view('catalogue.index', [
'livres' => Livre::orderBy('titre')->get(),
]);
}
/**
* Affiche la fiche d'un livre.
*/
public function show(string $slug): View
{
return view('catalogue.show', [
'livre' => Livre::where('slug', $slug)->firstOrFail(),
]);
}
}use App\Http\Controllers\CatalogueController;
Route::get('/livres', [CatalogueController::class, 'index'])->name('livres.index');
Route::get('/livres/{slug}', [CatalogueController::class, 'show'])->name('livres.show');Quand une requête correspond à GET /livres/le-petit-prince, Laravel crée une instance de CatalogueController et appelle sa méthode show, en lui passant le paramètre de route $slug. Les requêtes Eloquent (Livre::...) fonctionneront dès que la table et le modèle existeront (chapitres 10 et 11).
Le contrôleur étend App\Http\Controllers\Controller, la classe de base du projet. Depuis Laravel 11, cette classe est vide : on peut y ajouter des méthodes partagées par tous les contrôleurs. Un contrôleur n'est d'ailleurs pas obligé de l'étendre.
Un contrôleur à action unique
Quand une action est complexe, on peut lui consacrer une classe entière, avec une seule méthode __invoke :
php artisan make:controller ExporterCatalogue --invokable<?php
namespace App\Http\Controllers;
use App\Models\Livre;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ExporterCatalogue extends Controller
{
public function __invoke(): StreamedResponse
{
return response()->streamDownload(function () {
echo "isbn;titre;prix\n";
foreach (Livre::orderBy('titre')->cursor() as $livre) {
echo "{$livre->isbn};{$livre->titre};{$livre->prix}\n";
}
}, 'catalogue.csv');
}
}use App\Http\Controllers\ExporterCatalogue;
Route::get('/admin/export', ExporterCatalogue::class)->name('admin.export'); // pas de nom de méthodeLeçon 3
Leçon 4
| Verbe | URI | Action | Nom de la route | Rôle |
|---|---|---|---|---|
| GET | /livres | index | livres.index | lister |
| GET | /livres/create | create | livres.create | afficher le formulaire de création |
Leçon 5
Leçon 6
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
1Une route vers une méthode de contrôleur s'écrit…
2Un contrôleur à action unique contient la méthode…
3Route::resource('livres', …) crée…
4La route livres.edit correspond à…
5Route::apiResource exclut les actions…
6En Laravel 11+, $this->middleware('auth') dans le constructeur…
7Route::resource('categories', …) crée le paramètre…
8Avec update(Request $request, string $id), les paramètres de route se placent…
L'essentiel du chapitre
app/Http/Controllers) ; la route ne fait que l'aiguillage : [Controleur::class, 'methode']. Créer : php artisan make:controller Nom ; sous-dossier : Admin/Nom.__invoke, --invokable, route Route::post('/x', Classe::class).implements HasMiddleware + public static function middleware(): array (new Middleware('x', only: [...], except: [...])) ; Laravel 13 : #[Middleware]. Plus de $this->middleware().--resource --model=Livre --requests ; Route::resource('livres', …) → 7 routes : index, create, store, show, edit, update, destroy (livres.index…). Options : only, except, apiResource / --api (sans create/edit), imbriquées livres.avis, shallow, scoped, names, parameters (piège categories → {category}), missing, Route::resourceVerbs, Route::singleton. Routes supplémentaires avant Route::resource.public function __construct(protected PanierService $panier) {}) ou par la méthode (Request $request) ; paramètres de route après les dépendances.| POST |
/livres |
store |
livres.store |
| enregistrer le nouveau livre |
| GET | /livres/{livre} | show | livres.show | afficher un livre |
| GET | /livres/{livre}/edit | edit | livres.edit | afficher le formulaire de modification |
| PUT/PATCH | /livres/{livre} | update | livres.update | enregistrer les modifications |
| DELETE | /livres/{livre} | destroy | livres.destroy | supprimer |
| Commande | Effet |
|---|---|
make:controller LivreController --resource | les 7 méthodes, avec des paramètres string $id |
... --resource --model=Livre | paramètres typés Livre $livre |
... --resource --model=Livre --requests | génère aussi StoreLivreRequest et UpdateLivreRequest (validation, chapitre 7) |
... --api | contrôleur d'API : 5 méthodes, sans create ni edit |
make:model Livre -mcr | le modèle, sa migration et un contrôleur de ressources |
| Verbe | URI | Action | Nom |
|---|---|---|---|
| GET | /livres/{livre}/avis | index | livres.avis.index |
| GET | /livres/{livre}/avis/create | create | livres.avis.create |
| POST | /livres/{livre}/avis | store | livres.avis.store |
| GET | /avis/{avi} | show | avis.show |
| GET | /avis/{avi}/edit | edit | avis.edit |
| PUT/PATCH | /avis/{avi} | update | avis.update |
| DELETE | /avis/{avi} | destroy | avis.destroy |
Écris : (1) la commande qui crée Admin\AuteurController avec les sept méthodes typées par le modèle Auteur et les classes de validation ; (2) la route de ressource dans le groupe admin ; (3) la route de ressource du site public CatalogueController limitée à index et show, avec le paramètre {livre} remplacé par {slug} ; (4) la ressource imbriquée peu profonde des commandes d'un client.
Réécris ce contrôleur Laravel 9 pour Laravel 13 (deux versions : avec HasMiddleware, puis avec les attributs) :
class CommandeController extends Controller
{
protected $paiement;
public function __construct(PaiementService $paiement)
{
$this->paiement = $paiement;
$this->middleware('auth');
$this->middleware('role:admin')->only('destroy');
}
}Le plus simple reste d'affecter le middleware sur la route :
Route::get('/profil', [ProfilController::class, 'show'])->middleware('auth');On peut aussi le déclarer dans le contrôleur : le contrôleur implémente l'interface HasMiddleware et sa méthode statique middleware() renvoie la liste. Les options only et except limitent le middleware à certaines méthodes.
<?php
namespace App\Http\Controllers;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
class CommandeController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
'auth', // toutes les méthodes
new Middleware('verified', only: ['store']), // seulement store
new Middleware('role:admin', except: ['index', 'show']),
];
}
// index(), show(), store(), destroy()...
}Un middleware peut aussi être une closure :
use Closure;
use Illuminate\Http\Request;
public static function middleware(): array
{
return [
function (Request $request, Closure $next) {
logger('Accès aux commandes', ['user' => $request->user()?->id]);
return $next($request);
},
];
}En Laravel 13, on peut écrire la même chose avec des attributs PHP, sur la classe ou sur une méthode :
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
#[Middleware('role:admin', except: ['index', 'show'])]
class CommandeController extends Controller
{
#[Middleware('verified')]
public function store() { /* ... */ }
}Le résumé déclare les middlewares dans le constructeur avec $this->middleware('auth'). Cette méthode venait de l'ancienne classe de base Controller. Depuis Laravel 11, la classe de base est vide : $this->middleware() provoque l'erreur « Call to undefined method ». On utilise l'interface HasMiddleware (Laravel 11 à 13) ou l'attribut #[Middleware] (Laravel 13).
Pour chaque « ressource » (livres, auteurs, commandes), on fait presque toujours les mêmes opérations : lister, afficher, créer, modifier, supprimer (CRUD). Laravel génère le contrôleur et les sept routes en deux lignes.
php artisan make:controller Admin/LivreController --resource --model=LivreLe nom Admin/LivreController crée la classe dans app/Http/Controllers/Admin, avec l'espace de noms App\Http\Controllers\Admin. L'option --model=Livre type les paramètres avec le modèle (liaison implicite).
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Livre;
use Illuminate\Http\Request;
class LivreController extends Controller
{
/** Display a listing of the resource. */
public function index() { /* liste des livres */ }
/** Show the form for creating a new resource. */
public function create() { /* formulaire de création */ }
/** Store a newly created resource in storage. */
public function store(Request $request) { /* enregistrer */ }
/** Display the specified resource. */
public function show(Livre $livre) { /* afficher un livre */ }
/** Show the form for editing the specified resource. */
public function edit(Livre $livre) { /* formulaire de modification */ }
/** Update the specified resource in storage. */
public function update(Request $request, Livre $livre) { /* mettre à jour */ }
/** Remove the specified resource from storage. */
public function destroy(Livre $livre) { /* supprimer */ }
}use App\Http\Controllers\Admin\LivreController;
Route::prefix('admin')->name('admin.')->middleware('auth')->group(function () {
Route::resource('livres', LivreController::class);
});Les actions gérées par un contrôleur de ressources
Pour Route::resource('livres', ...), sans le préfixe admin :
Tous les contrôleurs sont créés par le conteneur de services de Laravel. Il peut donc fournir automatiquement toutes les classes dont le contrôleur a besoin : il suffit de les typer.
L'injection par le constructeur
<?php
namespace App\Services;
use Illuminate\Support\Facades\Session;
class PanierService
{
public function ajouter(int $livreId, int $quantite = 1): void
{
$panier = Session::get('panier', []);
$panier[$livreId] = ($panier[$livreId] ?? 0) + $quantite;
Session::put('panier', $panier);
}
public function nombreArticles(): int
{
return array_sum(Session::get('panier', []));
}
}<?php
namespace App\Http\Controllers;
use App\Services\PanierService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PanierController extends Controller
{
public function __construct(
protected PanierService $panier, // fourni automatiquement par Laravel
) {}
public function store(Request $request): JsonResponse
{
$this->panier->ajouter($request->integer('livre_id'), $request->integer('quantite', 1));
return response()->json(['nb_articles' => $this->panier->nombreArticles()]);
}
}On n'écrit jamais new PanierService() : Laravel crée l'objet et le passe au constructeur. Le contrôleur ne dépend que d'une classe, qu'on peut remplacer par une fausse version dans les tests.
L'injection par la méthode
On peut aussi typer une dépendance dans une méthode. Le cas le plus courant : la requête Request. Si la méthode reçoit aussi des paramètres de route, on les place après les dépendances :
// Route::put('/livres/{id}', [LivreController::class, 'update']);
public function update(Request $request, string $id): RedirectResponse
{
$titre = $request->input('titre');
// ...
return redirect()->route('livres.index');
}Le résumé déclare une propriété protected $users; puis l'affecte dans le constructeur. Depuis PHP 8.0, la promotion des propriétés fait les deux en une ligne : public function __construct(protected UserRepository $users) {}. Le résumé contient aussi une commande illisible pour les contrôleurs d'API (« edit--apimake:controller ») : la bonne commande est php artisan make:controller PhotoController --api.
Modifier, mettre à jour et supprimer une ressource : les actions edit, update et destroy, puis le passage aux contrôleurs.
(1) Crée CatalogueController (méthodes index et show) qui, pour l'instant, renvoie les livres du tableau du chapitre 3 (déplace le tableau dans une méthode privée livres()). (2) Crée Admin/LivreController avec --resource ; chaque méthode renvoie un texte (« Liste des livres », « Formulaire de création »…). (3) Déclare les routes (catalogue public + ressource dans le groupe admin protégé par auth et role:admin,libraire). (4) Crée le contrôleur invocable Admin/DashboardController. (5) Traduis les verbes create et edit en ajouter et modifier. (6) Vérifie avec php artisan route:list --path=admin.
Sans regarder le cours, complète le tableau des routes de Route::resource('auteurs', AuteurController::class) : verbe, URI, méthode du contrôleur, nom de la route. Puis indique quelles actions affichent une vue et lesquelles redirigent après leur traitement.
Dans le groupe admin, les URI deviennent /admin/livres… et les noms admin.livres.index, admin.livres.store, etc.
Plusieurs ressources d'un coup :
Route::resources([
'livres' => LivreController::class,
'auteurs' => AuteurController::class,
]);Tableau: Les options de génération
Personnaliser le modèle introuvable
Si le livre lié n'existe pas, Laravel renvoie une 404. La méthode missing permet de faire autre chose, par exemple rediriger vers la liste :
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
Route::resource('livres', LivreController::class)
->missing(fn (Request $request) => Redirect::route('admin.livres.index'));Les routes de ressources partielles
Route::resource('livres', CatalogueController::class)->only(['index', 'show']); // site public
Route::resource('commandes', CommandeController::class)->except(['edit', 'update']);Les routes de ressources d'API
Une API ne renvoie pas de formulaires HTML : create et edit sont inutiles. apiResource les exclut :
Route::apiResource('livres', Api\LivreController::class); // 5 routes
Route::apiResources([
'livres' => Api\LivreController::class,
'auteurs' => Api\AuteurController::class,
]);Les ressources imbriquées
Quand une ressource appartient à une autre (les avis d'un livre), on utilise la notation point :
Route::resource('livres.avis', AvisController::class);
// GET /livres/{livre}/avis/{avi} ...Les méthodes du contrôleur reçoivent les deux paramètres : show(Livre $livre, Avis $avi). L'imbrication peu profonde (shallow) garde le parent seulement là où il est utile (lister, créer) :
Route::resource('livres.avis', AvisController::class)->shallow();La méthode scoped active la liaison délimitée et choisit la colonne de l'enfant : ->scoped(['avi' => 'id']) vérifie que l'avis appartient bien au livre.
Nommer les routes et les paramètres
Route::resource('livres', LivreController::class)->names([
'create' => 'livres.ajouter', // renomme une route
]);
Route::resource('categories', CategorieController::class)->parameters([
'categories' => 'categorie', // {category} devient {categorie}
]);Traduire les verbes des URI
Par défaut, les URI contiennent les verbes anglais create et edit. Pour les traduire, on appelle Route::resourceVerbs au début de la méthode boot de AppServiceProvider :
use Illuminate\Support\Facades\Route;
public function boot(): void
{
Route::resourceVerbs([
'create' => 'ajouter',
'edit' => 'modifier',
]);
}
// /livres/ajouter et /livres/{livre}/modifier (les noms de routes restent livres.create et livres.edit)Compléter un contrôleur de ressources
Pour ajouter une route en plus des sept (par exemple les livres populaires), on la déclare avant Route::resource, sinon /livres/populaires serait capturée par /livres/{livre} :
Route::get('/livres/populaires', [LivreController::class, 'populaires'])->name('livres.populaires');
Route::resource('livres', LivreController::class);Le contrôleur de ressource singleton
Certaines ressources n'existent qu'en un seul exemplaire pour l'utilisateur : son profil, ses préférences. Route::singleton crée les routes sans identifiant :
Route::singleton('profil', ProfilController::class);
// GET /profil (show), GET /profil/edit (edit), PUT/PATCH /profil (update)