Yii 2 + Mercure

Mercure

Some of you might already heard about Mercure. It is quite new thing but it definitely rocks. Quoting its author Kévin Dunglas:

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

Since I have started using Symfony with API Platform and discovered how well it works with Mercure I said to myself - I need something like this for Yii as well. And I think I manage to get it.

The idea

We have got the Active Record class representing the API resource. API itself is already provided at our server, handled by Yii. Clients can connect to it, fetch the list of resources, create, update, and delete one. All like requested.

Let’s make this concept more solid now. API serves the shoe shop and API clients are the mobile apps used by the customers.

Imagine the situation when two customers browse the same shoes page but there is only one pair available in stock. First customer buys the pair reducing the stock number to zero. Then the second customer, looking at the same page where stock number still says 1 available (he did not refresh the page) tries to buy it. Result? Several things might happen now. Error message. Sad pop up with “we are sorry” text. Maybe 404 page (oh noes!).

We can avoid it with Mercure. Immediately after first customer’s action updates the stock count Mercure update is published to all clients browsing the page. And clients can immediately update the stock information without re-fetching the resource.

Ok, how to do it? Using Mercure behavior.

Yii 2 Mercure behavior

Installation

Add the package to your composer.json :

{
    "require": {
        "bizley/mercure-behavior": "^1.0"
    }
}

and run composer update or alternatively run composer require bizley/mercure-behavior:^1.0

You will of course need Mercure Hub as well. Refer to dunglas/mercure for the instructions how to get one (I recommend using Docker image).

Usage

Add this behavior to the resource object you want to be subject of Mercure updates (usually it’s an Active Record instance).

use \bizley\yii2\behaviors\mercure\MercureBehavior;

public function behaviors()
{
    return [
        MercureBehavior::class
    ];
}

By default MercureBehavior will dispatch update to Mercure Hub in JSON format after the resource has been successfully created, updated, or deleted, using the Mercure publisher component registered under the ‘publisher’ name.

You can customize the configuration according to your needs, for example:

public function behaviors()
{
    return [
        [
            'class' => MercureBehavior::class,
            'publisher' => \bizley\yii2\mercure\Publisher::class,
            'format' => \yii\web\Response::FORMAT_XML
        ]
    ];
}

Resource object must implement \bizley\yii2\behaviors\mercure\MercureableInterface. This interface provides methods used in Mercure update:

  • getTopic() Returns 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). In our example this could be https://api.shoeshop/shoes/1. Remember that this must be unique for the resource (not the whole class) so it should be dynamically updated with resource ID.
  • getId() Returns the ID that can uniquely identify a resource. In our example this will be 1.
  • getMercureTarget() Returns the list of Mercure publisher targets. For public updates set to [’*’] - otherwise provide specific targets to dispatch updates only to authorized clients. In our example this can be left as ['*']. See Authorization section below to learn more.

Publisher

MercureBehavior uses yii2-mercure package as publisher. You can use it separately of the behavior as well (installation instructions are provided in the repository documentation).

Configuration

You can register publisher in your app configuration

'components' => [
    'publisher' => [
        'class' => \bizley\yii2\mercure\Publisher::class,
        // options here
    ],
],

so it can be used in behavior automatically or you can provide the whole publisher configuration in behavior configuration like:

public function behaviors()
{
    return [
        [
            'class' => MercureBehavior::class,
            'publisher' => [
                'class' => \bizley\yii2\mercure\Publisher::class,
                // options here
            ]
        ]
    ];
}

Options in both cases are the same:

  • hubUrl The URL of Mercure hub.
  • jwt JSON Web Token or anonymous function returning it. See Authorization section below to learn more.
  • httpClient String with the name of the registered HTTP client component, an array with the HTTP client configuration, or actual HTTP client object. When useYii2Client option is set to true (default) this option is expected to point to Yii 2 HTTP client component. If you want to use it you must install it like described in the link provided and register it in the configuration (so you can set it as 'httpClient' => 'name-of-the-client-component') or provide array configuration for it (like 'httpClient' => ['class' => \yii\httpclient\Client::class]).
  • useYii2Client Boolean flag indicating whether this component should expect Yii 2 HTTP client as HTTP client (true by default) or other custom HTTP client (false).

