Server-Sent events with PHP and Symfony

AlessandroMinoccheri
7 min readMay 25, 2022

--

In the magic world of PHP, there is a library for many things that you don’t know, every day new libraries are born and it’s impossible to know each of them.
For this reason, I try always to follow on Twitter, LinkedIn, and GitHub good developers that are sharing new approaches and libraries.
For one of our customers, we needed an application where the server can send data to all clients.
So we need to build the software with a Server-Sent events approach in a short time to release the product as soon as possible to validate the customer idea.

If you would like more about Server-Sent events, what it means, advantages and limits, you can read my previous article here.

To realize a Server-Sent Events in PHP we found a Symfony library symfony/mercure, I saw this library in some article on LinkedIn and it’s the proof that following the right people can be very important.

How to install it

We can use Flex to install it so, you can write into your CLI::

composer require mercure

After that, you can configure Mercure environment variables into the file: config/packages/mercure.yaml

mercure:
hubs:
default:
url: https://mercure-hub.example.com/.well-known/mercure
jwt:
secret: '!ChangeMe!'
publish: ['foo', 'https://example.com/foo']
subscribe: ['bar', 'https://example.com/bar']
algorithm: 'hmac.sha256'
provider: 'My\Provider'
factory: 'My\Factory'
value: 'my.jwt'

Let me explain what are the values to set into the configuration file:

secret: the key to use to sign the JWT (all other options, besides algorithm, subscribe, and publish will be ignored)
publish: a list of topics to allow publishing when generating the JWT (only usable when secret or factory are provided)
subscribe: a list of topics to allow subscribing to when generating the JWT (only usable when secret, or factory are provided)
algorithm: The algorithm to use to sign the JWT (only usable when the secret is provided)
provider: The ID of service to call to provide the JWT (all other options will be ignored)
factory: The ID of service to call to create the JWT (all other options, besides subscribe, and publish will be ignored)
value: the raw JWT to use (all other options will be ignored)

Automatically inside your .env will be updated with environment variables.

If you are using a Symfony version less than 6, the YAML file doesn’t exist and you need only to configure your environment variables into the .env file like this:

MERCURE_URL=http://localhost:9090/.well-known/mercure
MERCURE_PUBLIC_URL=http://localhost:9090/.well-known/mercure
MERCURE_JWT_SECRET=”JWT-AuctionEngine”

Using a specific Docker container

You can use an official docker for your local environment, with a simple configuration like this:

version: "3.5"services:
mercure:
container_name: mercure_sse_poc
image: dunglas/mercure
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: 'JWT-AuctionEngine'
MERCURE_SUBSCRIBER_JWT_KEY: 'JWT-AuctionEngine'
MERCURE_CORS_ALLOWED_ORIGINS: '*'
MERCURE_EXTRA_DIRECTIVES: |-
cors_origins "*"
anonymous 1
ports:
— "9090:80"
— "4433:443"

JWT_KEY must be the same as your environment variables, in this little example, I declare that it’s not necessary to be authenticated and you can use Mercure from all domains.
In production, I recommend restricting those parameters to avoid undesired problems.

How to use Symfony/Mercure

To send data to your client you need to instantiate a Mercure object called Update with 2 arguments: the topic name where you want to send your data, and the data encoded.
After that, you can publish your event using an object that implements HubInterface (for this example).
But symfony/mercure has already its implementation so you can use it without writing your implementation.
Here is a little example:

public function send(Request $request, HubInterface $hub): void
{
$update = new Update(
'channelname',
json_encode([
'foo' => 'bar'
], JSON_THROW_ON_ERROR)
);
$hub->publish($update);
}

