Les Controllers Symfony : quand la complexité rencontre l'élégance

Temps de lecture : 9 min

Découvrez comment Symfony transforme la complexité en élégance avec les controllers et le pattern ADR. Un voyage technique inoubliable.

Les paradigmes de Symfony : ou comment faire simple quand on peut faire compliqué

MVC, mais pas trop quand même

Symfony se présente comme un framework MVC (Model-View-Controller), cette architecture que tous les devs prétendent comprendre mais que personne n'implémente correctement. Le principe c'est :

ADR : Action-Domain-Responder, parce-que MVC ça fait trop mainstream

Depuis quelques années, un nouveau pattern fait son chemin : Action-Domain-Responder (ADR). Ce pattern est comme le MVC, mais avec un nom plus compliqué pour impressionner lors des entretiens d'embauche.

L'implémentation : les Controllers suffixés Action

J'ai eu une révélation en lisant cet excellent article de COil🐝. L'idée de l'utilisation du pattern ADR n'est pas de moi et c'est manière de faire fait partie des découvertes qui me font aimer mon métier : apprendre sans cesse.

Cette brillante approche épurée permet une implémentation qui ferait pleurer de joie les puristes de l'ADR : les controllers suffixés Action : c'est comme donner des noms de fonction à tes chiens : "Assis", "Couché", "GèreLaDemandeHTTP".

HomeAction : L'action qui te souhaite la bienvenue (ou pas)

Alors en pratique ça donne ça :

// src/Controller/HomeAction.php
namespace App\Controller;

use App\Entity\Content\Page;
use App\Enum\Content\PageTemplateEnum;
use App\Repository\Content\PageRepository;
use App\Repository\Content\PostRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class HomeAction extends AbstractController
{
    public function __construct(
        private readonly PageRepository $pageRepository,
        private readonly PostRepository $postRepository,
    ) {
    }

    #[Route(
        path: '/',
        name: self::class,
        methods: ['GET'],
    )]
    public function __invoke(): Response
    {
        return $this->render(self::class.'.html.twig', [
            'page' => $this->getHomePage(),
            'posts' => $this->postRepository->findAllVisible(limit: 4),
        ]);
    }

    private function getHomePage(): Page
    {
        $page = $this->pageRepository->findPageByTemplate(template: PageTemplateEnum::HOME);

        if (!$page instanceof Page) {
            throw $this->createNotFoundException('Page d\'accueil introuvable.');
        }

        return $page;
    }
}

Ce Controller qui fait UNE SEULE CHOSE. C'est comme un député qui tient ses promesses - rare, précieux, et probablement en voie de disparition.

La méthode __invoke() : le héros anonyme que personne ne respecte

Vous remarquerez que j'utilise la méthode magique __invoke(). C'est comme le mec qui fait tout le boulot dans un projet de groupe mais dont personne ne se souvient du nom. Cette méthode transforme ton objet Controller en fonction callable, ce qui signifie que quand Symfony exécute ton controller, il l'appelle directement comme une fonction.

// Dans les coulisses, Symfony fait essentiellement ceci :
$controller = new PostAction($postRepository);
$response = $controller();  // Invoque la méthode __invoke()

Route name = self::class : L'astuce qui te fait passer pour un génie

Tout d'abord le code :

#[Route(
    path: '/billets/{category}/{slug}',
    name: self::class,
    requirements: ['slug' => '^[a-z0-9-]+$', 'category' => '^[a-z0-9-]+$'],
    methods: ['GET'],
)]

Cette petite astuce, utiliser name: self::class que j'ai découvert dans l'artice cité ci-dessus, c'est comme donner à ton chat ton propre nom - narcissique mais pratique pour retrouver qui est responsable. Ça signifie que le nom de la route est EXACTEMENT le même que le FQCN (Fully Qualified Class Name) de ton controller. Donc App\Controller\PostAction devient le nom de ta route.

Pourquoi faire ça ? Parce que après tu peux faire des trucs comme là où avant je me prenais la tête avec des Enums :

return $this->render(self::class.'.html.twig', [...]);

Qui va résoudre automatiquement en App/Controller/PostAction.html.twig. C'EST PAS BEAU ÇA ?

FQCN dans Twig : quand le backend s'invite dans le frontend

Mais l'histoire ne s'arrête pas là !. Dans l'article cité, l'obsession va jusqu'à utiliser ces FQCN pour générer des URLs dans mls templates Twig. Une extension Twig fait donc passer mon niveau d'indirection à celui d'un ministre qui évite une question gênante.

L'extension Twig qui fait tout le boulot

// src/Twig/Extension/RoutingExtension.php
namespace App\Twig\Extension;

use InvalidArgumentException;
use Override;
use Symfony\Component\Routing\RouterInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

use function Symfony\Component\String\u;

final class RoutingExtension extends AbstractExtension
{
    /**
     * @var array<int, string>|null
     */
    private ?array $controllers = null;

    public function __construct(
        private readonly RouterInterface $router,
    ) {
    }

    #[Override]
    public function getFunctions(): array
    {
        return [
            new TwigFunction('ctrl_fqcn', $this->getControllerFqcn(...)),
            // autres fonctions...
        ];
    }

