Symfony News

Symfony Gets Real-time Push Capabilities!

Being able to broadcast data in real-time from servers to clients is a requirement for many modern web and mobile applications.

Because the most popular PHP SAPIs aren't able to maintain persistent connections, Symfony wasn't providing any built-in method to push data to clients until now. It was mandatory to rely on external service providers or on other programming languages to implement such feature.

This time is over! Say hello to the Mercure component and to the MercureBundle!

As their names indicate, they implement Mercure, an open protocol designed from the ground up to publish updates from server to clients. It is a modern and efficient alternative to timer-based polling and to WebSocket.

These two newcomers in the Symfony family are specifically designed for use cases requiring real-time capabilities such as:

  • Creating an UI reacting in live to changes made by other users (e.g. a user changes the data currently browsed by several other users, all UIs are instantly updated)
  • notifying the user when an asynchronous job has been completed
  • creating chat applications

Because it is built on top Server-Sent Events (SSE), Mercure is supported out of the box in most modern browsers (Edge and IE require a polyfill) and has high-level implementations in many programming languages.

Mercure comes with an authorization mechanism, automatic re-connection in case of network issues with retrieving of lost updates, "connection-less" push for smartphones and auto-discoverability (a supported client can automatically discover and subscribe to updates of a given resource thanks to a specific HTTP header).

All these features are supported in the Symfony integration!

Unlike WebSocket, which is only compatible with HTTP 1.x, Mercure leverages the multiplexing capabilities provided by HTTP/2 and HTTP/3 (but also supports older versions of HTTP).

In this recording you can see how a Symfony web API leverages Mercure and API Platform to update in live a React app and a mobile app (React Native) generated using the API Platform client generator:

Install

The first step to be able to "push" with Symfony is to install the official Symfony Flex recipe for the Mercure integration:

1
$ composer require mercure

To manage persistent connections, Mercure relies on a Hub: a dedicated server that handles persistent SSE connections with the clients. The Symfony app publishes the updates to the hub, that will broadcast them to clients.

An official and open source (AGPL) implementation of a Hub can be downloaded as a static binary from Mercure.rocks.

Run the following command to start it:

1
$ JWT_KEY='aVerySecretKey' ADDR='localhost:3000' ALLOW_ANONYMOUS=1 ./mercure

Note

Alternatively to the binary, a Docker image, a Helm chart for Kubernetes and a managed, High Availability Hub are also provided by Mercure.rocks.

Tip

The API Platform distribution comes with a Docker Compose configuration as well as a Helm chart for Kubernetes that are 100% compatible with Symfony, and contain a Mercure hub. You can copy them in your project, even if you don't use API Platform.