The object Update has different arguments (directly from https://mercure.rocks/spec):

topic: The identifiers of the updated topic. It is RECOMMENDED to use an IRI as an identifier. If this name is present several times, the first occurrence is considered to be the canonical IRI of the topic, and other ones are considered to be alternate IRIs. The hub MUST dispatch this update to subscribers that are subscribed to both canonical or alternate IRIs.
data (optional): the content of the new version of this topic.
private (optional): if this name is set, the update MUST NOT be dispatched to subscribers not authorized to receive it. See authorization. It is recommended to set the value to on but it CAN contain any value including an empty string.
id (optional): the topic’s revision identifier: it will be used as the SSE’s id property. The provided id MUST NOT start with the # character. The provided id SHOULD be a valid IRI. If omitted, the hub MUST generate a valid IRI (RFC3987). A UUID (RFC4122) or a DID MAY be used. Alternatively, the hub MAY generate a relative URI composed of a fragment (starting with #). This is convenient to return an offset or a sequence that is unique for this hub. Even if provided, the hub MAY ignore the id provided by the client and generate its id.
type (optional): the SSE’s event property (a specific event type).
retry (optional): the SSE’s retry property (the reconnection time).

Frontend

The frontend client needs to subscribe to the topic, the following example is a piece of a React component:

const url = 'http://127.0.0.1:9090/.well-known/mercure?topic=auctions-1';
const eventSource = new EventSource(url);
eventSource.onmessage = event => {
const results = JSON.parse(event.data);
console.log(results);
}

You can use this code for example in a UseEffect function of React and you are telling that you want to subscribe to the topic auctions-1 and every event sent to that channel you will print it into the console to see what you have received and you can use those data for whatever you want.

What happens?

I have written a little example here to understand well what happens: https://github.com/AlessandroMinoccheri/sse-poc
Now if you clone the repository and follow the instructions to install the backend and frontend you can visit the page: http://localhost:8080/
You can see a simple form to submit a bid offer.

Now, if you open the browser network, select Fetch/XHR (for Chrome), and reload the page you can see a subscription request to a specific topic.

Then, if you insert a number into the text field and click the button to make a bid, you can see in the network tab a new POST request for the bid, but inside the previous request in the tab Events, you can see a new event sent by the server with new information!

So in the same subscription request, we can receive events for the server, for this reason, the Server-Sent events approach is considered a monodirectional communication because is the server that sends data to the client, and the client decides only to subscribe to topics.

If you make other bids you can see in the tab events all data sent from the server in the same HTPP connection.

Subscribe to multiple topics

Is it possible for the client to subscribe to multiple topics? Yes, absolutely, but how?
Well, you can easily do this thing into your frontend app (in this example using React):

const url = 'http://127.0.0.1:9090/.well-known/mercure?topic=auctions-1';
const eventSource = new EventSource(url);
eventSource.onmessage = event => {
const results = JSON.parse(event.data);
console.log(results);
}
const anotherUrl = 'http://127.0.0.1:9090/.well-known/mercure?topic=anotherTopic';
const anotherEventSource = new EventSource(anotherUrl);
anotherEventSource.onmessage = event => {
const results = JSON.parse(event.data);
console.log(results);
}

You can subscribe to many topics in this way because the server will send data to a specific topic.
You can use this approach to create a private topic for a single user, or group of users to send events only to them.

Asynchronous dispatching

You can write your implementation to dispatch asynchronous events, but this is not recommended because Mercure already sends them asynchronously.
But if you want, you can use Symfony Messenger to dispatch it by yourself.
Let’s see how the previous example could be with your own implementation:

public function send(Request $request, MessageBusInterface $customBus): void
{
$update = new Update(
'channelname',
json_encode([
'foo' => 'bar'
], JSON_THROW_ON_ERROR)
);
$customBus->publish($update);
}

As you can see the code doesn’t change, you can use a different implementation.
But remember that Mercure already sends events asynchronously so it depends on you if you want to maintain by yourself the dispatching system.

Use Case Examples

There are many examples of using the Server-Sent events approach.
Imagine that you need to build a stock option client, the frontend application can subscribe to topics to receive data from the server only when a stock quote changes its value.
Or if you need to build a Twitter feed update, the server will send data when to every user (with a custom topic each) with new tweets.
Recently I built a push notification system where clients receive information and updates from the server like the common push notifications app on your phone.

Conclusions

There are a lot of useful resources and examples, in my opinion using symfony/mercure library has simplified my life because in some minutes you can create a system ready to send events to the client without a lot of configurations and code to do.
Using the right tool can be important to the success of the project, but to know it you don’t have to start from the implementation.

Start understanding what is the real problem to solve, I usually do an EventStorming before coding because we need to know the domain and business logic.

After that, you can understand better if Server-Sent Events is the right approach or not.

--

--

AlessandroMinoccheri
AlessandroMinoccheri

Responses (2)