Instant Messaging in Symfony: A Deep Dive into Mercure Integration

Jakub Skowron (skowron.dev)
5 min readOct 18, 2023

--

Photo by Christopher Robin Ebbinghaus on Unsplash

Modern web and mobile applications increasingly require the ability to transmit data in real-time from the server to the client. Symfony, a popular PHP framework, did not offer a built-in method for “pushing” data to clients until recently. However, this changed thanks to the Mercure component.

Use Case: Interactive Customer Service Chat

Imagine that we run an online store and want to enhance customer satisfaction by introducing an interactive chat with customer service. The business process would look like this:

  1. Registration/Login: The customer enters the site and has the option to log in or register.
  2. Department Selection: After logging in, the customer can choose the department they want to contact (e.g., technical support, returns, product information).
  3. Starting a Chat: The customer types their question or issue and then waits for an available representative.
  4. Real-time Communication: Thanks to Mercure, the customer and representative can communicate in real-time, exchanging messages and files.
  5. Ending the Chat: After resolving the issue, the chat is closed, and the customer has the opportunity to rate the service.
  6. Conversation Archiving: All messages are saved and can be viewed in the future by both the customer and the representative.

What is WebSocket?

WebSocket is a communication protocol that provides a full bi-directional connection between the server and the client over a single, persistent TCP connection. Unlike traditional HTTP requests, which are one-way, WebSocket allows continuous data exchange between the server and the client without the need to establish a connection multiple times.

Main features of WebSocket:

  • Bi-directionality: The server and client can send messages independently of each other.
  • Real-time: Data is transmitted almost immediately after it is generated.
  • Lower overhead: The absence of the need to establish a connection multiple times reduces latency and server load.

Get to Know Mercure

Mercure is a tool that makes it easy to add push functionality to applications. It’s a communication protocol that allows servers to send messages to clients in real-time. Mercure is language-independent and can be used with any programming language or platform. In Symfony, Mercure is available as a bundle, making it easy to integrate with Symfony applications.

Installation and Configuration of Mercure

Using mercure.rocks

If you don’t want to install Mercure locally, you can use the mercure.rocks platform. Here are the steps to start using this platform:

  1. Registration: Go to mercure.rocks and sign up by creating a new account.
  2. Creating a New Instance: After logging into the panel, click the “Create a new instance” button. Enter a name for your instance and choose the appropriate plan.
  3. Instance Configuration: After creating an instance, you can configure various options such as CORS domains, JWT, etc.
  4. Starting Communication: Use the provided URL endpoint to integrate Mercure with your Symfony application. You can do this by adding the URL to the Mercure configuration in Symfony.
  5. Managing the Instance: In the mercure.rocks panel, you have full control over your instance. You can monitor activity, manage subscriptions, and much more.

Running Mercure Locally Using Docker Compose

If you prefer to run Mercure locally, you can use Docker Compose. Here’s a sample docker-compose.yml file:

version: '3.7'

services:
mercure:
image: dunglas/mercure
environment:
- JWT_KEY='yourJWTkey'
- DEMO=1
- ALLOW_ANONYMOUS=1
- CORS_ALLOWED_ORIGINS=*
- PUBLISH_ALLOWED_ORIGINS=http://localhost
ports:
- "80:80"

After creating the docker-compose.yml file, you can run Mercure using the following command:

docker-compose up -d

After starting, Mercure will be available at http://localhost.

Practical Implementation in Symfony 6

Suppose we want to create a simple real-time chat in our Symfony application. Users will be able to join different rooms and send messages, which will be immediately visible to all other users in that room.

Backend Implementation

First install Mercure bundle, you can use Composer:

composer require symfony/mercure-bundle

YAML Configuration for the Mercure Bundle

# config/packages/mercure.yaml

mercure:
enable_profiler: '%kernel.debug%'
hubs:
default:
url: 'http://localhost/.well-known/mercure'
jwt: '%env(MERCURE_JWT_TOKEN)%'

