Running the Mercure Service in the symfony Binary

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Mercure itself is a "service" or "server" - kind of like MySQL or Elasticsearch. The Mercure server is called the "hub"... and there are several good ways to get it running. First, they have a managed version where they handle it all for you. This is great for production: it keeps things simple and you can help support the project.

Or, you can download Mercure and set it up locally. Or you can set up Mercure with Docker - that's totally supported. Or the final or is... if you're using the Symfony binary as your local web server then... well... it's already running!

The Embedded Mercure Hub

Head to your open terminal tab, clear the screen and run:

symfony server:status

As a reminder, way back at the start of this tutorial, we used the Symfony binary to run a local web server for us. Back at the browser, open a new tab and go to https://127.0.0.1:8000 - the URL to our site - then /.well-known/mercure.

Tip

The latest symfony binary no longer embeds Mercure. But it's still easy to set up. First, add a mercure service to your docker-compose.yaml file:

... line 1
services:
... lines 3 - 13
mercure:
image: dunglas/mercure
command: caddy run -config /etc/caddy/Caddyfile.dev
ports: ['80']
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeMe!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeMe!'

You can also copy the code block from the script below the video. Start the container by running:

docker-compose up -d

That's it! But instead of being accessible at the URL you see in the tutorial, the Mercure hub will be exposed on a random port. To find it, run:

symfony var:export --multiline

And look for the MERCURE_URL value - it should equal something similar to http://127.0.0.1:64150/.well-known/mercure. Put this into your address bar to see your Mercure Hub (you'll see the same error as in the video).

If everything is working... yes! You should see this error:

Missing "topic" parameter.

This is a Mercure hub. Yup, the Symfony binary comes with Mercure already running at this URL. We get that for free.

The Environment Variables

To communicate with this, head back over to your editor and open the .env file.

39 lines .env
... lines 1 - 29
###> symfony/mercure-bundle ###
# See https://symfony.com/doc/current/mercure.html#configuration
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
MERCURE_URL=https://127.0.0.1:8000/.well-known/mercure
# The public URL of the Mercure hub, used by the browser to connect
MERCURE_PUBLIC_URL=https://127.0.0.1:8000/.well-known/mercure
# The secret used to sign the JWTs
MERCURE_JWT_SECRET="!ChangeMe!"
###

These three environment variables define values that are used in a new config file: config/packages/mercure.yaml. MERCURE_PUBLIC_URL is the public URL to the Mercure hub that our JavaScript will use to subscribe to messages and MERCURE_URL is the URL that our PHP code will use to publish messages. These are usually the same. MERCURE_SECRET is basically a password that will allow us to publish: more on that later.

mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'

In our case, both URL variables already, by chance, point to the correct URL! Yay! But actually, if you're using the latest version of the Symfony binary... we don't even need these variables in this file! Why? Well, in addition to setting up Mercure for us, the Symfony binary also sets these environment variables automatically to their correct values.

Check it out. Back over in our editor, open public/index.php. Let me close a few things... then open it. Cool. Right after the runtime load, I'll paste in some code.

14 lines public/index.php
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
dd(array_filter($_SERVER, function($item) {
return str_contains($item, 'MERCURE');
}, ARRAY_FILTER_USE_KEY));
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

This looks fancy, but I'm basically dumping the $_SERVER variable... except only the keys that contain MERCURE. The $_SERVER variable - among other things - will contain all environment variables. I'm filtering for MERCURE basically... because I don't want to accidentally publish any secret keys from my computer to the internet... as much fun as that would be.

Anyways, this will run before the .env file is loaded, so it will only print real environment variables. Back over on our site, refresh!

Tip

If you're using the docker-compose.yaml setup described earlier, you will only see 2 environment variables here... which are the only 2 you need anyways.

Yay! We see 4 environment variables including 2 we need! The first one is just a flag that tells us that the Symfony binary is running Mercure... and that last one is there for legacy reasons: we don't need it.

This means that our app is already configured and ready to talk to our Mercure Hub! In production, you'll need to run a real Mercure Hub and set these environment variables manually, however you do that in your hosting environment.

So... we have a Mercure hub running! What does that... mean? Well, it's a central place where some things can listen for messages and other things can publish messages. Next, let's do both of these things: listen to a Mercure "topic" in JavaScript and publish messages to it, both from the command line - just to see how it works - and from PHP, which is our real goal.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.13.1
        "doctrine/doctrine-bundle": "^2.2", // 2.3.2
        "doctrine/orm": "^2.8", // 2.9.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^6.1", // v6.1.4
        "symfony/asset": "5.3.*", // v5.3.0-RC1
        "symfony/console": "5.3.*", // v5.3.0-RC1
        "symfony/dotenv": "5.3.*", // v5.3.0-RC1
        "symfony/flex": "^1.3.1", // v1.13.3
        "symfony/form": "5.3.*", // v5.3.0-RC1
        "symfony/framework-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/property-access": "5.3.*", // v5.3.0-RC1
        "symfony/property-info": "5.3.*", // v5.3.0-RC1
        "symfony/proxy-manager-bridge": "5.3.*", // v5.3.0-RC1
        "symfony/runtime": "5.3.*", // v5.3.0-RC1
        "symfony/security-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/serializer": "5.3.*", // v5.3.0-RC1
        "symfony/twig-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/ux-chartjs": "^1.1", // v1.3.0
        "symfony/ux-turbo": "^1.3", // v1.3.0
        "symfony/ux-turbo-mercure": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.0-RC1
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.2
        "symfony/yaml": "5.3.*", // v5.3.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/intl-extra": "^3.2", // v3.3.0
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.3.0-RC1
        "symfony/maker-bundle": "^1.27", // v1.31.1
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/stopwatch": "^5.2", // v5.3.0-RC1
        "symfony/var-dumper": "^5.2", // v5.3.0-RC1
        "symfony/web-profiler-bundle": "^5.2", // v5.3.0-RC1
        "zenstruck/foundry": "^1.10" // v1.10.0
    }
}