Favicons : quand les petits pixels font les grands sites
Un favicon devant un lien, c’est le sourire du site qui te dit : oui, tu peux cliquer, je connais.
Fatigué des God Controllers qui gonflent avec 12 dépendances et mélangent toute la logique métier ? Il est temps de repenser leur rôle et de les transformer en simples chefs d'orchestre en adoptant le pattern Single Action Controller et en déléguant le travail à des services ou aux attributs de Symfony.
Dans l'écosystème Symfony, le contrôleur est la pierre angulaire de toute requête HTTP. C'est le chef d'orchestre : il reçoit la requête, coordonne le travail et retourne une réponse.
Mais soyons honnêtes, nous avons tous déjà vu (ou écrit) ce Controller. Celui qui commence avec 15 lignes, puis 50, puis finit par dépasser les 150 lignes, important 12 services différents.
C'est ce qu'on appelle un God Controller : un anti-pattern qui centralise toute la logique applicative en un seul endroit. Le résultat ? Un code :
Prenons cet exemple. Il n'est pas horrible, mais c'est le début des ennuis. Il gère la page d'accueil, mais demain il gérera aussi, la page "à propos", etc.
// src/Controller/HomeController.php
namespace App\Controller;
use App\Entity\Content\Page;
use App\Enum\Content\PageTemplateEnum;
use App\Repository\Content\PageRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class HomeController extends AbstractController
{
public function __construct(
private readonly PageRepository $pageRepository,
// Bientôt, on ajoutera le Mailer, le FormFactory, l'EntityManager...
) {
}
#[Route(path: '/', name: 'app_home', methods: ['GET'])]
public function index(): Response
{
return $this->render('home/index.html.twig', [
'page' => $this->getHomePage(),
]);
}
// Bientôt, on ajoutera la méthode contactAction(), aboutAction()...
private function getHomePage(): Page
{
$page = $this->pageRepository->findPageByTemplate(template: PageTemplateEnum::HOME);
if (!$page instanceof Page) {
throw $this->createNotFoundException('Page d\'accueil introuvable.');
}
return $page;
}
}
La violation principale est que le contrôleur sait comment faire le travail (la logique de getHomePage()). Il ne fait pas que coordonner, il exécute la logique métier.
La première étape pour guérir est de tout casser. On adopte une approche plus moderne : le Single Action Controller (ou "Controller Invocable").
Le principe est simple : Une classe = Une action = Une route.
// src/Controller/HomeAction.php
namespace App\Controller;
use App\Entity\Content\Page;
use App\Enum\Content\PageTemplateEnum;
use App\Repository\Content\PageRepository;
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,
) {
}
#[Route(path: '/', name: self::class, methods: ['GET'])]
public function __invoke(): Response
{
return $this->render(self::class.'.html.twig', [
'page' => $this->getHomePage(),
]);
}
private function getHomePage(): Page
{
// ... (la même logique que tout à l'heure)
}
}
C'est mieux ! Mais est-ce résolu ? Non. On a juste déplacé le problème. On n'a plus un God Controller, on a une God Action. Ce Controller est toujours trop lourd. Il fait encore trop de choses :
__invoke).getHomePage).PageRepository.Un Controller ne doit avoir qu'une seule responsabilité : la coordination. Il doit juste dire : "Toi, fais-moi ce boulot" et "Toi, affiche-moi ce résultat".
Pour cela, il existe deux chemins principaux.
Pour les cas simples (récupérer une ou plusieurs entités), pourquoi réécrire une logique que Symfony sait déjà gérer ? C'est là que la magie des attributs entre en jeu.
Regardez cette transformation finale.
// src/Controller/Front/HomeAction.php (Notre "Action" finale et épurée)
namespace App\Controller\Front;
use App\Entity\Page;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\Cache; // <-- Magie N°1
use Symfony\Component\Routing\Attribute\Route;
final class HomeAction extends AbstractController
{
#[Cache( // On délègue la logique de cache HTTP
maxage: 300,
smaxage: 600,
public: true,
mustRevalidate: true,
)]
#[Route(
path: '/',
name: self::class,
methods: ['GET'],
)]
public function __invoke(
// On délègue la recherche de l'entité
#[MapEntity(expr: 'repository.findOneBy({"status": "online", "template": "home"})')] // <-- Magie N°2
Page $page,
): Response {
// Le contrôleur ne fait plus que coordonner :
// 1. Il reçoit la Page (déjà trouvée)
// 2. Il passe la Page à Twig (le Responder)
return $this->render(self::class.'.html.twig', [
'page' => $page,
]);
}
}
Analysons cette version :
PageRepository.$response->set...), on déclare simplement nos intentions.C'est super. Mais que se passe-t-il si notre page d'accueil doit aussi charger les 4 derniers articles de blog ? L'attribut #[MapEntity] ne peut pas gérer cette double logique.
Dès que la logique devient trop complexe pour un attribut, nous passons au Cas B : extraire la logique dans un service dédié.
C'est là qu'on touche au cœur du pattern ADR (Action-Domain-Responder).
HomeAction.HomePageHandler, qui contient la logique métier.On crée un service qui va faire tout le sale boulot. Pour être propre, il retournera un objet dédié (un DTO / ViewModel).
// src/ViewModel/HomePageViewModel.php
namespace App\ViewModel;
use App\Entity\Content\Page;
final class HomePageViewModel
{
/** @param array<int, Post> $posts */
public function __construct(
public readonly Page $page
) {
}
}
// src/Service/HomePageHandler.php (Notre "Domaine")
namespace App\Service;
// ... (tous les 'use' nécessaires)
use App\Repository\PageRepository;
use App\ViewModel\HomePageViewModel;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final class HomePageHandler
{
public function __construct(
private readonly PageRepository $pageRepository
) {
}
public function handle(): HomePageViewModel
{
$page = $this->pageRepository->findPageByTemplate(/* ... */);
if (!$page) {
throw new NotFoundHttpException('Page d\'accueil introuvable.');
}
$posts = $this->postRepository->findAllVisible(limit: 4);
return new HomePageViewModel(page: $page);
}
}
Notre HomeAction change alors pour n'injecter que ce service.
// src/Controller/Front/HomeAction.php (Version pour logique complexe)
namespace App\Controller\Front;
use App\Service\HomePageHandler; // <-- On injecte le Handler
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 HomePageHandler $homePageHandler, // Une seule dépendance !
) {
}
#[Route(path: '/', name: self::class, methods: ['GET'])]
public function __invoke(): Response
{
// 1. On DÉLÈGUE le travail au Domaine
$viewModel = $this->homePageHandler->handle();
// 2. On PASSE le résultat au Responder (Twig)
return $this->render(self::class.'.html.twig', [
'page' => $viewModel->page,
]);
}
}
Ce Controller est également parfait : il est épuré et ne fait que de la coordination.
Que vous utilisiez la délégation par attributs ou par service, ces astuces rendent l'approche Action géniale au quotidien.
Regardez l'attribut Route :
#[Route(
path: '/',
name: self::class, // <-- LA MAGIE
methods: ['GET'],
)]
Cette petite astuce utilise la constante magique self::class pour nommer la route. Le nom de la route devient le FQCN (Fully Qualified Class Name) du Controller.
App\Controller\Front\HomeActionC'est narcissique, mais incroyablement pratique. Fini les app_home ou front_post_show à inventer et à mémoriser.
Puisque le nom de la route est le FQCN, on peut l'utiliser pour deviner le nom du template !
return $this->render(self::class.'.html.twig', [...]);
Symfony va chercher un template nommé App/Controller/Front/HomeAction.html.twig.
// Et dans le contrôleur :
// Symfony résoudra @Action/Front/HomeAction.html.twig
return $this->render('@Action'.(new \ReflectionClass($this))->getShortName().'.html.twig', [...]);
// Ou, plus simple, si vous n'avez pas de sous-dossiers :
return $this->render('@Action/HomeAction.html.twig', [...]);
C'est le coup de grâce. Puisque nos routes s'appellent App\Controller\Front\PostAction, c'est pénible à écrire dans Twig :
{# Beurk, c'est long #}
{{ path('App\\Controller\\Front\\PostAction', {'slug': post.slug}) }}
On va donc créer une petite extension Twig développée par COil🐝 strangebuzz/MicroSymfony: Template introduction, check out: https://www.strangebuzz.com/en/blog/introducing-the-microsymfony-application-template qui nous permet de retrouver le FQCN juste avec le nom court de la classe.
// src/Twig/Extension/RoutingExtension.php
namespace App\Twig\Extension;
// ... (Exactement le code que vous avez fourni)
final class RoutingExtension extends AbstractExtension
{
// ...
public function getFunctions(): array
{
return [
new TwigFunction('ctrl_fqcn', $this->getControllerFqcn(...)),
];
}
public function getControllerFqcn(string $ctrlShortname): string
{
// ... (Logique pour scanner les routes et trouver le FQCN)
}
}
Et voilà comment on l'utilise dans le template. N’est-ce pas merveilleux ?
{# templates/components/Post.html.twig #}
<a href="{{ path(ctrl_fqcn('PostAction'), {'slug': post.slug, 'category': post.category.slug}) }}">
{{ post.title|raw }}
</a>
C'est plus court, plus lisible, et ça donne l'impression d'être intelligent. Que demander de plus ?
PostAction affiche un post.MapEntity), soit une seule dépendance (le Handler). Fini les constructeurs à 12 arguments.MapEntity est si simple qu'il n'a presque pas besoin de test unitaire. Un contrôleur avec un Handler est trivial à tester (il suffit de mocker le Handler).__invoke, self::class et MapEntity peut sembler magique pour les nouveaux développeurs.ctrl_fqcn() est géniale, mais elle lie vos templates à votre extension maison.
⠀Si vous écrivez encore des méthodes indexAction(), showAction() et editAction() dans un seul Controller, vous n'êtes pas un mauvais développeur. Vous êtes juste... un peu bloqué en 2010.
Embrassez le changement, adoptez le pattern ADR (ou juste le "Single Action Controller"), nommez vos contrôleurs avec le suffixe Action, et utilisez __invoke().
N’oubliez pas : Un bon Controller, c'est comme un bon politicien. Il délègue tout le travail et prend tout le crédit. Laissez vos Attributs et vos Handlers (Domaine) faire le boulot et vos contrôleurs juste coordonner le flux.