Now, set the URL of your hub as the value of the MERCURE_PUBLISH_URL env var. The .env file of your project has been updated by the Flex recipe to provide example values. Set it to the URL of the Mercure Hub (https://localhost:3000/hub by default).

In addition, the Symfony application must bear a JSON Web Token (JWT) to the Mercure Hub to be authorized to publish updates. This JWT should be stored in the MERCURE_JWT_SECRET environment variable.

The JWT must be signed with the same secret key than the one used by the Hub to verify the JWT (aVerySecretKey in our example). Its payload must contain at least the following structure to be allowed to publish:

1
2
3
4
5
{
    "mercure": {
        "publish": []
    }
}

Because the array is empty, the Symfony app will only be authorized to publish public updates.

Tip

The jwt.io website is a convenient way to create and sign JWTs. Checkout this example JWT, that grants publishing rights for all targets (notice the star in the array). Don't forget to set your secret key properly in the bottom of the right panel of the form!

Caution

Don't put the secret key in MERCURE_JWT_SECRET, it will not work! This environment variable must contain a JWT, signed with the secret key.

Also, be sure to keep both the secret key and the JWTs... secrets!

Publishing

The Mercure Component provides an Update value object representing the update to publish. It also provides a Publisher service to dispatch updates to the Hub.

The Publisher service can be injected in any other services, including controllers, using the service autowiring feature:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/PublishController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Publisher;
use Symfony\Component\Mercure\Update;

class PublishController
{
    public function __invoke(Publisher $publisher): Response
    {
        $update = new Update(
            'https://example.com/books/1',
            json_encode(['status' => 'OutOfStock'])
        );

        // The Publisher service is an invokable object
        $publisher($update);

        return new Response('published!');
    }
}

The first parameter to pass to the Update constructor is the topic being updated. This topic should be an IRI (Internationalized Resource Identifier, RFC 3987): a unique identifier of the resource being dispatched.

Usually, this parameter contains the original URL of the resource transmitted to the client, but it can be any valid IRI, it doesn't have to be an URL that exists (similarly to XML namespaces).

The second parameter of the constructor is the content of the update. It can be anything, stored in any format. However, serializing the resource in a hypermedia format such as JSON-LD, Atom, HTML or XML is recommended.

Subscribing

Subscribing to updates in JavaScript is straightforward:

1
2
3
4
5
const es = new EventSource('https://localhost:3000/hub?topic=' + encodeURIComponent('https://example.com/books/1'));
es.onmessage = e => {
    // Will be called every time an update is published by the server
    console.log(JSON.parse(e.data));
}

As you can see, you don't need any JS library or SDK. It just works!

Mercure also allows to subscribe to several topics, and to use URI Templates as patterns:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// URL is a built-in JavaScript class to manipulate URLs
const u = new URL('https://localhost:3000/hub');
u.searchParams.append('topic', 'https://example.com/books/1');
// Subscribe to updates of several Book resources
u.searchParams.append('topic', 'https://example.com/books/2');
// All Review resources will match this pattern
u.searchParams.append('topic', 'https://example.com/reviews/{id}');

const es = new EventSource(u);
es.onmessage = e => {
    console.log(JSON.parse(e.data));
}

Tip

Google Chrome DevTools natively integrate a practical UI displaying in live the received events.

To use it:

  • open the DevTools
  • select the "Network" tab
  • click on the request to the Mercure hub
  • click on the "EventStream" sub-tab.

Tip

Test if a URI Template match an URL using the online debugger.

Async dispatching

Instead of calling the Publisher service directly, you can also let Symfony dispatching the updates asynchronously thanks to the provided integration with the Messenger component.

First, install the Messenger component:

1
$ composer require messenger

You should also configure a transport (if you don't, the handler will be called synchronously).

Now, you can just dispatch the Mercure Update to the Messenger's Message Bus, it will be handled automatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/PublishController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mercure\Update;

class PublishController
{
    public function __invoke(MessageBusInterface $bus): Response
    {
        $update = new Update(
            'https://example.com/books/1',
            json_encode(['status' => 'OutOfStock'])
        );

        // Sync, or async (RabbitMQ, Kafka...)
        $bus->dispatch($update);

        return new Response('published!');
    }
}

Web APIs

When creating a web API, it's convenient to be able to instantly push new versions of the resources to all connected devices, and to update their views.

API Platform can use the Mercure Component to dispatch updates automatically, every time an API resource is created, modified or deleted.

Start by installing the library using its official recipe:

1
$ composer require api

Then, creating the following entity is enough to get a fully-featured hypermedia API, and automatic update broadcasting through the Mercure hub:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
* @ApiResource(mercure=true)
* @ORM\Entity
*/
class Book
{
    /**
     * @ORM\Id
     * @ORM\Column
     */
    public $name;

    /**
     * @ORM\Column
     */
    public $status;
}

As showcased in the previous recording, the API Platform Client Generator also allows to scaffold complete React and React Native apps from this API. These apps will automatically render the content of Mercure updates in real-time.

Checkout the dedicated API Platform documentation to learn more about its Mercure support.

Going Further

There is more! Symfony leverages all the features of the Mercure protocol, including authorization, discoverability through Web Linking and advanced SSE configuration. Checkout the brand new Symfony Docs articles about Mercure to learn all the details!


Be trained by Symfony experts - 2019-03-11 Clichy - 2019-03-11 Clichy - 2019-03-13 Clichy


About us

What a Symfony developer should know about the framework: News, Jobs, Tweets, Events, Videos,...

Resources

Find us on Twitter

Find us on Facebook