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.
Si t'as déjà eu l'impression que ton serveur s'écroule sous les requêtes comme un député sous une question gênante, bienvenue à toi. Pour éviter de surcharger ton application Symfony de requêtes synchrones et bloquantes en transformant ton CPU en radiateur, alors utilise Symfony Messenger.
Bon, voilà le topo : j'avais besoin de récupérer les stats de visite depuis Umami, les transformer, calculer des scores pour chaque page et tout stocker en base. Le genre de traitement qui, une fois mis bout à bout, te fait un truc aussi long qu'un discours présidentiel
Ce qu'il ne faut pas faire au risque de finir enduit de miel à la merci de frelons affamés : "Ok, je vais tout mettre dans une seule fonction, avec 600 lignes de code et 15 niveaux d'imbrication. De toute façon, c'est juste une commande console, qui va remarquer ?"
Pour faire simple, Messenger c'est comme un gars qui fait passer des petits papiers en classe, mais pour ton code. Tu lui files un message, il se débrouille pour le donner à celui qui sait le traiter.
Le principe c'est :
Et le meilleur dans tout ça ? Tu peux décider si ça se fait tout de suite (synchrone) ou plus tard (asynchrone). Comme quand t'annonces à tes potes que tu as un billet en trop pour aller voir Lady Gaga : soit ils hurlent tout de suite, soit tu reportes la crise pour plus tard.
composer require symfony/messenger
Et boom, le bundle est installé. Symfony te génère même un fichier de config config/packages/messenger.yaml où tu peux définir tes transports et routes. Tout simple, tout propre.
Première étape, j'ai créé un message pour stocker mes données d'analytics. C'est juste une classe PHP avec quelques propriétés et des getters :
// src/Message/Analytic/ProcessPageMetricsMessage.php
namespace App\Message\Analytic;
class ProcessPageMetricsMessage
{
private readonly string $url;
private readonly int $views;
public function __construct(string $url, int $views)
{
$this->url = $url;
$this->views = $views;
}
public function getUrl(): string
{
return $this->url;
}
public function getViews(): int
{
return $this->views;
}
}
C'est plus minimaliste que la garde-robe d'un naturiste, mais ça fait le job.
Ensuite, j'ai créé le Handler qui traite ce message. C'est là que la magie opère :
// src/MessageHandler/Analytic/ProcessPageMetricsHandler.php
namespace App\MessageHandler\Analytic;
use App\Entity\Analytic\PageMetric;
use App\Message\Analytic\ProcessPageMetricsMessage;
use App\Service\Analytic\PageViewTransformer;
use App\Service\Analytic\ScoreCalculator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class ProcessPageMetricsHandler
{
public function __construct(
private EntityManagerInterface $entityManager,
private PageViewTransformer $pageViewTransformer,
private ScoreCalculator $scoreCalculator,
) {
}
public function __invoke(ProcessPageMetricsMessage $message): void
{
// Créer une nouvelle métrique
$metric = new PageMetric();
$metric->setUrl($message->getUrl());
$metric->setViews($message->getViews());
// Trouver le post associé
$post = $this->pageViewTransformer->findPostFromUrl($message->getUrl());
if ($post instanceof \App\Entity\Content\Post) {
$metric->setPost($post);
}
// Calculer le score pondéré
$weightedScore = $this->scoreCalculator->calculateWeightedScore($metric);
$metric->setWeightedScore($weightedScore);
// Persister en base de données
$this->entityManager->persist($metric);
$this->entityManager->flush();
}
}
Remarque l'attribut
#[AsMessageHandler] en haut, qui dit à Symfony "Hé, ce gars-là, il traite les messages".
La partie fun, c'est comment j'envoie mes messages. J'ai créé une commande console qui récupère les stats depuis Umami puis dispatch un message pour chaque URL :
// src/Command/Analytic/FetchMetricsStatsCommand.php
namespace App\Command\Analytic;
use App\Message\Analytic\ProcessPageMetricsMessage;
use App\Service\Analytic\PageViewTransformer;
use App\Service\Analytic\UmamiClient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Messenger\MessageBusInterface;
#[AsCommand(
name: 'app:analytics:fetch-stats',
description: 'Fetch Umami stats and dispatch messages to process them',
)]
class FetchMetricsStatsCommand extends Command
{
public function __construct(
private readonly UmamiClient $umamiClient,
private readonly PageViewTransformer $pageViewTransformer,
private readonly MessageBusInterface $messageBus,
#[Autowire('%umami_website_id%')] private readonly string $websiteId,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Récupération des statistiques depuis Umami...');
// Récupérer les métriques d'Umami
$umamiData = $this->umamiClient->getMetrics(
websiteId: $this->websiteId,
);
if ($umamiData === []) {
$output->writeln('<e>Aucune donnée récupérée depuis Umami</e>');
return Command::FAILURE;
}
// Transformer les données brutes en DTOs
$metrics = $this->pageViewTransformer->transformUmamiData($umamiData);
$output->writeln(\sprintf('Transformation de %d métriques...', \count($metrics)));
// Dispatcher un message pour chaque métrique
$dispatchedCount = 0;
foreach ($metrics as $metric) {
$message = new ProcessPageMetricsMessage(
$metric->getUrl(),
$metric->getViews(),
);
$this->messageBus->dispatch($message);
++$dispatchedCount;
}
$output->writeln(\sprintf('<info>%d messages envoyés au bus pour traitement</info>', $dispatchedCount));
return Command::SUCCESS;
}
}
L'avantage de Messenger, c'est que tu peux démarrer en mode synchrone (les messages sont traités immédiatement) puis passer en asynchrone quand tu veux. Dans ma config actuelle, c'est du synchrone, mais le passage à l'asynchrone c'est littéralement 2 lignes à décommenter :
# config/packages/messenger.yaml
framework:
messenger:
# Décommente ça pour envoyer les messages en échec dans ce transport pour traitement ultérieur
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
# Pour passer en asynchrone, suffit de décommenter :
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
# failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
# Route tes messages vers les transports
# 'App\Message\YourMessage': async
Découplage : Mon code est organisé en petits morceaux indépendants, comme un menu McDo mais en version propre.
Scalabilité : Si demain mes pages explosent en popularité (haha, la bonne blague), je peux passer en asynchrone et distribuer le traitement.
Tolérance aux pannes : Si un traitement échoue, ça ne bloque pas tout le reste. C'est comme quand t'as un chat possédé par le démon sur trois : les deux autres peuvent continuer à vivre.
Lisibilité : Mon code est plus court et plus focalisé. Chaque classe fait une seule chose, bien.
Dans mon architecture, tout part du client Umami qui récupère les données brutes :
UmamiClient se connecte à l'API Umami et récupère les métriquesPageViewTransformer convertit ces données en objets DTO (PageMetricDto)FetchMetricsStatsCommand boucle sur ces DTOs et envoie un message pour chacunProcessPageMetricsHandler reçoit chaque message et fait le traitement de fond :
PageMetricScoreCalculatorEt voilà, la boucle est bouclée ! Après ça, mes TopPosts sont automatiquement mis à jour avec les articles les plus pertinents.
Avec Symfony Messenger, j'ai transformé ce qui aurait pu être un gros bloc de code indigeste en un système élégant, découplé et prêt à scaler. Et tout ça sans même avoir besoin de me lever de mon canapé (ouais presque hein), ce qui est quand même le but ultime de tout développeur qui se respecte.
Si tu te demandes comment suivre ce chemin de sagesse, voici un conseil : commence petit, en synchrone, puis évolue quand tu en as besoin. C'est comme les implants capillaires : t'y vas progressivement, sinon les gens remarquent.