Introduction
Après notre POC sur la compression vidéo et le HLS avec FrankenPHP, il serait ambitieux — voire légèrement déraisonnable — de tenter de recréer YouTube de zéro. En revanche, une idée beaucoup plus intéressante s’impose : comment gérer un état de session live avec Mercure ?
Pourquoi ce besoin ? Parce que dès qu’on parle de classe virtuelle ou de session vidéo en direct, on doit synchroniser bien plus que le flux vidéo. Il faut savoir qui est connecté, qui quitte la session, et surtout, mettre à jour ces informations sans requêtes répétées ni boucles côté client.
C’est précisément là que Mercure entre en scène. Ce protocole de Server-Sent Events (SSE) est pensé pour le temps réel côté web, et il s’intègre parfaitement à Symfony 7. En combinant les deux, on peut littéralement “réveiller” notre interface : présence des utilisateurs, changements d’état, réactions instantanées… tout sans WebSocket ni surcharge.
Alors, prêt à pousser notre POC un cran plus loin et à voir comment Mercure peut transformer une simple session vidéo en véritable expérience live Symfony 7 ?
Mercure, SSE… késako ?
Avant d’implémenter quoi que ce soit, petit rappel de base. Il y a environ un an, j’avais bricolé un chat temps réel avec Mercure et Turbo, et le résultat était bluffant : un flux réactif, instantané, sans WebSocket, le tout géré en PHP pur. Une vraie petite magie de la stack Symfony.
Le principe du SSE (Server-Sent Events) est simple et élégant : le serveur peut envoyer des messages continus au navigateur, sans que celui-ci ait besoin de les redemander. Le front s’abonne à un flux d’événements (feed), et tout ce qui transite sur ce canal est immédiatement reçu par le client.
Mercure, qui repose sur cette technologie, permet donc de diffuser des messages temps réel entre plusieurs appareils — qu’ils soient sur desktop, mobile, ou même un autre navigateur à l’autre bout du monde. Résultat : on peut partager un état commun entre utilisateurs sans requêtes AJAX, sans polling, et avec un minimum de configuration.
Et les cas d’usage sont nombreux :
– Gérer la présence en direct dans une session (comme notre cas ici).
– Construire un chat en temps réel avec Symfony 7.
– Mettre en place un système de notifications instantanées.
– Synchroniser des actions multi-appareils, à la manière de Facebook quand on like un post sur mobile et que le changement apparaît aussitôt sur le web.
Mercure est donc plus qu’un simple outil de “push” : c’est une colonne vertébrale du temps réel dans l’écosystème Symfony.
Stack technique
Pour ce POC “Live Session”, on va rester pragmatiques et efficaces. L’objectif n’est pas de sur-architecturer le projet, mais de valider un concept fonctionnel : la gestion de présence en temps réel avec Mercure.
Notre stack de départ :
– Symfony 7.3 pour profiter des dernières améliorations de performance et de DX (developer experience).
– PHP 8.4, histoire de rester sur une base moderne et tirer parti des optimisations du moteur.
– Docker pour orchestrer les services essentiels :
- PostgreSQL pour la base de données,
- MailCatcher pour les tests de notifications,
- et bien sûr, Mercure pour le temps réel.
Et là, je te vois venir : “Pas de FrankenPHP ?”
Eh bien non, pas tout de suite, et pour une bonne raison : Mercure.
Certes, Mercure est déjà intégré à Caddy (le serveur web de FrankenPHP), mais il nécessite une configuration HTTPS complète et la gestion de JWT (JSON Web Tokens) pour la sécurité des publications et abonnements. Or, dans cette première version, on veut rester concentrés sur le cœur du sujet : la présence utilisateur.
Le passage à FrankenPHP viendra plus tard, en partie 3, quand tout sera stabilisé. On pourra alors profiter du couple Mercure + Caddy pour le déploiement sécurisé et les perfs réseau, mais sans se rajouter de complexité inutile dès le départ.
La classroom et les données
Pour poser les bases de notre Live Session avec Symfony 7 et Mercure, on va démarrer avec une structure minimaliste, mais représentative d’un vrai cas d’usage.
Côté outillage, rien de superflu : un petit coup de Bootstrap pour la mise en forme rapide, et surtout, l’excellent Zenstruck/Foundry pour générer nos fixtures. L’idée est de remplir la base avec un jeu de données réaliste : un professeur, quelques élèves, plusieurs sessions de cours passées, et une session active prête à accueillir des connexions live.
Le but est simple : simuler le comportement réel d’une salle de classe virtuelle, sans perdre de temps sur le front. L’UI sera volontairement rudimentaire — l’important, c’est de valider le flux d’événements Mercure et la logique de présence.
Notre modèle de données tient en trois entités bien pensées :
– User : représente nos élèves et notre professeur.
– ClassroomSession : gère le contexte d’un cours (horaire, participants, statut).
– Presence : enregistre en temps réel qui est connecté ou déconnecté de la session.
Trois entités, un flux SSE, et la promesse d’un comportement temps réel fluide sans la moindre boucle côté client.
Aperçu UI : la page de statut des étudiants
Une fois nos entités en place, on peut donner un peu de chair à notre prototype. L’interface reste volontairement simple, mais elle traduit parfaitement ce qu’on veut démontrer : la mise à jour en temps réel de la présence des utilisateurs dans une session live.
L’exemple ci-dessous illustre une vue de statut de session : le professeur et ses élèves apparaissent avec leur statut en direct. Pas besoin de rafraîchir la page, c’est Mercure qui pousse les changements via le flux SSE.

