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 ↔ Hub

3. 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

AspectDev localProduction
Hubdocker-compose upHub Mercure, Docker, Helm chart
TLS--no-tls (Symfony CLI)HTTPS obligatoire (même domaine)
NginxNon requisproxy_buffering off; obligatoire
Auth cookieDomaine localhostApp + 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