DEV Community

vichanse
vichanse

Posted on

Real Time web applications with the Server Sent Events protocol. Symfony, ApiPlatform, Angular, Akita and Mercure

In this post i will show you how to create a modern reactive application using:

  • PHP 7.4
  • Symfony 5
  • Api Platform 2.5
  • Mercure
  • Angular
  • Akita

The work of this post is inspired by Stefano ALLETTI post

Server-Sent Events (SSE) is a standard that enabled Web servers to push data in real time to clients.

The idea is simple: a browser can subscribe to a stream of events generated by a server, receiving updates whenever a new event occurs. This led to the birth of the popular EventSource interface, which accepts an HTTP stream connection and keeps the connection open while retrieving available data from it. The connection is kept open until closed by calling EventSource.close().

Like a good self-respecting Frenchman, I love wine. That's why I decided to make an application allowing me to know in real time the availability of my favorite wine.
In this post, we will learn how to use SSE by building a live winestore product availability demo application with Symfony, Angular and Mercure. You can find the final code of the application in this GitHub repository.

Building a Real-Time App with Server-Sent Events

In order to learn how to use SSE, we are going to develop a live wine stock availability application. The application will consist of a simple web page showing a list of wines and two modals, one showing wine details and another one for editing the wine quantity.

List Details Update
Alt Text Alt Text Alt Text

Through this real-time app, we can find wine status availability and after implementing Server-Sent Events, we will see automagically updates when the status changes.

Building Real-Time Backends with Server-Sent Events

Create a folder called winestore

For simplicity, i cloned the latest stable version of API Platform (2.5.4) into this folder. You can find inspiration from the project Github repository above.

$ mkdir winestore
Enter fullscreen mode Exit fullscreen mode

Install Symfony and Api Platform

Firstly, make sure you have installed PHP 7.2.5 or a higher version and the Composer package manager to create a new Symfony application. after that, create a new project by executing the following command in the terminal:

$ composer create-project symfony/skeleton api 
Enter fullscreen mode Exit fullscreen mode

Now let's install some necessary bundles with composer

  • Api Platform
$ composer req api 
Enter fullscreen mode Exit fullscreen mode

Make sure your database configurations are set in the .env file

###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
DATABASE_URL=postgres://api-platform:!ChangeMe!@db/api?server_version=12
###< doctrine/doctrine-bundle ###
Enter fullscreen mode Exit fullscreen mode
  • Maker bundle to quickly build entities
$ composer require symfony/maker-bundle --dev
Enter fullscreen mode Exit fullscreen mode

Let's create three entities : Wine, Comment and MediaObject and we define them as ApiResource

  • Hautelook/alice bundle for managing fixtures with Alice and Faker
$ composer require --dev hautelook/alice-bundle 
Enter fullscreen mode Exit fullscreen mode

Create a fixture file wines.yaml

After this create and update the database

$ bin/console doctrine:schema:drop --force --no-interaction
$ bin/console doctrine:schema:create --no-interaction
Enter fullscreen mode Exit fullscreen mode

And load your fixtures with this following command

$ bin/console hautelook:fixtures:load --no-interaction --purge-with-truncate -vvv
Enter fullscreen mode Exit fullscreen mode

Opening the Swagger-Frontend you can now play around adding, deleting and viewing wines.

If you are using the symfony server, you need to open http://localhost:8000/api. If you use docker, the url is http://localhost:8080/api

Alt Text

Mercure : Real-time made easy๐Ÿ˜›

Mercure is an open protocol allowing to push data updates to web browsers and other HTTP clients in a convenient, fast, reliable and battery-friendly way. It is especially useful to publish real-time updates of resources served through web APIs, to reactive web and mobile applications.

If you're following along using the API Platform distribution, you don't need to install mercure as it is already installed and configured.

If you installed from scratch using symfony flex you must:

1) Install mercure
$  composer require mercure
Enter fullscreen mode Exit fullscreen mode
2) Download and run a Mercure hub

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.

Alt Text

An official and open source (AGPL) implementation of a Hub can be downloaded as a static binary from Mercure.rocks. Put the downloaded hub at the root of the project.

Run the following command to start it:
$  ./mercure --jwt-key='!changeMe!' --addr='localhost:3000' --allow-anonymous --cors-allowed-origins='*'
Enter fullscreen mode Exit fullscreen mode
Configuration