Usage

The 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 jwt property mentioned earlier.

The JWT must be signed with the same secret key as the one used by the Hub to verify the JWT (default Mercure demo key is !ChangeMe! - not to be used on production). Its payload must contain at least the following structure to be allowed to publish:

{
    "mercure": {
        "publish": []
    }
}

Because the array is empty, the app will only be authorized to publish public updates (see the Authorization section below for further information).

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!

Client subscribing using JavaScript

const eventSource = new EventSource(
    'http://localhost:3000/hub?topic=' + encodeURIComponent('https://api.shoeshop/shoes/1')
);
eventSource.onmessage = event => {
    // Will be called every time an update is published by the server
    console.log(JSON.parse(event.data));
}

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

// URL is a built-in JavaScript class to manipulate URLs
const url = new URL('http://localhost:3000/hub');
url.searchParams.append('topic', 'https://api.shoeshop/shoes/1');
// Subscribe to updates of several shoes resources
url.searchParams.append('topic', 'https://api.shoeshop/shoes/2');
// All shoes resources will match this pattern
url.searchParams.append('topic', 'https://api.shoeshop/shoes/{id}');

const eventSource = new EventSource(url);
eventSource.onmessage = event => {
    console.log(JSON.parse(event.data));
}

Authorization

Mercure also allows to dispatch updates only to authorized clients (described as targets).
Publisher’s JWT must contain all of these targets or * in mercure.publish or you’ll get a 401.
Subscriber’s JWT must contain at least one of these targets or * in mercure.subscribe to receive the update.

To subscribe to private updates, subscribers must provide a JWT containing at least one target marking the update to the Hub.

To provide this JWT, the subscriber can use a cookie, or a Authorization HTTP header. Cookies are automatically sent by the browsers when opening an EventSource connection. They are the most secure and preferred way when the client is a web browser. If the client is not a web browser, then using an authorization header is the way to go.


Some parts of this tutorial are copied from
Symfony’s “Pushing Data to Clients Using the Mercure Protocol” page.

4 Likes

Hi @Bizley,

first of all thanks a lot to bring Mercure to yii2 through your extension.

Well I`m playing following you article but get this error:

HTTP Client Exception – yii\httpclient\Exception
fopen(http://127.0.0.1:3000): failed to open stream: Connection refused

My project run on docker without problems, i have Mercure binary (for Mac OS, darwin) running with this command:

JWT_KEY=’!ChangeMe!’ ADDR=’:3000’ DEMO=1 ALLOW_ANONYMOUS=1 CORS_ALLOWED_ORIGINS=* PUBLISH_ALLOWED_ORIGINS=‘http://127.0.0.1:3000’ ./mercure

My activeRecord model behavior method:

public function behaviors()
    {
        return [   
            [
                'class' => MercureBehavior::class,
                'publisher' => [
                    'class' => Publisher::class,
                    'hubUrl' => 'http://127.0.0.1:3000',
                    'jwt' => '!ChangeMe!',
                    'httpClient' => [
                        'class' => Client::class,
                    ],
                    'useYii2Client' => true,
                ],
                // 'format' => Response::FORMAT_XML
            ]
        ];
    }

As well I can access without problems to http://127.0.0.1:3000 from my browser.

Any ideas what can be happening?¿?

Thanks a lot in advance!!!

Does it work when you set hubUrl to 'http://127.0.0.1:3000/hub'?
Are you setting the proper JWT for your Publisher?

Yes works ok with browser on http://127.0.0.1:3000/hub (or localhost).

Yes, as well jwt is the same in publisher and hub (you can see up in my code).

Now after some test, I’m getting a different error:

HTTP Client Exception – yii\httpclient\Exception
fopen(http://localhost:3000/hub): failed to open stream: Cannot assign requested address

Thanks a lot in advance!!!

I’ve just noticed the jwt parameter you are using is wrong. It must be a proper JWT token (not the JWT_KEY set for Mercure). See Authorization doc about how to make one.

But this probably is not the reason you’ve got this error. This looks like a server problem. Have you got any server logs indicating the problem? Also check for Mercure logs as well.