Introduction
Envoyer des données du serveur vers le client, sans polling, sans WebSocket complexe.
Push natif
Le serveur envoie sans que le client ne demande.
Résilient
Reconnexion auto + récupération des messages manqués.
Simple
API native navigateur — aucune lib JS requise.
Le problème
Polling
- ✓ Simple
- ✗ Latence élevée
- ✗ Charge serveur inutile
Long Polling
- ✓ Moins de requêtes
- ✗ Connexions suspendues
- ✗ Scalabilité difficile
Mercure / SSE
- ✓ Push natif serveur
- ✓ HTTP standard
- ✓ Reconnexion auto
Polling = téléphoner toutes les 5 min pour demander "t'as du nouveau ?" — Mercure = recevoir un SMS dès que ça change.
Polling · SSE · WebSockets
Polling
- ✓ Simple à implémenter
- ✗ Latence = intervalle
- ✗ Requêtes inutiles si rien ne change
- ✗ Ne scale pas bien
SSE — Mercure
- ✓ API navigateur native (
EventSource) - ✓ HTTP standard — proxy transparent
- ✓ Reconnexion + rattrapage intégrés
- ✗ Serveur → Client uniquement
WebSockets
- ✓ Bidirectionnel full-duplex
- ✓ Idéal chat, binaire, jeux intensifs
- ✗ Protocole séparé (
ws://) - ✗ Proxy et reconnexion complexes
Architecture Mercure
Topics = IRIs
Chaque message cible une URL identifiant une ressource. Le Hub route les updates vers les seuls abonnés dont le JWT autorise ce topic.
Double JWT
Publisher (PHP) : autorise les topics publiables. Subscriber (navigateur) : cookie HttpOnly pour les topics privés.
Résilience native
Le Hub stocke un historique de messages. Le Last-Event-ID permet au client de rattraper les messages manqués après une coupure, sans perte.
Cas d'usage
Notifications temps réel
Alertes, badges, toasts — un utilisateur agit, tous voient la mise à jour instantanément.
Jeux & Collaboration
Jeux tour par tour, éditeurs collaboratifs, tableaux Kanban en direct.
Dashboards live
Métriques, statuts de services, graphiques sans rechargement.
Jobs asynchrones
Progression d'un import CSV, export PDF, ou traitement long côté serveur.
Installation & Configuration
1. Installer
composer require mercure
2. Variables d'environnement
MERCURE_URL=https://mercure/.well-known/mercure # URL interne PHP → Hub (réseau Docker) MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure # URL publique Navigateur → Hub MERCURE_JWT_SECRET=!ChangeThisMercureHubJWTSecretKey! # Clé partagée Symfony ↔ Hub3. mercure.yaml
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: ['*']
subscribe: ['*']
Hub Mercure avec Docker
services:
mercure:
image: dunglas/mercure
restart: unless-stopped
environment:
SERVER_NAME: 'mercure, :80'
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET}
volumes:
- mercure_data:/data
php:
environment:
MERCURE_URL: http://mercure/.well-known/mercure # interne réseau Docker
MERCURE_PUBLIC_URL: https://localhost/.well-known/mercure # navigateur → Hub
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET}
Deux URLs distinctes : MERCURE_URL est l'URL interne utilisée par PHP pour publier (réseau Docker). MERCURE_PUBLIC_URL est l'URL publique utilisée par le navigateur pour s'abonner.
UI de debug disponible sur /.well-known/mercure/ui/ — publier des messages manuellement et observer les souscriptions.
Mixed Content : app HTTPS → Hub HTTPS obligatoire. En local : symfony server:start --no-tls
Publier depuis Symfony
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
class StockController extends AbstractController
{
public function update(HubInterface $hub): Response
{
$update = new Update(
'https://example.com/products/42', // topic (IRI)
json_encode([
'stock' => 0,
'message' => 'Rupture de stock',
])
);
$hub->publish($update); // POST vers le Hub
return new Response('OK');
}
}
Mise à jour privée (token requis côté client)
$update = new Update(
topic: 'https://example.com/orders/99',
data: json_encode(['status' => 'expédié']),
private: true // seuls les abonnés autorisés reçoivent
);
S'abonner dans le navigateur
{# templates/product/show.html.twig #}
<script>
const es = new EventSource(
"{{ mercure('https://example.com/products/42') |escape('js') }}"
);
es.onmessage = (event) => {
const data = JSON.parse(event.data);
document.getElementById('stock').textContent = data.stock;
};
</script>Plusieurs topics
const es = new EventSource("{{ mercure([
'https://example.com/products/42',
'https://example.com/notifications/user/{{ app.user.id }}'
]) |escape('js') }}");
La fonction Twig mercure() génère l'URL Hub avec les bons paramètres ?topic= automatiquement.
Publication Asynchrone
Par défaut, $hub->publish() est synchrone. Pour ne pas bloquer, on dispatch via Messenger.
use Symfony\Component\Messenger\MessageBusInterface;
class OrderService
{
public function ship(Order $order): void
{
// ... logique métier ...
// Envoi délégué à un worker (Redis/RabbitMQ/Doctrine)
$this->bus->dispatch(new Update(
'https://example.com/orders/' . $order->getId(),
json_encode(['status' => 'expédié'])
));
}
}
messenger.yaml : transport Redis
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'Symfony\Component\Mercure\Update': async
docker-compose : service Redis
services:
redis:
image: redis:7-alpine
restart: unless-stopped
php:
environment:
MESSENGER_TRANSPORT_DSN: redis://redis:6379/messages
Redis est l'idéal avec Docker — image Alpine légère (~30 Mo), zéro configuration, latence sub-milliseconde. Lancer le worker : bin/console messenger:consume async. Alternatives : RabbitMQ (routing avancé), Doctrine (si pas de Redis), Kafka (très gros volumes).
Publication Asynchrone
| Aspect | Dev local | Production |
|---|---|---|
| Hub | docker-compose up | Hub Mercure, Docker, Helm chart |
| TLS | --no-tls (Symfony CLI) | HTTPS obligatoire (même domaine) |
| Nginx | Non requis | proxy_buffering off; obligatoire |
| Auth cookie | Domaine localhost | App + Hub sur même domaine |
Config Nginx requise
location /.well-known/mercure {
proxy_pass http://mercure;
proxy_read_timeout 24h;
proxy_buffering off; # ← OBLIGATOIRE
proxy_cache off;
}nginx
Bonnes pratiques
- JWT secret fort (> 256 bits)
- Whitelist stricte des topics
- Topics privés pour données sensibles
- Messenger async pour les volumes élevés
- Superviser le worker
En résumé
Avantages
- ✓ Temps réel sans complexité WebSocket
- ✓ Push natif (
EventSource) - ✓ PHP stateless préservé
- ✓ Async via Messenger
- ✓ API Platform intégré
Points de vigilance
- ✗ Hub séparé à opérer
- ✗ Nginx à configurer
- ✗ Unidirectionnel seulement
Besoin
- Notification utilisateur
- Ressource privée
- Job long (import/export)
- API REST temps réel
- Dashboard live
Solution
- Update public + EventSource
- Update privé + cookie JWT
- Messenger async + progression SSE
#[ApiResource(mercure: true)]- Topics multiples + onmessage JS