Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Partial Handler Failures & Advanced Routing

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 just broke our image deleting process into smaller pieces by creating a new command class, a new handler and dispatching that new command from within the handler! This... technically isn't anything special, but it is cool to see how you can break each task down into as small pieces as you need.

But let's... make sure this actually works. Everything should still process synchronously. Delete the first image and... refresh to be sure. It's gone!

Thinking about Failures and if Messages are Dispatched

Before we handle the new command class asynchronously, we need to think about something. If, for some reason, there's a problem removing this ImagePost from the database, Doctrine will throw an exception right here and the file will never be deleted. That's perfect: the row in the database and file on the filesystem will both remain.

But if deleting the row from the database is successful... but there's a problem deleting the file from the filesystem - like a temporary connection problem talking to S3 if our file were stored there... that file would... actually.. never be deleted! And... maybe you don't care. But if you do, you could wrap this entire block in a Doctrine transaction to make sure it's all successful before finally removing the row. Of course... once we change this message to be handled asynchronously, deleting the actual file will be done later... and we will be, kinda "trusting" that it will be handled successfully. We're going to talk about failures and retries really soon.

Routing the Message Async

Anyways, now that we've broken this into two pieces, head over to config/packages/messenger.yaml. Copy the existing line, paste and route the new DeletePhotoFile to async.

framework:
messenger:
... lines 3 - 12
routing:
... lines 14 - 15
'App\Message\DeletePhotoFile': async

Cool! With any luck, the row in the database will be deleted immediately... then the file a few seconds later.

And because we just made a change to some handler code, go over, stop our worker and restart it:

php bin/console messenger:consume -vv

Testing time! Refresh to be safe... and let's try deleting. Check out how much faster that is! If you scoot over to the worker terminal... yea, it's doing all kinds of good stuff here. Oh, and fun! An exception occurred while handling one of the messages - a file wasn't found. I think that's from the duplicate row caused by the Doctrine bug a few minutes ago: the file was already gone when the second image was deleted. The cool thing is that it's already retrying that message in case it was a temporary failure. Eventually, it gives up and "rejects" the message.

Let's try this whole crazy system together! Upload a bunch of photos... then... quick! Delete a couple! If you look at the worker... it's all beautifully mixed up: a few AddPonkaToImage objects are handled here... then DeletePhotoFile.

Routing with Interfaces & Base Classes

Oh, and by the way: if you look at the routing section in messenger.yaml, you'll usually route thing by their exact class name: App\Message\AddPonkaToImage goes to async. But you can also route via interfaces or base classes. For example, if you have a bunch of classes that should go to the async transport, you could create your very own interface - maybe AsyncMessageInterface - make your messages implement that, then only need to route that interface to async here. But be careful because, if a class matches multiple routing lines, it will be sent to all those transports. Oh, and last thing - in case you have a use-case, each routing entry can send to multiple transports.

Next: remember how the serialized message in the database was wrapped in something called an Envelope? Let's learn what that is and how its stamp system gives us some cool superpowers.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

This tutorial is built with Symfony 4.3, but will work well on Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // v1.8.0
        "doctrine/doctrine-bundle": "^1.6.10", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
        "doctrine/orm": "^2.5.11", // v2.6.3
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.1
        "sensio/framework-extra-bundle": "^5.3", // v5.3.1
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/flex": "^1.9", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/property-access": "4.3.*", // v4.3.2
        "symfony/property-info": "4.3.*", // v4.3.2
        "symfony/serializer": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.5", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.7
        "symfony/debug-bundle": "4.3.*", // v4.3.2
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/var-dumper": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}
userVoice