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 behttps://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 be1
. -
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. WhenuseYii2Client
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.