On retrouve plusieurs éléments intéressants :
– Le nom de la session et les informations du professeur.
– Le nombre total de participants et le nombre de connectés en direct.
– Une table de suivi avec le nom, l’email, le statut et la dernière activité de chaque utilisateur.
– Des indicateurs visuels pour chaque état : vert (online), jaune (away), rouge (offline).
Sous le capot, le front est abonné au topic Mercure correspondant à la session. Dès qu’un utilisateur rejoint, quitte, ou reste inactif, un événement SSE est publié par Symfony — et l’interface réagit instantanément.
C’est une démonstration très concrète du principe de temps réel sans WebSocket : pas de polling, pas d’API qui tourne en boucle, juste un flux propre et réactif.
La page Classroom : cœur battant du live
Bienvenue dans la vue principale de notre système de session live. Ici, tout se joue : la vidéo, la liste des participants connectés, et le retour d’état en direct.

La partie centrale héberge la vidéo du cours (ou une iframe YouTube, selon le mode de test). Ce n’est pas le sujet du POC, mais elle sert de repère temporel : on simule un vrai live.
À droite, le panneau latéral affiche en continu les participants actuellement connectés. Le statut de chacun — en ligne, hors ligne, ou “heartbeat” récent — est mis à jour automatiquement via Mercure.
D’un point de vue technique, voici ce qui se passe :
– Lorsqu’un utilisateur rejoint la session, son client publie un événement “presence.update” sur le topic Mercure correspondant à la session.
– Symfony reçoit l’événement et le propage à tous les abonnés (professeur, élèves, autres appareils).
– Le front, abonné via un flux SSE (Server-Sent Events), réagit immédiatement sans aucune requête supplémentaire.
Ce mécanisme, ultra léger, donne une illusion de WebSocket tout en restant 100 % compatible HTTP.
Et c’est précisément ce qui fait la beauté du couple Symfony 7 + Mercure : une expérience de temps réel fluide, sans surcouche complexe.
Et forcément cela se remarque au meme moment sur la page de status pour contrôler tout ceci