Value Objects Value Objects represent descriptive aspects of the domain and are immutable.

namespace Domain\ValueObject;

class RoomName
{
private $value;
public function __construct(string $value)
{
if (empty($value)) {
throw new \InvalidArgumentException('Room name cannot be empty.');
}
$this->value = $value;
}
public function getValue(): string
{
return $this->value;
}
}
class MessageContent
{
private $value;
public function __construct(string $value)
{
if (empty($value)) {
throw new \InvalidArgumentException('Message content cannot be empty.');
}
$this->value = $value;
}
public function getValue(): string
{
return $this->value;
}
}

DTO (Data Transfer Object) DTO is used to transfer data between application layers.

namespace Application\DTO;

class SendMessageDTO
{
public $roomId;
public $roomName;
public $messageContent;
}

Sending Message Logic Using Mercure

namespace Infrastructure\Adapter;

use Domain\Model\Message;
use Domain\Port\MessageSender;
use Symfony\Component\Mercure\PublisherInterface;
use Symfony\Component\Mercure\Update;
class MercureMessageSender implements MessageSender
{
private $publisher;
public function __construct(PublisherInterface $publisher)
{
$this->publisher = $publisher;
}
public function sendMessage(Message $message)
{
$update = new Update(
'/chat/' . $message->getRoomId(),
json_encode(['content' => $message->getContent()->getValue()])
);
$this->publisher->__invoke($update);
}
}

Integration with Symfony

// src/Controller/ChatController.php

use Application\DTO\SendMessageDTO;
use Application\Service\ChatService;
use Domain\Model\ChatRoom;
use Domain\Model\Message;
use Domain\ValueObject\RoomName;
use Domain\ValueObject\MessageContent;
use Symfony\Component\HttpFoundation\Response;
class ChatController extends AbstractController
{
private $chatService;
public function __construct(ChatService $chatService)
{
$this->chatService = $chatService;
}
public function sendMessage(SendMessageDTO $sendMessageDTO): Response
{
$roomName = new RoomName($sendMessageDTO->roomName);
$messageContent = new MessageContent($sendMessageDTO->messageContent);
$room = new ChatRoom($sendMessageDTO->roomId, $roomName);
$message = new Message($sendMessageDTO->messageId, $messageContent, new \DateTime());
$this->chatService->sendMessageToRoom($room, $message);
return new JsonResponse(['status' => 'success']);
}
}

YAML Configuration for the Mercure Bundle

# config/packages/mercure.yaml

mercure:
enable_profiler: '%kernel.debug%'
hubs:
default:
url: 'https://your-mercure-hub.example.com/.well-known/mercure'
jwt: '%env(MERCURE_JWT_TOKEN)%'

Frontend Implementation

On the frontend side, you would use JavaScript to subscribe to the Mercure updates and update the chat in real-time. Here’s a basic example using vanilla JavaScript:

const url = new URL('http://your-app/.well-known/mercure');
url.searchParams.append('topic', 'http://your-app/chat');

const eventSource = new EventSource(url);
eventSource.onmessage = event => {
const data = JSON.parse(event.data);
const chatBox = document.getElementById('chat-box');
const messageElement = document.createElement('div');
messageElement.innerText = data.message;
chatBox.appendChild(messageElement);
};

Conslusion

Modern web and mobile applications increasingly require the ability to transmit data in real-time from the server to the client. Symfony, a popular PHP framework, did not offer a built-in method for “pushing” data to clients until recently. However, this changed with the introduction of the Mercure component. The article above provides practical guidance on implementing a real-time chat in Symfony 6 using Mercure. It demonstrates how to configure and use Mercure in a Symfony application, both locally and through the mercure.rocks platform.

--

--

Jakub Skowron (skowron.dev)

Poland based PHP/Python Web Backend dev. Love to work with Symfony and FastAPI frameworks. In spare time totally gearhead.