Publish messages to dunglas/mercure from any PHP-Based Application including Laravel/Lumen Broadcasting & Notification

Syed Sirajul Islam Anik
8 min readMar 9, 2021

--

Image from: https://github.com/dunglas/mercure

Recently I have been doing some hands-on with the dunglas/mercure, and thought to build a package that can help any PHP-based application to publish messages to the Mercure Hub. The package also helps to publish messages from the Laravel/Lumen including the Broadcasting & Notification channel out of the box.

Recently I have published an article on Mercure and how to use it with Laravel/Lumen. If you haven’t read it, then you can give it a try. But the article doesn’t include how to use it with Events & Notifications. Rather instructs how to use it in the frontend and to send events through the framework.

Installation

To install the package, you’ll require to have composer on your machine. Next, run the following command.

composer require anik/mercure

The above command will install the package and its dependencies on your project.

For any PHP-based applications (Laravel/Lumen developer can skip)

Next, you’ll need to get the publisher instance. From your codebase, you can do the following.

<?php// From wherever you need to call the publisher.
$config = [
"url" => "https://mercure.host/.well-known/mercure",
"jwt" => "PUBLISHER_JWT",
];
$publisher = \Anik\Mercure\Factory\Publisher::instance($config);

This is the bare minimum requirement for the configuration to get the publisher instance. Here, in the $config array, you MUST have to have the url key with the endpoint explicitly mentioned. Till now, it’s been /.well-known/mercure. If they change it in the future, just change the endpoint then.

Next, in the $config array, you can have any of the following set.

  • jwt — The signed JWT key, which will be used to publish the messages.
  • provider — A callable that must return a valid JWT string.
  • secret and payload — If you want to generate a JWT to be created on the fly before making requests, you must have to provide the secret with as a string and claims in the payload as an array. Make sure you know what to send when generating the JWT from that payload. You can also provide algo as a part of the $config‘s root key. That algorithm will be used to generate the JWT. Also, make sure your server/machine has the required modules installed to use that algo.
  • http_client — The client that is an instance of the Symfony\Contracts\HttpClient\HttpClientInterface which will be used to push the updates to the Mercure hub. If it is not provided or not an instance of the HttpClientInterface, then it’ll use the default client.

After getting the $publisher instance, we can immediately publish the update to the Mercure hub. To publish a message, we just need the following snippet.

<?php
use Symfony\Component\Mercure\Update;
// as we have the $publisher now,
$update = new Update(
$arrayOfTopics,
$myMessageInStringFormat,
$isAPrivateMessage,

null,
$typeOfMessageType,
null
);
// Publish the message to the mercure hub
$publisher($update);
  • $arrayOfTopics can either be an array of topics or a string variable as a single topic name. Any of these will work.
  • $mymessageInStringFormat is the message that will be delivered to the subscriber. It must have to be a string. Even if you know still mentioning here, the json_encode will convert the array into a string.
  • $isAPrivateMessage is a boolean value that denotes if it’s a private type of message or anyone connected to the topic will receive this message (public)?
  • $typeOfMessageType a string value, that means if you used an addEventListener then what was the type in that listener? If passed an empty or null value, the default "message” type will be used and es.onmessage = () => {} listener to be used in the frontend.

The 4th param is for string $id and the 6th param is for int $retry. You’ll find them in the Mercure hub spec. And can also pass those values.

Note:

If the Mercure hub cannot be connected or the JWT is invalid, it’ll raise Exceptions. You’ll need to catch those exceptions.

Using the package with Laravel/Lumen

At Least Laravel/Lumen version the package requires is Laravel/Lumen 6.x. If you’re using any version lower than the requirement, you won’t be able to use the package.

Laravel: If you’re using the supported Laravel version and you’re using Laravel, then due to the package discovery feature, you’re all good to go. You can run php artisan vendor:publish to publish the configuration.

Lumen: If you’re using the supported Lumen version, you’ll need to register the service provider. Register the following provider in your bootstrap/app.php file.

$app->register(
\Anik\Mercure\Provider\MercureServiceProvider::class
);

As Lumen doesn’t support vendor:publish command, so you’ll need to copy the vendor/anik/mercure/src/config/mercure.php config file in your project root’s config directory.

Understanding the configuration file

  • mercure.default — The default connection name to use when resolving the DI instances.
  • mercure.enable_broadcasting — The service provider doesn’t bind the broadcasting driver by default. If you set the variable to true, it then registers the driver.
  • mercure.enable_notification — The service provider doesn’t bind the notification channel by default. If you set the variable to true, it then registers the driver.
  • mercure.connections — A set of connections for the hubs. mercure.default chooses from this set of connections.
  • mercure.connections.*.url — A MUST set string URL pointing to the hub endpoint, to which the messages will be published.
  • mercure.connections.*.jwt — A JWT string that determines the authorization to post the update messages on topics.
  • mercure.connections.*.provider — An alternative to the mercure.connections.*.jwt. A callable that should return a JWT string.
  • mercure.connections.*.secret [string] and mercure.connections.*.payload [array] — An alternative to mercure.connections.*.jwt and mercure.connections.*.provider. But *.secret and *.payload must have to be available together. Can be used to generate the JWT on the fly. Must know what to set on the *.payload array.
  • mercure.connections.*.http_client — An HTTP client. The implementation of Symfony\Contracts\HttpClient\HttpClientInterface. Will use the default client if left empty or not an implementation of HttpClientInterface.

Publish message from Laravel/Lumen

