Publishing Mercure Updates in PHP

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

We now know that we can easily subscribe to a Mercure topic in JavaScript. And, if we publish a message to that topic with <turbo-stream> HTML in it, our JavaScript will instantly notice & process it. Sweet!

So far, we've published messages to Mercure via curl at the command line... but that was just to get a feel for how it works. In reality, we're going to publish message from PHP... which is a heck of a lot simpler anyways.

Copy the <turbo-stream>... then go find ProductController... and the reviews action.

Publishing a Message from PHP

To publish updates to a Mercure Hub, we need to autowire a new service. Use HubInterface and I'll call it $mercureHub.

Down below, to start, let's publish an update when we submit the form... but not necessarily when it's successful. I'm lazy: this will let us test without filling out the form successfully. Add a variable - $update - set to new Update() - a handy class for creating messages. We need to pass this two arguments. The first is the topic or topics that we want to publish to. Use product-reviews. The second is the data that we want to send. Paste in the <turbo-stream> string.

Below, to publish this, all we need is $mercureHub->publish($update).

... lines 1 - 20
class ProductController extends AbstractController
{
... lines 23 - 71
/**
* @Route("/product/{id}/reviews", name="app_product_reviews")
*/
public function productReviews(Product $product, CategoryRepository $categoryRepository, Request $request, EntityManagerInterface $entityManager, HubInterface $mercureHub)
{
... lines 77 - 82
if ($request->isMethod('POST')) {
... lines 84 - 85
$update = new Update(
'product-reviews',
'<turbo-stream action="update" target="product-quick-stats"><template>QUICK STATS CHANGED!</template></turbo-stream>'
);
$mercureHub->publish($update);
... lines 91 - 109
}
... lines 111 - 117
}
... lines 119 - 126
}

Kind of... beautiful, isn't it?

Let's try this! Find your browser and refresh so the quick stats area is restored. Scroll down and submit the form empty. Uh... 500 error? Open the profiler for that request. Hmm:

Failed to send an update

Setting verify_peer to False in dev for Macs

Not... very explanatory. But notice that there were four exceptions. When this happens, it's often one of the other exceptions that has more details. Ah:

SSL peer certificate or SSH remote key was not okay

This... is a problem specific to the Symfony binary web server, https and... Macs. You can learn more about it on this issue for the Symfony CLI. If you're not using a Mac, good for you! That hopefully just worked.

If you are, the easiest way to fix this is to disable "peer verification" in the dev environment.

To do this, open config/packages/framework.yaml. At the bottom, use when@dev to set config specific to the dev environment - that's a feature that's new to Symfony 5.3. Under this, set framework, http_client, default_options then verify_peer: false.

... lines 1 - 25
when@dev:
framework:
http_client:
default_options:
verify_peer: false

That's not something you want to set in production... and it's a bummer we need to set it in the dev environment. But it should fix our issue.

Close this... then refresh the page again. Scroll down... and submit the review form. Ok! We get the normal validation error - that's expected. But scroll up. Yes! We just updated the page with our stream through Mercure! That's awesome!

So next: let's use this new superpower to simplify our reviews action. We can now redirect on success like we originally were... and publish a stream to update the quick stats area.

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
    }
}