Aller au contenu principal

Arrêtez les God Controllers, passez à l'ADR

Transformez vos contrôleurs monolithiques en Actions atomiques. Guide complet sur le pattern Action-Domain-Responder.
Catégorie

PHP

Fonctionnalités du langage, évolutions récentes et techniques avancées en PHP 8.x.

Lecture
4 min
Niveau
Intermédiaire
mars 22 2025
Partager

Le Controller est souvent le maillon faible d'une application Symfony.

Historiquement, on nous a appris à regrouper toutes les méthodes liées à une entité dans une même classe : ProductController contient index, new, edit, delete.

Résultat ? Une classe de 500 lignes, avec 15 dépendances injectées dans le constructeur, dont la moitié ne sont utilisées que par une seule méthode. C'est ce qu'on appelle un "God Controller" (ou Controller Fourre-Tout).

Il y a une meilleure voie. Une voie architecturale qui aligne votre code avec le protocole HTTP lui-même : le pattern ADR (Action-Domain-Responder).

La philosophie ADR

Le principe est simple : Une Classe = Une Route = Une Action.

Au lieu d'avoir un couteau suisse qui fait tout mal, vous avez un jeu de bistouris extrêmement précis.

Dans Symfony, cela se traduit par des contrôleurs invokables (qui n'ont qu'une seule méthode __invoke).

Avantages immédiats

  • Exploration : Vous cherchez le code qui affiche un article ? CTRL+P -> PostAction. Vous tombez directement dessus. Pas besoin de scroller dans un fichier de 1000 lignes.
  • Injection de Dépendances : Votre classe n'injecte que ce dont elle a besoin pour cette action précise. Fini les constructeurs obèses.
  • Responsabilité Unique : La classe ne fait qu'une seule chose, et elle le fait bien.

Étude de cas : PostAction.php

Voici le fichier src/Controller/Content/PostAction.php tel qu'il est en production.

PHP
namespace App\Controller\Content;

use App\Entity\Content\Post;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\Cache;
use Symfony\Component\Routing\Attribute\Route;

final class PostAction extends AbstractController
{
    #[Cache(maxage: 900, public: true, mustRevalidate: true)]
    #[Route(
        path: '/billets/{slug}',
        name: self::class,
        requirements: ['slug' => '^[a-z0-9-]+$'],
        methods: ['GET']
    )]
    #[Template(template: self::class.'.html.twig')]
    public function __invoke(
        #[MapEntity(expr: 'repository.findPublishedBySlug(slug)')]
        Post $post,
        PostNavigationService $postNavigationService
    ): array {
        return [
            'post' => $post,
            'previousPost' => $postNavigationService->getPreviousPost($post),
            'nextPost' => $postNavigationService->getNextPost($post),
        ];
    }
}

Analysons cette classe ligne par ligne.

Attributs vs logique

Regardez le corps de la méthode __invoke. Il ne contient aucune logique technique. Tout ce qui relève du framework a été déplacé dans des attributs déclaratifs.

  • #[Route] : Définit l'URL. Notez l'usage de name: self::class. Le nom de la route est le FQCN de la classe (App\Controller\Content\PostAction). C'est unique par défaut.
  • #[Cache] : Gère les headers HTTP de cache (Cache-Control). Pas besoin d'écrire $response->setSharedMaxAge().
  • #[Template] : Retourner un tableau de variables suffit. Symfony devine le template (qui porte le même nom que la classe) et fait le rendu.
  • #[MapEntity] : C'est la magie. L'attribut dit à Symfony : "Prends le slug dans l'URL, appelle la méthode findPublishedBySlug du repository, et si tu ne trouves rien, lance une 404".

Le contrôleur ne fait plus le travail. Il coordonne.

Typage fort et injection ciblée

PHP
public function __invoke(
    Post $post,
    PostNavigationService $postNavigationService
): array

J'injecte le service PostNavigationService directement dans la méthode. Si nous étions dans un "God Controller", ce service aurait été injecté dans le constructeur, polluant toutes les autres méthodes qui n'en ont pas besoin (comme create ou delete).

Ici, la dépendance est claire, locale et justifiée.

L'exemple CategoryAction : Gestion des Erreurs

Parfois, l'attribut #[MapEntity] ne suffit pas et on doit gérer des cas limites manuellement. Voici un extrait de src/Controller/Content/CategoryAction.php.

PHP
public function __invoke(
    #[MapEntity(expr: 'repository.findOneBy({"slug": slug, "status": "online"})')]
    Category $category,
): array {
    $totalPosts = $this->postRepository->countByCategory($category);

    // Règle métier : On ne veut pas afficher une catégorie vide
    if ($totalPosts === 0) {
        throw $this->createNotFoundException();
    }

    // ...
}

Même ici, le controller reste très mince. Il vérifie une condition métier (catégorie vide), et déclenche une exception standard.

Pourquoi adopter l'ADR ?

Ce n'est pas juste une question d'esthétique. C'est une question de maintenabilité.

Imaginez que vous deviez modifier la logique d'affichage des billets de blog.

  • Ancienne méthode : Vous ouvrez BlogController.php (1200 lignes), vous cherchez la méthode showAction au milieu de rssAction, commentAction et deleteAction. Vous avez peur de casser quelque chose d'autre en modifiant une propriété privée partagée.
  • Méthode ADR : Vous ouvrez PostAction.php. Vous avez 30 lignes de code sous les yeux. Vous savez que tout ce qui est dans ce fichier ne concerne QUE l'affichage d'un billet. Vous modifiez en toute confiance.

Le mot de la fin

L'architecture logicielle n'est pas là pour faire joli. Elle est là pour réduire la charge cognitive du développeur.

Le pattern ADR transforme votre dossier src/Controller en une liste claire de fonctionnalités de votre application, plutôt qu'une liste de concepts techniques.

Adoptez les classes invokables. Utilisez les attributs. Laissez le framework travailler pour vous.

Poursuivre la lecture

Sélectionné avec soin pour vous.

Symfony

Twig Components : la puissance du Backend dans le Frontend

Arrêtez les macros Twig et les includes. Passez aux Composants Twig : Classes PHP typées, logique encapsulée et réutilisabilité maximale.

4 min de lecture
PHP

Les DTOs : arrêtez de transformer vos données en kebab

Découvrez pourquoi les DTOs sont essentiels pour des données bien rangées. Promis, pas de sauce blanche dans le code !

4 min de lecture
Symfony

L'injection de dépendance, ou comment être fainéant avec élégance

Découvrez comment Symfony vous aide à coder sans effort avec l'injection de dépendance. Dit adieu aux 'new' et bonjour à l'autowiring!

9 min de lecture