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?

Tip

A cooler way to debug with Mercure is to go to http://127.0.0.1:<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: 127.0.0.1:8000.

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!

12
Login or Register to join the conversation
Fedale Avatar

Hi Ryan, thanks for the tutorial!
I would like also suggest to use http://127.0.0.1:<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!!!

Cheers!

1 Reply
CDesign Avatar
CDesign Avatar CDesign | posted 2 months ago | edited

I am getting the following error after adding the turbo_stream_listen twig function:

An exception has been thrown during the rendering of a template ("The Turbo stream transport "default" doesn't exist.").

which is coming from this line.

Do I have something configured incorrectly? I also noticed that my /assets/controllers.json file was never changed after installing Mercure like Ryan's was. Does symfony/mercure-bundle need to register a controller there?

Thx!

Reply

Hey @CDesign!

Sorry for the slow reply! Hmm, let's dig into this. Let me answer things in reverse :).

I also noticed that my /assets/controllers.json file was never changed after installing Mercure like Ryan's was

This is probably ok - there IS A slightly different behavior now (actually, introduced by me darn it!) from when I made the video. We talk briefly about it here - https://symfonycasts.com/screencast/turbo/mercure#installing-the-mercure-libraries - but basically, you SHOULD have something that looks like this in your controllers.json file:

            "mercure-turbo-stream": {
                "enabled": true,
                "fetch": "eager"
            }

But it now comes from (and is under) the @symfony/ux-turbo key, whereas previously it was under a different key and didn't come until you installed the now-not-needed symfony/ux-turbo-mercure. Be sure you have "enabled": true for the above controller.

Now, to your first question! And I don't think the controllers.json item I just talked about is related:

An exception has been thrown during the rendering of a template ("The Turbo stream transport "default" doesn't exist.").

What version of symfony/mecure-bundle do you have (try composer show symfony/mercure-bundle)? The missing "default" transport thingy should come from THAT bundle. Also, after installing that bundle, you should have a config/packages/mercure.yaml file that looks like this:

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

See the default on there? If everything is hooked up properly, this config file is ready, which results in a default "Turbo stream transport" being made available. Something is short-circuiting before that happens...

Cheers!

Reply
CDesign Avatar
CDesign Avatar CDesign | weaverryan | posted 2 months ago | edited

First of all, thank you very much for the detailed response! Much appreciated!

1) On the controllers.json issue, I added the mercure-turbo-stream key (which was not there even though I ran recipes). So my file now looks like this:

{
    "controllers": {
        "@symfony/ux-chartjs": {
            "chart": {
                "enabled": true,
                "fetch": "lazy"
            }
        },
        "@symfony/ux-turbo": {
            "turbo-core": {
                "enabled": true,
                "fetch": "eager"
            },
            "mercure-turbo-stream": {
                "enabled": true,
                "fetch": "eager"
            }
        }
    },
    "entrypoints": []
}

But now yarn watch complains:

Error: Controller "@symfony/ux-turbo/mercure-turbo-stream" does not exist in the package and cannot be compiled.

2) On the twig render error, mercure-bundle 0.3.4 is installed, and the config in config/packages/mercure.yaml looks exactly like yours above and the three mercure environment vars are declared in .env as follows:

MERCURE_URL=https://example.com/.well-known/mercure
MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"

And php bin/console debug:config MercureBundle gives me:

mercure:
    hubs:
        default:
            url: '%env(MERCURE_URL)%'
            public_url: '%env(MERCURE_PUBLIC_URL)%'
            jwt:
                secret: '%env(MERCURE_JWT_SECRET)%'
                publish:
                    - '*'
                subscribe: {  }
                passphrase: ''
                algorithm: hmac.sha256
    default_cookie_lifetime: null

I don't know if it helps, but I see that same template render error in the console as soon as I start the server with symfony serve. Why would I get a render error simply by starting the server? I haven't sent a request yet. Also, in mercure.yaml, I tried changing 'default' to 'test' but the error reported still says "The Turbo stream transport 'default' doesn't exist." And that's even after dumping cache.

And not sure if it's relevant, but I am running PHP 7.4 (as that is what is in the 'require' section of composer.json) and Symfony 5.3.0-RC1 and symfony/ux-trubo 1.3.0. Thx!

Reply

Hey @CDesign!

Sorry for my slow reply!

And not sure if it's relevant, but I am running PHP 7.4 (as that is what is in the 'require' section of composer.json) and Symfony 5.3.0-RC1 and symfony/ux-trubo 1.3.0. Thx!

Ah... this helps a lot! The latest version of symfony/ux-turbo is 2.9! And the mercure stuff was moved into ux-turbo in 2.6.1. It sounds like you're coding along with our tutorial code. In that case, you should follow the directions in the original video: you WILL need to install the separate symfony/ux-turbo-mercure (which is deprecated in newer versions) and have a slightly different controllers.json. If you're using this on a real project, I'd back up and make sure you've got the latest versions of everything. Otherwise, if you're just playing around with stuff, follow the video exactly (and ignore the note). I'm happy to help with any snags if you hit them :).

Cheers!

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 'http://127.0.0.1:61364/.well-known/mercure?topic=product-reviews' from origin 'https://127.0.0.1:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Reply

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.

Cheers!

Reply
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
d24vbWVyY3VyZS9zdWJzY3JpcHRpb2
curl: (35) schannel: next InitializeSecurityContext failed: Unknown error (0x80092012) - The revocation function was unable to check revocation for the certificate.

Reply
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.

Reply

Hey Nick F.!

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

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "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.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
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.13.13
        "@fortawesome/fontawesome-free": "^5.15.3", // 5.15.3
        "@hotwired/turbo": "^7.0.0-beta.5", // 1.2.6
        "@popperjs/core": "^2.9.1", // 2.9.2
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets", // 0.1.0
        "@symfony/ux-turbo-mercure": "file:vendor/symfony/ux-turbo-mercure/Resources/assets", // 0.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.3.0
        "bootstrap": "^5.0.0-beta2", // 5.0.1
        "chart.js": "^2.9.4",
        "core-js": "^3.0.0", // 3.13.0
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.2
        "react-dom": "^17.0.1", // 17.0.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "https://github.com/weaverryan/stimulus-autocomplete#toggle-event-always-dist", // 2.0.0
        "stimulus-use": "^0.24.0-1", // 0.24.0-2
        "sweetalert2": "^11.0.8", // 11.0.12
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.2
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}
userVoice