Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Listening & Publishing

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

The purpose of Mercure is to have a hub where we can subscribe - or listen - to messages and also publish messages.

Here's our high-level goal, it's three steps. First, set up some JavaScript that listens to a "topic" in Mercure - a topic is like a message key or category. Second, in PHP, publish a message to that topic containing Turbo Stream HTML. And finally, when our JavaScript receives a message, make it pass the Turbo Stream HTML to the stream-processing system. The result? The power to update any part of anyone's page whenever we want to right from PHP. If this doesn't make sense yet, don't worry: we're going to put this into action right now.

But before we jump in, open index.php and remove the dump... so that our site is no longer dead. Excellent.

Listening in JavaScript via the Stimulus Controller

Ok, step 1: open templates/product/reviews.html.twig, which is the template that holds the entire reviews turbo frame. At the top, or really anywhere, add a div. Where its attributes live, render a new Twig function from the UX library we installed a few minutes ago - turbo_stream_listen() - and pass this the name of a "topic"... which could be anything. How about product-reviews. Then, close the div.

<div {{ turbo_stream_listen('product-reviews') }}></div>
... lines 2 - 43

I know, that looks kind of weird. To see what it does, go refresh a product page... and inspect the reviews area to find this div. Here it is.

Ok: this div is a dummy element. What I mean is: it won't ever contain content or be visible to the user in any way. Its real job is to activate a Stimulus controller that listens for messages in the product-reviews topic. You can see the data-controller attribute pointing to the controller we installed earlier as well as an attribute for the product-reviews topic and the public URL to our Mercure hub.

Viewing a Mercure Topic in your Browser

Go to your network tools and make sure you're viewing fetch or XHR requests. Scroll up. Woh! There was a request to our Mercure hub with ?topic=product-reviews. The Stimulus controller did this.

But the really interesting thing about this request is the "type": it's not fetch or XHR, it's eventsource. Right Click and open this URL in a new tab. Yup, it just spins forever. But not because it's broken: this is working perfectly. Our browser is waiting for messages to be published to this topic.

Publishing Messages via curl

We are now listening to the product-reviews topic both in this browser tab and, apparently, from some JavaScript on this page thanks to the Stimulus controller we just activated. So... how can we publish messages to that topic?


A cooler way to debug with Mercure is to go to<random_port>/.well-known/mercure/ui/ to see an interactive, debugging Mercure dashboard where you can listen and publish messages.

Basically... by sending a POST request to our Mercure hub. Over in its documentation, go to the "Get Started" page and scroll down a bit down. Here we go: publishing. This shows an example of how you can publish a basic message to Mercure. Copy the curl command version. Then, over my editor, I'll go to File -> "New Scratch File" to create a plaintext scratch file. I'm doing this so we have a convenient spot to play with this long command.

In fact, it's so long that I'll add a few \ so that I can organize it onto multiple lines. This makes it a bit easier to read... but I know, it's still pretty ugly.

Before we try this, change the topic: the example is a URL, but a topic can be any string. Use product-reviews. And at the end, update the URL that we're POSTing to so that it matches our server:

We'll talk about the other parts of this request in minute. For now, copy this, find your terminal, paste and... hit enter! Okay: we got a response... some uuid thing. Did that work?

Spin back over to your browser tab. Holy cats, Batman! It showed up! Our message contained this JSON data... which also appears in our tab.

The Parts of a Publish Request

Even if you're not super comfortable using curl at the command line - honestly, I do this pretty rarely - most of what's happening is pretty simple. First: we're sending a topic POST parameter set to product-reviews and a data POST parameter set to... well, whatever we want! For the moment, we're sending some JSON data, which is passed to anyone listening to this topic.

At the end of the command, we're making this a POST request to our Mercure Hub URL. But what about this Authorization: Bearer part... with this super long key? What's that? It's a JSON web token. Let's learn more about what it is, how it works and where it came from next. It's the key to convincing the Mercure Hub that we're allowed to publish messages to this topic.

Leave a comment!

Login or Register to join the conversation
Fedale Avatar

Hi Ryan, thanks for the tutorial!
I would like also suggest to use<random_port>/.well-known/mercure/ui/ to publish and subscribe Mercure events. It is really cool.

2 Reply

OMG, how did I not know about that! Awesome! I'm going to add a note - that is really cool!!!


1 Reply
wxcvbn612 Avatar
wxcvbn612 Avatar wxcvbn612 | posted 1 year ago

Hi sir, thanks for your tutos but when i use docker for mercure i get this error:
Access to resource at '' from origin '' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.


Hey wxcvbn612!

Hmm. I believe the problem is that Mercure is running in http and your site is running on https. This different "scheme", iirc, makes Mercure and your site work like they're on different domains, which means you have CORS problems. See if you can getting Mercure running in https, or use your site locally with http to fix the issue.


Nick-F Avatar

Yeah i'm not getting a response from curl

curl: (3) [globbing] unmatched close brace/bracket in column 14
curl: (6) Could not resolve host: Bearer
curl: (6) Could not resolve host: eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25v
curl: (35) schannel: next InitializeSecurityContext failed: Unknown error (0x80092012) - The revocation function was unable to check revocation for the certificate.

Nick-F Avatar

For people using windows like me:
The curl statement copied from the mercure website has to be edited to work in the windows command console.
1. replace all single quotes with double quotes
2. escape all double quotes within a string with a backslash "\"
3. add "--ssl-no-revoke" right after "curl"

Also for some reason, I'm unable to split the string on multiple lines in the scratch pad and then paste that into the command console, but just keeping it as a single line works.


Hey Nick F.!

Thanks for posting that - i can convert this into a note to help others :).


Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "", //
        "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.18.5
        "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