The preferred way to configure the Mercure is by using environment variables.

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 (http://localhost:3000/.well-known/mercure 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_TOKEN environment variable.

To create your token got to jwt.io. There you need to fill out two things, the payload and the secret.

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

{
    "mercure": {
        "publish": []
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that you're ready to use Mercure, you must add the Mercure option to the Wine ApiResource entity:

/**
 * @ApiResource(mercure=true,normalizationContext={"groups"={"read"}},denormalizationContext={"groups"={"write"}})
 * @ORM\Entity(repositoryClass="App\Repository\WineRepository")
 */
class Wine
{
...
Enter fullscreen mode Exit fullscreen mode

In this way API Platform will dispatch the updates to the Mercure hub every time a Wine is created, updated or deleted.

Building Angular with Server-Sent Events

First thing, we need to generate an angular application. This is made very easy using the angular cli

Go in the winestore folder and execute following commands:

# if you don't already have the cli installed
$ npm install -g @angular/cli
# generate new app
$ ng new api
# cd into the app
$ cd api
# Start it up
$ ng serve
Enter fullscreen mode Exit fullscreen mode

Open your web-browser of choice and navigate to http://localhost:4200 (4200 is the default port for angular apps, but you can specify whatever port your heart desires). If all went well, you should see the angular default landing page.

In this post i will focus on Angular and SSE. A full example of the client application can be found here

SSE and Angular

On the client side, we use the EventSource to receive the stream events. This object has 3 eventHandler methods:

  1. EventSource.onerror called when an error occurs,
  2. EventSource.onmessage called when a message event is received
  3. EventSource.onopen called when an open event is received
EventSource Observable Wrapper

To make it reactive and fit in an Angular application, itโ€™s better to wrap it in an RxJS Observable.

Here is the sse.service.ts file

import { Injectable, NgZone } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root',
})
export class SseService {
    constructor(private zone: NgZone) {}
    private getEventSource(url: string): EventSource {
        return new EventSource(url);
    }

    getServerSentEvent(url: string): Observable<MessageEvent> {
        return new Observable(observer => {
            const eventSource = this.getEventSource(url);

            eventSource.onmessage = event => {
                this.zone.run(() => observer.next(event));
            };
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The service method getServerSentEventcan then be subscribed like any other observable:

this.sseService.getServerSentEvent('url/parameters').subscribe(data => console.log(data));
Enter fullscreen mode Exit fullscreen mode

State Management with Akita

In order to keep track of what has been updated, we are going to use akita as a state management.

Akita is a state management pattern, built on top of RxJS. It is based on objected-oriented design so developers with OOP experience should feel right at home.

Alt Text

๐Ÿ‘‰๐Ÿพ 10 Reasons Why You Should Start Using Akita as Your State Management Solution

Akita has an API runStoreAction that simplify and improve the experience of working with event-based APIs such as Mercure in our example.

Setting Up Akita

Adding Akita to our project is easy. We can use the NG add schematic by running the following command:

ng add @datorama/akita
Enter fullscreen mode Exit fullscreen mode

The above command adds Akita, Akita's dev-tools, and Akita's schematics into our project. The next step is to create a store. We need to maintain a collection of wines, so we scaffold a new entity feature:

ng g af wines
Enter fullscreen mode Exit fullscreen mode

This command generates a wines store, a wines query, a wines service, and a wine model for us. Those files can be found here

Using runStoreActionin our service

...
import { runStoreAction, StoreActions } from '@datorama/akita';

@Injectable({ providedIn: 'root' })
export class WinesService extends NgEntityService<WinesState> {
...
sync() {
        const url = new URL(APP_CONFIG.merculeHubUrl);
        url.searchParams.append('topic', `${APP_CONFIG.apiWineUrl}/{id}`);

        return this.sseService.getServerSentEvent(`${url}`).pipe(
            tap(event => {
                const data = JSON.parse(event.data);
                const stock = data.stock;
                this.notificationService.add();
                runStoreAction('wines', StoreActions.UpdateEntities, {
                    payload: {
                        data: { stock },
                        entityIds: data.id,
                    },
                });
            }),
        );
    }
...
}
Enter fullscreen mode Exit fullscreen mode

In our service sync method we subscribe to server-sent events and update our store by running store action UpdateEntities

Summary

In this post, we used Server-Sent Events to develop a real-time application that simulates product availability in a winestore. During the development, we had the opportunity to explore Symfony, Api Platform, Mercure, Angular and Akita.
There is a lot more to develop such modern reactive applications. This post provides a good starting point but i highly recommend going through the docs and trying out different tools.

๐Ÿ“ข An example project containing all of the above can be found here

Have you enjoyed the post? Please share๐Ÿค˜๐Ÿพ.

Top comments (0)