Implémentez le TaskController complet de l’application fil rouge.
Entre la route et la réponse, une décision doit être prise.
Dans le chapitre précédent, vous avez construit la carte HTTP de l’application. Vous savez désormais qu’une requête GET /tasks doit afficher les tâches et qu’une requête POST /tasks doit en créer une. Pourtant, une route ne réalise rien par elle-même : elle reconnaît une intention et transmet l’exécution à une action. Cette action se trouve dans le contrôleur.
Le contrôleur est souvent la première classe dans laquelle un débutant place tout ce qui semble nécessaire : lecture du formulaire, requête SQL, validation, fabrication du HTML, envoi d’un e-mail et gestion des erreurs. Le résultat fonctionne parfois au début, puis devient rapidement impossible à comprendre. Une modification casse une autre action, les tests demandent une base réelle et chaque méthode grandit jusqu’à cacher le scénario métier.
L’objectif de ce chapitre n’est donc pas seulement de mémoriser sept noms de méthodes. Vous allez apprendre à considérer le contrôleur comme une frontière. À gauche se trouve le monde HTTP, avec des méthodes, des paramètres, des en-têtes et des données non fiables. À droite se trouve l’application, avec ses modèles, ses règles et ses services. Le contrôleur traduit proprement l’un vers l’autre.
Notre application de tâches doit lister, afficher, créer, modifier, terminer et supprimer une tâche. Chaque opération doit produire une réponse correcte, refuser les données invalides, protéger les tâches d’un autre utilisateur et rester facile à tester.
À la fin de ce chapitre, vous saurez
- expliquer précisément le rôle d’un contrôleur MVC
- associer une route à une action CRUD cohérente
- lire et valider une Request sans utiliser directement les superglobales
- choisir entre vue, redirection, JSON et réponse d’erreur
- séparer orchestration, persistance et règles métier
- écrire des actions suffisamment claires pour être testées
Méthode de travail
Avant d’écrire une action, racontez son scénario.
Prenons store(). L’utilisateur envoie un formulaire. L’application doit d’abord identifier les champs acceptés, vérifier leur validité, demander au modèle de créer la tâche, puis informer le navigateur du résultat. Cette phrase contient déjà les quatre étapes de l’action : recevoir, valider, exécuter et répondre.
Cette méthode évite de commencer par du code sans savoir où l’on va. Pour chaque action, écrivez d’abord le résultat attendu et les échecs possibles. show() peut réussir ou ne pas trouver la tâche. update() peut aussi recevoir une donnée invalide ou un utilisateur non autorisé. destroy() doit confirmer la suppression sans rendre un ancien objet devenu inexistant.
Les questions à poser pour chaque action
- Quelle donnée arrive de la route, du formulaire ou de la session ?
- Quelles données sont autorisées et quelles règles doivent être vérifiées ?
- Quel modèle ou service porte réellement l’opération ?
- Que se passe-t-il si la ressource est absente ou l’action interdite ?
- Quelle réponse permettra au client de comprendre le résultat ?
Gardez cette grille à côté de vous pendant tout le chapitre. Elle est plus importante que la syntaxe exacte, car elle continuera de fonctionner lorsque votre projet utilisera une API, AML View ou PHPAML Data.
Le contrôleur orchestre
Le contrôleur transforme une requête HTTP en cas d’usage puis en réponse. Il reçoit Request, coordonne un modèle ou un service, et retourne une vue, une redirection ou du JSON.
SQL, HTML, règles métier complexes et appels externes dans la même action produisent un contrôleur géant. Une action courte doit raconter un scénario complet et lisible.
Le contrôleur dirige; modèles et services exécutent le métier.
Les actions CRUD
Une ressource utilise index, create, store, show, edit, update et destroy. index liste; create affiche le formulaire; store traite sa soumission.
show affiche une tâche; edit prépare sa modification; update l’enregistre; destroy la supprime. Ce vocabulaire relie naturellement routes et contrôleurs.
Une action poursuit un seul résultat observable.
Request et validation
Request rassemble paramètres, corps, fichiers, en-têtes et utilisateur sans dépendre directement de $_POST. Une entrée navigateur reste non fiable, même avec required en HTML.
Sélectionnez les champs permis, normalisez, puis validez présence, type, format et longueur. En Web, renvoyez les erreurs; en API, utilisez 422. N’écrivez jamais avant validation.
Seules les données autorisées et validées atteignent le modèle.
$data = $request->validate([
'title' => ['required', 'string', 'max:120'],
]);Rendre une vue
view() définit un contrat privé : des noms explicites et uniquement les données nécessaires. Chargez-les avant le rendu afin d’éviter les requêtes surprises dans les templates.
Le contrôleur choisit quoi afficher; la vue décide comment. Le HTML, l’échappement, les boucles et partials appartiennent à la présentation.
Le contrôleur prépare; la vue présente.
Rediriger après les formulaires
Post/Redirect/Get redirige une mutation POST, PATCH ou DELETE vers une page GET. L’actualisation ne resoumet plus le formulaire et l’URL finale devient partageable.
Un message flash confirme le résultat une seule fois. Une API répond plutôt 201 après création; le Web redirige vers la liste ou la fiche.
Une mutation Web réussie finit généralement par une redirection.
JSON, erreurs et statuts
HTML et JSON partagent le métier, pas nécessairement la représentation. Utilisez 200 pour lire, 201 pour créer, 204 pour supprimer, 404 si absent et 403 si interdit.
Ne sérialisez jamais automatiquement toutes les propriétés du modèle. Construisez une ressource publique stable et excluez secrets et champs internes.
Le statut et le corps racontent le même résultat.
return json(['data' => $tasks], 200);Injection et tests
L’injection rend repository, horloge ou service remplaçables et empêche leur création cachée au milieu d’une action.
Testez statut, vue, données, redirection et changement persistant. Couvrez succès, champ vide, tâche absente et accès refusé sans figer chaque détail interne.
Une dépendance injectée documente aussi le besoin du contrôleur. Le constructeur devient la liste honnête de ses collaborateurs, ce qui facilite la lecture et révèle rapidement une classe qui en fait trop.
Un contrôleur mince possède des dépendances visibles et des scénarios testables.
Paramètres de route et ressources absentes
L’identifiant fourni par /tasks/{id} sélectionne une ressource, mais sa présence dans l’URL ne garantit ni sa validité ni son existence. La contrainte de route contrôle la forme; le contrôleur traite l’absence.
Convertissez le paramètre vers le type attendu et recherchez la tâche une seule fois. Si elle n’existe pas, interrompez immédiatement le scénario avec une réponse 404 au lieu de laisser null circuler.
Une API doit conserver une erreur structurée et stable. Une application Web peut rendre sa page 404, tout en utilisant le même statut afin que navigateur, cache et moteurs comprennent la réponse.
Échouez tôt et précisément lorsqu’une ressource demandée n’existe pas.
$task = Task::find($id);
if ($task === null) {
return response('Task not found', 404);
}Autorisation et sécurité
Être connecté ne signifie pas pouvoir modifier toutes les tâches. L’authentification identifie l’utilisateur; l’autorisation décide si cette personne peut exécuter l’action sur cette ressource.
Appliquez le middleware commun sur le groupe de routes, puis vérifiez la propriété ou la permission liée à la tâche. Une réponse 403 indique que l’identité est connue mais que l’action est refusée.
Ne faites pas confiance à user_id envoyé dans le formulaire. Utilisez l’identité sûre de la session et empêchez l’affectation massive de champs sensibles.
Valider répond “la donnée est-elle correcte ?”; autoriser répond “qui peut agir ?”.
$this->authorize('update', $task);
$task->update($validated);Transactions et effets secondaires
Certaines actions modifient plusieurs données : créer une tâche, ajouter un historique et mettre à jour un compteur. Sans transaction, une panne intermédiaire laisse un état partiel.
Placez l’opération cohérente dans un service métier transactionnel. Le contrôleur appelle ce cas d’usage puis traduit son résultat; il ne gère pas lui-même les détails de commit et rollback.
Les e-mails et appels distants demandent une stratégie distincte. Évitez de conserver une transaction SQL ouverte pendant un réseau lent; enregistrez l’intention puis exécutez l’effet de manière contrôlée.
Le contrôleur délimite le scénario; le service garantit sa cohérence métier.
Contrôleur Web ou contrôleur API
Un même cas d’usage peut servir deux interfaces, mais leurs entrées et sorties diffèrent. Le Web travaille avec session, CSRF, formulaires, vues et redirections; l’API avec jetons, JSON et statuts explicites.
Lorsque le projet grandit, séparer TaskController et Api/TaskController évite les conditions répétées du type “si JSON”. Les deux appellent le même service et choisissent leur propre Response.
Versionnez le contrat API lorsque des clients externes en dépendent. Une modification de template Web ne devrait jamais casser l’application mobile qui consomme /api/v1/tasks.
Partagez le cas d’usage, spécialisez le contrat de transport.
Construire une vraie stratégie de test
Un test heureux ne suffit pas. Pour store, vérifiez création valide, titre vide, titre trop long, champ inattendu, utilisateur non connecté et utilisateur sans permission.
Pour show, couvrez ressource existante et absente. Pour update, ajoutez une modification concurrente. Pour destroy, confirmez le statut ou la redirection ainsi que l’absence réelle de la tâche.
Testez l’interface observable plutôt que l’ordre de chaque appel privé. Le test doit rester vert lorsque l’implémentation est améliorée, mais devenir rouge dès que le contrat utilisateur est rompu.
Chaque branche importante du scénario mérite une preuve reproductible.
$response = $this->post('/tasks', ['title' => '']);
$response->assertStatus(422);Exemple complet
TaskController
Suivez entrée, validation, modèle et réponse : chaque ligne possède une responsabilité identifiable.
final class TaskController
{
public function store(Request $request): Response
{
$data = $request->validate([
'title' => ['required', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:1000'],
]);
Task::create($data);
return redirect(route('tasks.index'))
->with('success', 'Task created.');
}
public function show(int $id): Response
{
$task = Task::find($id);
if ($task === null) return response('Not found', 404);
return view('tasks/show', ['task' => $task]);
}
}Atelier guidé
Terminez le contrôleur CRUD.
- Implémentez les sept actions CRUD.
- Validez title et description.
- Gérez 404 et 403.
- Appliquez Post/Redirect/Get.
- Ajoutez une réponse JSON sûre.
- Testez succès, 422, 404 et 403.
Correction raisonnée
Vérifiez chaque frontière.
La route sélectionne, Request transporte, la validation sécurise, le modèle exécute et Response exprime le résultat. Cette chaîne rend chaque défaut localisable.
Pourquoi aucun PDO ici ?
Le stockage appartient au modèle afin que le contrôleur reste indépendant de SQLite, SQL ou MongoDB.
En résumé
Un bon contrôleur rend le parcours de la requête évident.
Vous avez commencé ce chapitre avec une route qui savait reconnaître une URL, mais ne savait pas encore accomplir le scénario. Vous disposez maintenant d’une couche capable de recevoir la requête, protéger l’entrée, appeler le bon comportement et produire une réponse HTTP cohérente.
La qualité d’un contrôleur ne se mesure pas au nombre d’opérations qu’il sait effectuer. Elle se mesure à la facilité avec laquelle un autre développeur peut suivre le scénario. Une action réussie se lit presque comme une liste : trouver la tâche, vérifier l’autorisation, valider la saisie, demander la modification, puis rediriger. Aucun détail technique ne doit masquer cette histoire.
Les points essentiels
- une route choisit l’action, mais le contrôleur orchestre le scénario
- toute entrée externe doit être limitée et validée
- une réponse doit avoir un statut, un contenu et une intention cohérents
- la persistance et les règles métier ne doivent pas envahir le contrôleur
- les échecs attendus font partie du contrat et doivent être testés
Erreurs fréquentes à éviter
Ne lisez pas directement $_POST dans chaque action. Ne faites pas confiance à un identifiant d’utilisateur envoyé par le formulaire. Ne retournez pas systématiquement 200, même lorsqu’une ressource est absente. Ne dupliquez pas les règles métier dans les contrôleurs Web et API. Enfin, ne rendez pas une vue directement après POST si une redirection peut empêcher la resoumission.
Sans regarder les exemples, pouvez-vous expliquer les quatre étapes d’une action, choisir le statut d’une ressource absente, justifier Post/Redirect/Get et dire quand extraire un service ? Si oui, vous êtes prêt à construire la présentation.
La suite du parcours
Le contrôleur possède maintenant les données et sait choisir une réponse. Dans le chapitre 6, nous allons construire les vues et les partials qui transformeront ces données en pages HTML lisibles, accessibles et sûres. Vous verrez comment transmettre les variables, échapper les sorties, afficher les erreurs de validation et réutiliser header et footer.