Symfony Mercure : push en temps reel avec exemples
Le temps reel dans une application web n'est plus un luxe - c'est une attente. Notifications instantanees, tableaux de bord en direct, flux d'activite : Mercure permet d'ajouter tout cela a une application Symfony sans la complexite des WebSockets. Voici un guide complet avec des exemples concrets.
Mercure en bref : pourquoi pas les WebSockets ?
Avant de plonger dans le code, clarifions le positionnement de Mercure face aux alternatives :
- Polling - le client interroge le serveur toutes les X secondes. Simple mais inefficace : charge inutile, latence elevee.
- Long polling - le serveur garde la connexion ouverte jusqu'a avoir une donnee. Mieux, mais consomme des ressources serveur.
- WebSockets - communication bidirectionnelle, temps reel vrai. Mais complexe a deployer (upgrade HTTP, sticky sessions, pas de cache HTTP).
- Server-Sent Events (SSE) - le protocole sur lequel repose Mercure. Unidirectionnel (serveur vers client), basee sur HTTP standard, reconnexion automatique, traversee native des proxies.
Mercure est un hub qui centralise les messages SSE. Le backend PHP publie des mises a jour sur le hub via HTTP POST, et les clients JavaScript s'y abonnent via l'API EventSource. C'est simple, standardise et suffisant pour la majorite des cas d'usage.
Architecture du systeme
Voici le flux complet d'une mise a jour temps reel avec Mercure :
- Un evenement se produit dans votre application Symfony (nouvelle commande, message, notification)
- Votre code PHP publie une Update sur le hub Mercure via HTTP POST
- Le hub Mercure diffuse cette Update a tous les clients abonnes au topic concerne
- Le JavaScript cote client recoit l'evenement et met a jour l'interface
Installation du hub Mercure
Le hub Mercure est un binaire Go autonome. Plusieurs options d'installation :
# Option 1 : Docker (recommande pour le developpement)
docker run -d --name mercure -p 3000:80 -e MERCURE_PUBLISHER_JWT_KEY='votre-cle-secrete-minimum-256-bits-de-long' -e MERCURE_SUBSCRIBER_JWT_KEY='votre-cle-secrete-minimum-256-bits-de-long' -e SERVER_NAME=':80' dunglas/mercure
# Option 2 : binaire precompile
# Telecharger depuis https://github.com/dunglas/mercure/releases
# Lancer avec :
MERCURE_PUBLISHER_JWT_KEY='votre-cle-secrete' MERCURE_SUBSCRIBER_JWT_KEY='votre-cle-secrete' ./mercure run
Pour une configuration Docker complete en production, consultez notre guide sur Docker pour Symfony en production.
Installation du composant Symfony
composer require symfony/mercure
La recipe Flex configure automatiquement les variables d'environnement dans votre fichier .env :
# .env
MERCURE_URL=http://localhost:3000/.well-known/mercure
MERCURE_PUBLIC_URL=http://localhost:3000/.well-known/mercure
MERCURE_JWT_SECRET=votre-cle-secrete-minimum-256-bits-de-long
Verifiez que la configuration du composant est en place :
# config/packages/mercure.yaml
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'
Publier une mise a jour depuis PHP
La publication se fait via le service HubInterface. Voici un exemple concret de systeme de notifications :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Routing\Attribute\Route;
class NotificationController extends AbstractController
{
#[Route('/api/notifications/send', name: 'notification_send', methods: ['POST'])]
public function send(Request $request, HubInterface $hub): JsonResponse
{
$data = json_decode($request->getContent(), true);
$userId = $data['user_id'];
$message = $data['message'];
// Publier une mise a jour sur un topic specifique a l'utilisateur
$update = new Update(
// Le topic - les clients s'abonnent a cette URI
topics: "https://monsite.com/users/{$userId}/notifications",
// Les donnees (JSON, HTML, texte...)
data: json_encode([
'type' => 'notification',
'message' => $message,
'timestamp' => (new \DateTimeImmutable())->format('c'),
]),
// true = topic prive (necessite un JWT cote client)
private: true,
);
$hub->publish($update);
return $this->json(['status' => 'sent']);
}
}
Publication depuis un service metier
En pratique, vous publierez souvent les mises a jour depuis un service plutot que depuis un controller :
<?php
namespace App\Service;
use App\Entity\Order;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
final class OrderNotifier
{
public function __construct(
private readonly HubInterface $hub,
) {}
public function notifyNewOrder(Order $order): void
{
// Notification pour le tableau de bord admin
$this->hub->publish(new Update(
topics: 'https://monsite.com/admin/orders',
data: json_encode([
'type' => 'new_order',
'order_id' => $order->getId(),
'total' => $order->getTotal(),
'customer' => $order->getCustomerName(),
]),
));
// Notification pour le client
$this->hub->publish(new Update(
topics: "https://monsite.com/users/{$order->getCustomerId()}/orders",
data: json_encode([
'type' => 'order_confirmed',
'order_id' => $order->getId(),
'message' => 'Votre commande a ete confirmee',
]),
private: true,
));
}
}
S'abonner depuis JavaScript
Cote client, l'API EventSource est native dans tous les navigateurs modernes. Aucune librairie JavaScript n'est necessaire :
// Abonnement a un topic public
const url = new URL('http://localhost:3000/.well-known/mercure');
url.searchParams.append('topic', 'https://monsite.com/admin/orders');
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Nouvelle commande :', data);
// Mettre a jour l'interface
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = `Commande #${data.order_id} - ${data.total} EUR`;
document.getElementById('notifications').prepend(notification);
};
eventSource.onerror = (error) => {
console.error('Erreur SSE :', error);
// EventSource se reconnecte automatiquement
};
Abonnement a un topic prive (avec JWT)
Pour les topics prives, le client doit fournir un JWT. Symfony peut generer un cookie d'autorisation via le controller :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\Routing\Attribute\Route;
class DashboardController extends AbstractController
{
#[Route('/dashboard', name: 'dashboard')]
public function index(Authorization $authorization): Response
{
$user = $this->getUser();
// Generer le cookie JWT pour l'abonnement Mercure
$response = $this->render('dashboard/index.html.twig');
$authorization->setCookie(
$response,
["https://monsite.com/users/{$user->getId()}/notifications"],
);
return $response;
}
}
Cote JavaScript, il suffit d'ajouter withCredentials pour envoyer le cookie :
const url = new URL('http://localhost:3000/.well-known/mercure');
url.searchParams.append('topic', 'https://monsite.com/users/42/notifications');
// withCredentials envoie le cookie JWT
const eventSource = new EventSource(url, { withCredentials: true });
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
showNotification(data.message);
};
Integration avec Symfony Messenger
Pour les applications a forte charge, publiez les mises a jour de facon asynchrone via Symfony Messenger :
<?php
namespace App\MessageHandler;
use App\Message\OrderPlaced;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class PublishOrderNotification
{
public function __construct(
private readonly HubInterface $hub,
) {}
public function __invoke(OrderPlaced $message): void
{
$this->hub->publish(new Update(
topics: 'https://monsite.com/admin/orders',
data: json_encode([
'type' => 'new_order',
'order_id' => $message->orderId,
]),
));
}
}
Ainsi, la publication Mercure ne bloque pas la requete HTTP principale. Le worker Messenger la traite en arriere-plan.
Integration avec Turbo Streams
Si vous utilisez Symfony UX Turbo, Mercure s'integre nativement pour mettre a jour le DOM automatiquement :
{# templates/dashboard/index.html.twig #}
{# S'abonner a un topic Mercure et mettre a jour le DOM automatiquement #}
<turbo-stream-source
src="{{ mercure('https://monsite.com/admin/orders') }}"
/>
<div id="orders">
{# Le contenu sera mis a jour automatiquement #}
</div>
Cote PHP, publiez un fragment Turbo Stream au lieu de JSON brut :
<?php
use Symfony\Component\Mercure\Update;
$update = new Update(
topics: 'https://monsite.com/admin/orders',
data: '<turbo-stream action="prepend" target="orders">
<template>
<div class="order-card">
Nouvelle commande #123 - 450 EUR
</div>
</template>
</turbo-stream>',
);
$hub->publish($update);
Securisation et bonnes pratiques
Cles JWT separees
En production, utilisez des cles differentes pour la publication et l'abonnement :
# .env.local (production)
MERCURE_JWT_SECRET=cle-publication-tres-longue-et-secrete
MERCURE_SUBSCRIBER_JWT_SECRET=cle-abonnement-differente
Limiter les topics
Ne publiez jamais sur des topics trop generiques. Preferez des URIs specifiques qui incluent l'identifiant de la ressource. Cela evite que des utilisateurs puissent espionner des donnees qui ne leur sont pas destinees.
CORS en production
Configurez le hub Mercure pour n'accepter que votre domaine :
# Configuration du hub Mercure en production
MERCURE_CORS_ALLOWED_ORIGINS=https://monsite.com
Cas d'usage concrets
Voici les scenarios ou Mercure brille dans un projet Symfony :
- Notifications en temps reel - alerter les utilisateurs sans qu'ils rafraichissent la page
- Tableau de bord admin - voir les nouvelles commandes, inscriptions, erreurs en direct
- Suivi de progression - barre de progression pour un import de fichier traite par un worker
- Flux d'activite - timeline de type reseau social avec mises a jour live
- Chat simple - messagerie avec diffusion des messages via Mercure
Pour consommer des APIs tierces en complement, notre guide sur le HttpClient Symfony vous sera utile.
Besoin d'integrer du temps reel dans votre application Symfony ? En tant que developpeur freelance base a Metz et Strasbourg, je peux vous accompagner sur l'implementation de Mercure dans votre projet. Contactez-moi pour discuter de votre besoin, consultez mes tarifs ou decouvrez mes services de developpement web.
