If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe 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.
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.
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.
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.
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.
OMG, how did I not know about that! Awesome! I'm going to add a note - that is really cool!!!
Cheers!
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!
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!
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!
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!
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.
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!
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.
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 :).
Cheers!
// 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
}
}
// 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
}
}
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.