Créez un chat avec Mercure et Symfony

Créez un chat avec Mercure et Symfony


Discovery du Hub Mercure

Notre client (le navigateur web) doit connaître l'URL du Hub pour pouvoir s'abonner à ses updates. Or, seule notre application Symfony connaît cette adresse, il faut donc la transmettre à nos clients. Pour cela, on utilise le mécanisme de Discovery de Mercure, en envoyant au client les informations nécessaires de notre Hub. Rendez-vous dans votre ChannelController, et ajoutez un Link à la réponse de l'action chat, comme ceci :

/** * @Route("/chat/{id}", name="chat") */ public function chat( Request $request, // Autowire the request object Channel $channel, MessageRepository $messageRepository ): Response { $messages = $messageRepository->findBy([ 'channel' => $channel ], ['createdAt' => 'ASC']); $hubUrl = $this->getParameter('mercure.default_hub'); // Mercure automatically define this parameter $this->addLink($request, new Link('mercure', $hubUrl)); // Use the WebLink Component to add this header to the following response return $this->render('channel/chat.html.twig', [ 'channel' => $channel, 'messages' => $messages ]); }

On ajoute un header de type Link à la réponse, avec l'URL de notre Hub. On récupère pour cela l'objet $request de la requête récupérée par le controller.

Il n'y a plus qu'à récupérer cette information dans le template templates/channel/chat.html.twig. Dans une application Client - API classique qui renverrait une réponse HTTP, il suffirait de récupérer les headers en Javascript comme ceci :

const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1]; // Cf documentation Symfony - Mercure

Cependant notre application renvoie une réponse Twig, c'est un chouia plus complexe de récupérer ce header :

// In the Hub URL, 'mercure' is used as the docker-compose service name. We replace it by the actual localhost url for the browser. const link = '{{ app.request.attributes.get('_links').getLinksbyRel('mercure')[0].getHref }}' .replace("mercure", "localhost:3000");

Il est ici important de remplacer la partie mercure de l'URL par celle réellement accessible depuis le client (localhost:3000). Et voilà, on connaît l'URL de notre Hub, qu'on peut stocker dans une variable de type URL :

const url = new URL(link);

S'abonner aux nouveaux messages

Maintenant qu'on connaît l'adresse du Hub, il suffit de définir le topic auquel on souhaite s'abonner. Il est nécessaire pour cela de définir un topic, sous la forme d'une URI, qui représente la ressource que l'on souhaite "écouter". Or, ce sont les nouveaux messages d'un canal de discussion en particulier que nous souhaitons recevoir automatiquement. Choisissons donc arbitrairement ce topic : http://astrochat.com/channel/{id} (en précisant l'Id du channel courant).

Quand nous publierons des updates, il faudra être cohérent et les publier sur ce même topic.

Pour le moment, finissons la partie abonnement. C'est avec l'API Javascript EventSource qu'on écoutera les événements publiés par notre Hub, et que nous traiterons les données. Je propose de traiter le tout comme ceci :

url.searchParams.append('topic', 'http://astrochat.com/channel/{{ channel.id }}'); // On ajoute le topic souhaité aux paramètres de la requête vers le Hub const eventSource = new EventSource(url); // On s'abonne au Hub const appUser = {{ app.user.id }}; eventSource.onmessage = ({data}) => { // On écoute les événements publiés par le Hub const message = JSON.parse(data); // Le contenu des événements est sous format JSON, il faut le parser document.querySelector('.bg-light').insertAdjacentHTML( // On injecte le nouveau message selon le HTML déjà présent plus haut dans notre fichier Twig 'beforeend', appUser === message.author.id ? `<div class="row w-75 float-right"> <b>${message.author.username}</b> <p class="alert alert-info w-100">${message.content}</p> </div>` : `<div class="row w-75 float-left"> <b>${message.author.username}</b> <p class="alert alert-success w-100">${message.content}</p> </div>` ) chatDiv.scrollTop = chatDiv.scrollHeight; // On demande au navigateur de scroller le chat tout en bas pour bien apercevoir le dernier message apparu }

On crée donc notre objet EventSource en lui passant l'URL de notre Hub, et on injecte l'Id du channel depuis la variable Twig correspondante. La méthode onmessage sera appelée à chaque nouvel événement publié par le Hub.

C'est bon, votre client est capable de recevoir les futures Updates. On injecte simplement une nouvelle div html pour chaque nouveau message afin de peupler le chat au fur et à mesure, sans avoir à rafraîchir la page.

Maintenant, ces messages, il faut les publier sur le Hub depuis le serveur !

Publier les messages sur le Hub

Le package Mercure vient avec certaines méthodes qui rendent extrêmement simples les interactions avec notre Hub, notamment un objet Update pour construire notre Update, et un Publisher pour publier cette dernière sur le Hub. C'est dans le MessageController que nous allons traiter cela, au moment de la réception d'un nouveau message. Injectez-le PublisherInterface, créez et publiez votre update. Voici comment j'ai modifié l'action sendMessage pour arriver à ce résultat (comme d'habitude, j'ai commenté les lignes qui ont changé) :

// ... use Symfony\Component\Mercure\PublisherInterface; use Symfony\Component\Mercure\Update; // ... /** * @Route("/message", name="message", methods={"POST"}) */ public function sendMessage( Request $request, ChannelRepository $channelRepository, SerializerInterface $serializer, EntityManagerInterface $em, PublisherInterface $publisher ): JsonResponse { // ... $update = new Update( // Création d'une nouvelle update sprintf('http://astrochat.com/channel/%s', // On précise le topic, avec pour Id l'identifiant de notre Channel $channel->getId()), $jsonMessage, // On y passe le message serializer en content value ); $publisher($update); // Le Publisher est un service invokable. On peut publier directement l'update comme cela return new JsonResponse( $jsonMessage, Response::HTTP_OK, [], true ); }

Comme vous le constatez, nous avons précisé le même topic que celui sur lequel notre javascript écoute les événements.

Essayez d'envoyer un message depuis le chat. Normalement, ce dernier devrait apparaître automatiquement ! Votre messagerie fonctionne désormais en temps réel grâce à Mercure !

Ouf, vous avez fait la majorité du chemin, mais ce n'est pas fini ! Quid de la sécurité dans tout ca ?

Justement c'est l'objet de la prochaine et dernière partie, accrochez-vous encore un tout petit peu !

Vous pouvez vous rendre sur cette branche pour être à jour sur cette étape du tutoriel, et continuer sereinement vers la prochaine partie.

Auteur(s)

Arthur Jacquemin

Arthur Jacquemin

Développeur de contenu + ou - pertinent @ ElevenLabs_🚀

Voir le profil

Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !

Notre équipe d'experts répond à toutes vos questions.

Nous contacter

Découvrez nos autres contenus dans le même thème

Astronaute revenant de mission

Retour sur le Forum PHP 2024

Découvrez un résumé concis des conférences qui nous ont le plus marqué lors du Forum PHP 2024 !

À la découverte de l'Anchor positioning API

La nouvelle Anchor positioning API en CSS

L'Anchor positioning API est arrivée en CSS depuis quelques mois. Expérimentale et uniquement disponible à ce jour pour les navigateurs basés sur Chromium, elle est tout de même très intéressante pour lier des éléments entre eux et répondre en CSS à des problématiques qui ne pouvaient se résoudre qu'en JavaScript.