Une meilleure architecture ADR pour vos contrôleurs Symfony

Publié le 01/11/2024 • Actualisé le 01/11/2024

Cet article montre différentes expériences et essais autour de l'architecture ADR appliquée aux contrôleurs Symfony. C'est parti ! 😎


English language detected! 🇬🇧

  We noticed that your browser is using English. Do you want to read this post in this language?

Read the english version 🇬🇧 Close

Prérequis

Je présumerai que vous avez les connaissances élémentaires de Symfony et que vous savez ce qu'est un contrôleur.

Configuration

  • PHP 8.3
  • Symfony 7.1

Introduction

Dans l'article de blog « Une semaine de Symfony » #923, il y avait un article écrit par Damien Alexandre de JoliCode : « Une bonne convention de nommage pour les routes, les contrôleurs et les templates ? ». Comme j'ai toujours été intéressé par les bonnes pratiques et les conventions, il a retenu mon attention. Je vous laisse lire l'article en entier, mais l'un des principaux sujets abordés est :

Pourrait-on utiliser la FQCN des contrôleurs comme nom de route ? 🤔

But

Cet article présente quelques idées intéressantes, mais essayons tout ceci dans un vrai projet Symfony. Pourquoi ne pas aller plus loin ?
MicroSymfony est un template d'application Symfony que j'ai développé et utilisant déjà le pattern ADR ; c'est donc un bon candidat pour tester tout ceci.

L'architecture ADR

ADR correspond à « Action Domain Responder » ; dans Symfony cela peut être implémenté avec des contrôleurs invocables. Comme expliqué dans la documentation :

«Controllers can also define a single action using the __invoke() method, which is a common practice when following the ADR pattern (Action-Domain-Responder).»

🇫🇷 « Les contrôleurs peuvent aussi définir une action grâce à la méthode __invoke(), qui est une pratique commune quand on suit l'architecture ADR. »

Voici le snippet extrait de la documentation :

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

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/hello/{name}', name: 'hello')]
class Hello
{
    public function __invoke(string $name = 'World'): Response
    {
        return new Response(sprintf('Hello %s!', $name));
    }
}

Peut-on améliorer ceci ? 🤔

POC : utilisation de la FQCN du contrôleur comme nom de route

Dans la documentation, nous voyons que la route est une simple chaîne. Elle est mise en dur. Y a-t-il une convention pour cette chaîne ? On peut mettre ce que l'on veut. Quand l'application grossit, si on a plusieurs développeurs, chacun peut utiliser une convention différente. Comment éviter ceci ?

Quand on utilise l'architecture ADR, chaque action est encapsulée dans une classe de contrôleur contenant une seule méthode __invoke(). Cela veut dire que le contrôleur identifie l'action et la FQCN identifie de façon unique chaque contrôleur. Pourquoi donc ne pas utiliser la FQCN self::class comme nom de route ? C'est le point principal de l'article de JoliCode. Le snippet précédent devient :

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

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/hello/{name}', name: self::class)]
class Hello
{
    public function __invoke(string $name = 'World'): Response
    {
        return new Response(sprintf('Hello %s!', $name));
    }
}

Vérifions la route avec la commande debug:router :

$ bin/console debug:router "App\Controller\Hello"
+--------------+---------------------------------------------------------+
| Property     | Value                                                   |
+--------------+---------------------------------------------------------+
| Route Name   | App\Controller\Hello                                    |
| Path         | /hello/{name}                                           |
| Path Regex   | {^/hello(?:/(?P<name>[^/]++))?$}sDu                  |
| Host         | ANY                                                     |
| Host Regex   |                                                         |
| Scheme       | ANY                                                     |
| Method       | ANY                                                     |
| Requirements | NO CUSTOM                                               |
| Class        | Symfony\Component\Routing\Route                         |
| Defaults     | _controller: App\Controller\Hello()                     |
|              | name: World                                             |
| Options      | compiler_class: Symfony\Component\Routing\RouteCompiler |
|              | utf8: true                                              |
+--------------+---------------------------------------------------------+

Comme prévu, le nom de route associé à l'action devient la FQCN du contrôleur 🎉. On peut ainsi bel et bien utiliser la FQCN comme nom de route. De plus, on évite de mettre des chaînes en dur dans le code et on peut appliquer cette convention à tous les autres contrôleurs.

Veuillez noter que si omet le nom de la route, alors Symfony en génère un comme expliqué dans cet article de blog . Dans ce cas, le nom de la route généré sera app_hello__invoke. Cette fonctionnalité a été introduite dans Symfony 6.4.

Parfait, cela fonctionne, mais pouvons-nous aller plus loin dans cette approche ? Ne pouvons-nous pas utiliser self::class à d'autres endroits ?