La magie de turbo
Le morceau de code Twig ci-dessus résume à lui seul la puissance du combo Symfony 7 + Turbo.
On ne parle pas ici d’un front complexe en React ou Vue, mais bien d’un rendering serveur dynamique, où chaque partie de l’interface peut se mettre à jour de façon autonome — sans jamais recharger la page.
Dans notre exemple, le composant clé est ce bloc :
<turbo-frame id="online-users" src="{{ path('app_classroom_online_users_frame', {id: session.id}) }}">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-users me-2"></i>
Online Participants
</h6>
<span class="badge badge-primary" data-classroom-presence-target="onlineCount">{{ onlineUsers|length }}</span>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush" data-classroom-presence-target="onlineUsers">
{% for user in onlineUsers %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<div class="status-indicator me-2 status-online"></div>
<div>
<div class="fw-medium">{{ user.fullName }}</div>
<small class="text-muted">{{ user.email }}</small>
</div>
</div>
<div class="text-end">
{% if user.id == app.user.id %}
<span class="badge badge-info badge-sm">You</span>
{% endif %}
{% if user.id == session.teacher.id %}
<span class="badge badge-warning badge-sm">Teacher</span>
{% endif %}
<div class="small text-muted">
Joined {{ user.joinedAt|date('H:i') }}
</div>
</div>
</div>
{% endfor %}
{% if onlineUsers|length == 0 %}
<div class="list-group-item text-center text-muted py-4">
<i class="fas fa-user-slash fa-2x mb-2"></i>
<div>No participants online</div>
</div>
{% endif %}
</div>
</div>
</div>
</turbo-frame>
Ce Turbo Frame agit comme une mini-application à l’intérieur de la page. Il charge son contenu via une route Symfony dédiée (app_classroom_online_users_frame
) et se met à jour automatiquement dès qu’un Turbo Stream est diffusé.
Concrètement, lorsqu’un utilisateur rejoint ou quitte la classroom, un événement Mercure est publié.
Symfony capture cet événement et déclenche un Turbo Stream Update qui cible le id="online-users"
.
Résultat : la liste des participants connectés se met à jour instantanément, sans qu’aucune requête AJAX manuelle ne soit envoyée.
Ce modèle repose sur trois piliers :
- Turbo Frame — pour isoler une section de page.
- Turbo Stream — pour injecter des changements côté serveur.
- Mercure (SSE) — pour transporter les événements temps réel jusqu’au navigateur.
Le tout est propulsé par AssetMapper, qui gère les assets front modernes sans build complexe.
Le code reste donc 100 % Symfony, 0 % boilerplate.
Et le plus beau ? Tout ce mécanisme est natif. Pas besoin de React, ni de WebSocket : le framework s’occupe de la magie.
Persistance et stateless : un duo à équilibrer
Pour l’instant, on garde les pieds sur terre côté stockage. Toute la logique de présence utilisateur est persistée dans PostgreSQL, via notre entité Presence
.
Pourquoi ? Parce que si Mercure et Turbo permettent de propager les changements en temps réel, ils ne gardent aucune mémoire de ces états.
Mercure est par nature stateless — il diffuse les messages, puis les oublie. Si on veut pouvoir reconstruire l’état d’une session (savoir qui était connecté, quand, combien de temps, etc.), il faut bien sauvegarder chaque changement.
Dans notre implémentation, dès qu’un événement de connexion ou déconnexion est reçu, le back effectue un flush en arrière-plan.
L’interface utilisateur, elle, se met à jour instantanément via le flux Mercure, tandis que Symfony assure la persistance silencieuse en base.
Ce découplage est crucial :
– Mercure + Turbo gèrent la réactivité et le temps réel.
– PostgreSQL conserve la traçabilité et la cohérence.
C’est cette architecture qui rend le système robuste. Même si le flux SSE tombe ou qu’un client se reconnecte plus tard, l’état global de la session reste consistant et fiable côté serveur.
Conclusion
On tient déjà une base solide et vivante : ce POC démontre que Symfony 7, combiné à Mercure et Turbo, peut offrir une expérience temps réel fluide, sans WebSocket, ni complexité front.
Et pourtant, on n’en est qu’au début.
La suite logique ? Un live chat intégré à la classroom, toujours propulsé par ce duo magique Mercure + Turbo. De quoi enrichir encore plus la notion de présence et d’interaction directe.
Bien sûr, ce projet reste pour l’instant un proof of concept. Mercure, en mode HTTP classique, n’est pas taillé pour une mise en production sans une vraie couche de sécurisation et de scaling. Sans parler de quelques pages dont le design… disons, “expérimental”.
Mais ce genre de prototype, c’est exactement ce qui fait la beauté du développement moderne : explorer, comprendre, détourner les outils pour tester leurs limites.
Et toi, tu t’es déjà amusé avec Mercure sur un POC, un chat, ou un projet un peu fou ? Parce qu’une fois qu’on y a goûté, difficile de revenir en arrière.
Laisser un commentaire