    public function getControllerFqcn(string $ctrlShortname): string
    {
        if ($this->controllers === null) {
            $this->controllers = array_unique(array_map(
                static fn ($value): string => u($value)->trimSuffix('::__invoke')->toString(),
                array_keys($this->router->getRouteCollection()->getAliases()),
            ));
        }

        foreach ($this->controllers as $controller) {
            /** @var class-string $controller */
            if (u($controller)->endsWith($ctrlShortname)) {
                return $controller;
            }
        }

        throw new InvalidArgumentException('No controller found for the "'.$ctrlShortname.'" shortname.');
    }
    
    // autres méthodes...
}

Cette extension permet d'utiliser ctrl_fqcn() dans les templates Twig pour retrouver le FQCN complet d'un contrôleur à partir de son nom court. C'est comme si tu pouvais retrouver l'arbre généalogique complet de quelqu'un juste en connaissant son prénom, mais pour le code.

Comment ça s'utilise dans les templates

{# templates/components/Post.html.twig #}
<article class="group relative focus:outline-none py-8 lg:py-10 px-10 lg:px-14 bg-white overflow-hidden transform transition hover:scale-[1.01] h-full will-change-transform rounded-3xl flex flex-col shadow-xl hover:shadow-2xl border border-gray-100 hover:bg-gradient-to-b hover:to-white hover:from-{{ post.category.color.value }}-100/80"
    data-seo-container>
    <div class="relative max-w-screen-sm mr-4 lg:mr-8 w-full flex-grow flex flex-col">
        {% if legend is not null %}
            <strong class="inline-block heading mb-3">{{ legend }}</strong>
        {% endif %}
        <h3 class="heading text-2xl line-clamp-3">
            <a title="{{ post.title|raw }}"
               data-seo-target
               href="{{ path(ctrl_fqcn('PostAction'), {'slug': post.slug, 'category': post.category.slug}) }}">
                {{ post.title|raw }}
            </a>
        </h3>
        {# ... reste du template ... #}
    </div>
</article>

C'EST PAS MERVEILLEUX ? Au lieu d'écrire :

{{ path('App\\Controller\\PostAction', {'slug': post.slug, 'category': post.category.slug}) }}

Je peux simplement écrire :

{{ path(ctrl_fqcn('PostAction'), {'slug': post.slug, 'category': post.category.slug}) }}

C'est plus court, plus lisible, et ça me donne l'impression d'être intelligent. Que demander de plus ?

Les Avantages de cette approche

1. Une organisation claire comme de l'eau de roche (si l'eau de roche était claire)

Chaque controller fait UNE CHOSE et UNE SEULE. PostAction affiche un post. HomeAction affiche la page d'accueil. SearchAction gère la recherche. C'est tellement explicite que même un stagiaire pourrait comprendre.

2. Un mapping automatique entre contrôleurs et templates

Grâce à self::class.'.html.twig', chaque contrôleur sait exactement quel template utiliser, sans avoir à le spécifier explicitement. C'est comme si ton chien savait naturellement où faire ses besoins, sauf que là ça fonctionne vraiment.

3. Un routing basé sur les noms de classe

Les noms de routes sont directement liés aux noms des classes, ce qui signifie que si tu changes le nom d'une classe, tu changes automatiquement le nom de la route. C'est comme avoir un chien qui change de nom quand tu l'appelles différemment - magique, non ?

4. Une génération d'URLs simplifiée dans Twig

Grâce à la fonction ctrl_fqcn(), générer des URLs dans les templates devient un jeu d'enfant. Plus besoin de se souvenir du nom exact de la route (enfin quand tu aimes souffrir sans utiliser la complétion d'un vrai IDE), juste du nom du controller - qui est généralement assez explicite.

Les inconvénients

1. Un peu de magie noire

L'utilisation de __invoke() et de self::class peut sembler magique et obscure pour les nouveaux développeurs. Mais franchement, si tu ne comprends pas ça, tu ne devrais probablement pas toucher à Symfony.

2. Beaucoup de petits fichiers

Avec un contrôleur par action, tu te retrouves avec beaucoup de petits fichiers. C'est comme avoir 50 paquets de chips avec une seule chips dans chaque paquet. Mais hey, c'est le prix de la modularité !

3. Une dépendance potentielle à l'extension Twig

L'utilisation de ctrl_fqcn() dans les templates crée une dépendance à cette extension Twig personnalisée. Si je décide de changer d'approche, je devrai mettre à jour tous mes templates.

Le mot de la fin

Les controllers suffixés Action avec la méthode __invoke() et l'utilisation des FQCN pour le nommage des routes et le rendu des templates, c'est comme la fusion nucléaire du développement web : compliqué à mettre en place, mais une fois que ça fonctionne, c'est propre, élégant et puissant.

Si tu es encore au stade où tu écris des méthodes indexAction(), showAction() et editAction() dans un seul controller, c'est que t'es resté bloqué en 2010, comme la personne qui porte encore des jeans taille basse en pensant que c'est à la mode.

Embrasse le changement, adopte le pattern ADR, nomme tes controllers avec le suffixe Action, et utilise __invoke(). Tes collègues seront impressionnés, et tu pourras enfin prétendre être un vrai développeur Symfony, pas juste quelqu'un qui copie-colle du code de Stack Overflow.

N'OUBLIE PAS : Un bon controller, c'est comme un bon politicien - il délègue tout le travail et prend tout le crédit. Alors laisse tes services faire le boulot et tes contrôleurs juste coordonner le flux.

Liens utiles

Confidentialité

Ce site utilise Umami pour analyser le trafic de manière anonyme. Acceptez-vous la collecte de données anonymes ?