If you’re using Laravel/Lumen you can also publish messages with the above-mentioned pattern for generic usage. But with Laravel/Lumen, you can use the Dependency Injection for the following classes.

<?php
use Symfony\Component\Mercure\Publisher;
use Symfony\Component\Mercure\PublisherInterface;
use Symfony\Component\Mercure\Update;
// in my controller/jobs, wherever DI is supported.
public function controllerMethod (PublisherInterface $publisher)
{
return $publisher(new Update('topic', 'msg', true));
}
public function anotherMethod (Publisher $publisher)
{
return $publisher(new Update('topic', 'msg', true));
}

There is also an arbitrary string binding. To use that, you can do the following.

<?php
// anywhere in your codebase
app('mercure')(new Update('topic', 'msg', true));

In all the above cases, it’ll use the default hub’s connection.

Broadcasting through the Mercure

Laravel ships with broadcasting preinstalled. But if you’re using Lumen, you’ll need to install the illuminate/broadcasting package. Register the service provider. And copy the broadcasting.php configuration.

In both Laravel and Lumen, you’ll need to add the driver in your broadcasting.php config. So, it’ll be like.

<?php
// ... configuration above
'connections' => [
// ...
'mercure' => [
'driver' => 'mercure',
'connection' => 'hub',
],
// ...
],
// ...

Here, broadcasting.connections.mercure is the connection name. You can name it whatever you want. broadcasting.connections.mercure.driver must have to the mercure driver. broadcasting.connections.mercure.connection value is set to hub pointing which mercure.connections.* that will be used when broadcasting the event. If unspecified, it’ll then fall back to the config mercure.default‘s value.

To use the mercure driver, set mercure.enable_broadcasting value to true as explained in the configuration section.

You’ll also need to set the broadcasting.default to mercure (the name you used in the broadcasting.connections).

So, after you broadcast the event through Mercure, in your client you’ll receive the message in {"event": "event_name", "data": "data_you_passed"} format.

Tweaks for sending broadcasts

Like the other broadcasting drivers, you’ll use the same class event class to broadcast the event to Mercure. But as the Mercure allows a set of configurations (private, id, retry, type), you can pass the following values (Event::broadcastWith method or as public property) to work as configuration.

  • __msg_private — All the update messages are default sent as private. If you want to send the messages as public, set this variable to false.
  • __msg_id — If you want to set the message ID. set your desired value.
  • __msg_retry — No of retries.
  • __msg_type — The type of the message. Falls back to the event name.

Notes:

  • It’s your responsibility to choose the channel names. The package doesn’t touch the channel names. If you set them as private or public or presence, they’ll be cast to string and used as the topic names accordingly.
  • The package also sends the message to all the channels. If you want your messages to be treated separately, broadcast them separately.
  • If your channels are empty, the package will not send any message to the hub.
  • If you use the previously mentioned keys __msg_* then these keys will not be forwarded to the client.

Notifications through Mercure

Laravel ships with notification preinstalled. But if you’re using Lumen, you’ll need to install the illuminate/notifications package. Register the service provider.

Next, to use the mercure driver, setmercure.enable_notification value to true as explained in the configuration section.

Then in your Notification instance, from the via method, you can return mercure driver as the notification channel. Even if you return \Anik\Mercure\Channel\MercureChannel::class, it’ll also work. When using the MercureChannel::class, setting mercure.enable_notification value to true is not required.

  • Your notification class should return the topic name or array of topic names from the Notification::broadcastOn() method.
  • If your class contains the toMercure method, whatever is passed will get broadcasted to the Mercure hub.
  • In absence of the toMercure method, it’ll look for the toArray method in your notification class and the data will get passed to the hub.
  • In absence of the both methods, it’ll just pass an empty “string” to the hub.
  • The event name will be parsed in order of the presence of the method. Firstly, it’ll check if broadcastType method. Otherwise, it’ll check for the broadcastAs method. In absence of them, it’ll use the class name as the event name.

Notes:

  • It’s your responsibility to choose the channel names. The package doesn’t touch the channel names. If you set them as private or public or presence, they’ll be cast to string and used as the topic names accordingly.
  • The package also sends the message to all the channels. If you want your messages to be treated separately, broadcast them separately.
  • If your channels are empty, the package will not send any message to the hub.
  • If you use keys with __msg_* for the payload, then these keys will not be forwarded to the client.

Graceful publishing

By default when using instances from app('mercure'), app(Publisher::class) or app(PublisherInterface::class) and calling the publish method to send updates manually, it’ll raise an exception if you’re not allowed to publish the update to the hub due to JWT or the Mercure hub cannot be connected.

If you want to publish updates gracefully, the App\Mercure\Adapter\Mercure instance can be used otherwise. And the publishGracefully method can be used.

use Anik\Mercure\Adapter\Mercure;(new Mercure($publisherInterfaceInstance))->publishGracefully(
['array', 'of', 'topics'],
'event_name',
['array' => 'containing', 'our' => 'message']
);

The same __msg_* rule applies here for the third parameter (payload) to pass the Mercure related configuration.

This method can send null when the channels are empty. Can send bool when there is anything wrong with submitting the update to the hub and string when the request is successful.

Here is the package repository. Don’t forget to appreciate by pressing the star button in the project. Feel free to inform the bugs, PRs if you know where to change.

Happy coding. ❤

--

--

Syed Sirajul Islam Anik

software engineer with "Senior" tag | procrastinator | programmer | !polyglot | What else 🙄 — Open to Remote