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.
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 dename: 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éthodefindPublishedBySlugdu 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
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.
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éthodeshowActionau milieu derssAction,commentActionetdeleteAction. 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.