Symfony11 min

Symfony Mercure : push en temps reel avec exemples

Par Pierre-Arthur Demengel
SymfonyMercureTemps reelSSE

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 :

  1. Un evenement se produit dans votre application Symfony (nouvelle commande, message, notification)
  2. Votre code PHP publie une Update sur le hub Mercure via HTTP POST
  3. Le hub Mercure diffuse cette Update a tous les clients abonnes au topic concerne
  4. 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.

Questions fréquentes

13 projets livrésGrand-Est & BelgiqueLighthouse >90Disponible immédiatement

Un projet en tête ?

Discutons de votre site web. Réponse garantie sous 24h.

Ou appelez directement :06 95 41 30 25

WhatsApp
Appeler