POC : utilisation de la FQCN du contrôleur comme nom et chemin de template Twig

Quand on crée un contrôleur, on peut étendre le contrôleur abstrait Symfony AbstractController qui fournit quelques fonctions utiles. Notamment, la fonction render() pour rendre une réponse depuis un template Twig.
Regardons un exemple dans le projet MicroSymfony : le contrôleur HomeAction affiche la page d'accueil du projet. On utilise la FQCN du contrôleur pour rendre le template Twig.

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Attribute\Cache;
use Symfony\Component\Routing\Attribute\Route;

/**
 * @see StaticActionTest
 */
#[AsController]
#[Cache(maxage: 3600, public: true)]
final class HomeAction extends AbstractController
{
    /**
     * Simple page with some content.
     */
    #[Route(path: '/', name: self::class)]
    public function __invoke(): Response
    {
        $readme = (string) file_get_contents(__DIR__.'/../../README.md');

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

Cela fonctionne, mais on doit modifier le chemin du template dans le projet ; c'est désormais :

templates
    App
        Controller
            HomeAction.html.twig

Veuillez noter qu'on a un peu de magie 🧙 puisque la FQCN est App\Controller\HomeAction alors que le chemin généré est finalement App/Controller/HomeAction. Symfony convertit les anti-slash en slashes, car le chemin passé en paramètre est normalisé.

Peut-on faire mieux ? Si on regarde le code du contrôleur, il y a encore des chaînes en dur. Pouvez-vous les voir ?

POC : utilisation de la FQCN du contrôleur comme chemin de route

Prenons comme exemple un autre contrôleur, celui affichant le fichier composer.json du projet :

#[Route(path: '/composer', name: self::class)]
public function __invoke(): Response
{
    $composer = (string) file_get_contents(__DIR__.'/../../composer.json');

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

Dans l'attribut #Route de la fonction __invoke(), remplaçons la chaîne /composer de la valeur de path: par la FQCN de la classe. Le code devient :

#[Route(path: self::class, name: self::class)]
public function __invoke(): Response
{
    $composer = (string) file_get_contents(__DIR__.'/../../composer.json');

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

Encore une fois, nous pouvons le faire, l'URL devient :

https://127.0.0.1:8001/App\Controller\ComposerAction

L'URL est certes affreuse 😱, mais cela fonctionne ! Devrions-nous utiliser ceci ? Pas sur un site public où le SEO est important. Mais, pour un site interne ou un outil, pourquoi pas ? Ce qui est commode ici, c'est que l'on sait le code qui est utilisé rien qu'en regardant l'URL.

Testez par vous-même !

Vous pouvez tester tout ceci en moins d'une minute ; lancez (composer et le binaire Symfony sont requis) :

composer create-project strangebuzz/microsymfony && cd microsymfony && make start && open https://127.0.0.1:8000

Vous pouvez créer une PR si vous détectez des erreurs ou si quelque chose peut être amélioré. 🙏

Conclusion

J'utilise cette nouvelle architecture dans le projet MicroSymfony (à part pour le chemin des routes), et j'ai introduit un nouveau helper Twig path(ctrl_fqcn('HomeAction')) afin de ne pas avoir à saisir la FQCN complète des contrôleurs dans les templates Twig quand on veut générer des URL. (ce qui est assez moche puisque qu'on doit utiliser App\\Controller\\ComposerAction 😱).
Essayons et voyons si cette méthode peut convenir à de plus gros projets. Au moins, nous savons que c'est possible. Qu'en pensez-vous ? 🙂

Et voilà ! J'espère que vous avez aimé. Découvrez d'autres informations en rapport à cet article avec les liens ci-dessous. Comme toujours, retours, likes et retweets sont les bienvenus. (voir la boîte ci-dessous) À bientôt ! COil. 😊

  GitHub   Lire la doc   Lire la doc  Plus sur le web

Ils m'ont donné leurs retours et m'ont aidé à corriger des erreurs et typos dans cet article, un grand merci à : BernardNgandu 👍

  Travaillez avec moi !


A vous de jouer !

Ces articles vous ont été utiles ? Vous pouvez m'aider à votre tour de plusieurs manières : (cf le tweet à droite pour me contacter )

  • Me remonter des erreurs ou typos.
  • Me remonter des choses qui pourraient être améliorées.
  • Aimez et retweetez !
  • Suivez moi sur Twitter
  • Inscrivez-vous au flux RSS.
  • Cliquez sur les boutons Plus sur Stackoverflow pour me faire gagner des badges "annonceur" 🏅.

Merci d'avoir tenu jusque ici et à très bientôt sur Strangebuzz ! 😉

COil