Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Dispatching a Message inside a Handler?

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

Deleting an image is still done synchronously. You can see it: because I made it extra slow for dramatic effect, it takes a couple of seconds to process before it disappears. Of course, we could hack around this by making our JavaScript remove the image visually before the AJAX call finishes. But making heavy stuff async is a good practice and could allow us to put less load on the web server.

Let's look at the current state of things: we did update all of this to be handled by our command bus: we have a DeleteImagePost command and DeleteImagePostHandler. But inside config/packages/messenger.yaml, we're not routing this class anywhere, which means it's being handled immediately.

Oh, and notice: we're still passing the entire entity object into the message. In the last two chapters, we talked about avoiding this as a best practice and because it can cause weird things to happen if you handle this async.

But... if you're planning to keep DeleteImagePost synchronous... it's up to you: passing the entire entity object won't hurt anything. And... really... we do need this message to be handled synchronously! We need the ImagePost to be deleted from the database immediately so that, if the user refreshes, the image is gone.

But, look closer: deleting involves two steps: deleting a row in the database and removing the underlying image file. And... only that first step needs to happen right now. If we delete the file on the filesystem later... that's no big deal!

Splitting into a new Command+Handler

To do part of the work sync and the other part async, my preferred approach is to split this into two commands.

Create a new command class called DeletePhotoFile. Inside, add a constructor so we can pass in whatever info we need. This command class will be used to physically remove the file from the filesystem. And if you look in the handler, to do this, we only need the PhotoFileManager service and the string filename.

So this time, the smallest amount of info we can put in the command class is string $filename.

... lines 1 - 2
namespace App\Message;
class DeletePhotoFile
{
... lines 7 - 8
public function __construct(string $filename)
{
... line 11
}
... lines 13 - 17
}

I'll hit Alt + enter and go to "Initialize Fields" to create that property and set it.

... lines 1 - 2
namespace App\Message;
class DeletePhotoFile
{
private $filename;
public function __construct(string $filename)
{
$this->filename = $filename;
}
... lines 13 - 17
}

Now I'll go to Code -> Generate - or Cmd+N on a Mac - to generate the getter.

... lines 1 - 2
namespace App\Message;
class DeletePhotoFile
{
private $filename;
public function __construct(string $filename)
{
$this->filename = $filename;
}
public function getFilename(): string
{
return $this->filename;
}
}

Cool! Step 2: add the handler DeletePhotoFileHandler. Make this follow the two rules for handlers: implement MessageHandlerInterface and create an __invoke() method with one argument that's type-hinted with the message class: DeletePhotoFile $deletePhotoFile.

... lines 1 - 2
namespace App\MessageHandler;
use App\Message\DeletePhotoFile;
... line 6
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class DeletePhotoFileHandler implements MessageHandlerInterface
{
... lines 11 - 17
public function __invoke(DeletePhotoFile $deletePhotoFile)
{
... line 20
}
}

Perfect! The only thing we need to do in here is... this one line: $this->photoManager->deleteImage(). Copy that and paste it into our handler. For the argument, we can use our message class: $deletePhotoFile->getFilename().

... lines 1 - 2
namespace App\MessageHandler;
use App\Message\DeletePhotoFile;
... line 6
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class DeletePhotoFileHandler implements MessageHandlerInterface
{
... lines 11 - 17
public function __invoke(DeletePhotoFile $deletePhotoFile)
{
$this->photoManager->deleteImage($deletePhotoFile->getFilename());
}
}

And finally, we need the PhotoFileManager service: add a constructor with one argument: PhotoFileManager $photoManager. I'll use my Alt+Enter -> Initialize fields trick to create that property as usual.

... lines 1 - 2
namespace App\MessageHandler;
use App\Message\DeletePhotoFile;
use App\Photo\PhotoFileManager;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class DeletePhotoFileHandler implements MessageHandlerInterface
{
private $photoManager;
public function __construct(PhotoFileManager $photoManager)
{
$this->photoManager = $photoManager;
}
public function __invoke(DeletePhotoFile $deletePhotoFile)
{
$this->photoManager->deleteImage($deletePhotoFile->getFilename());
}
}

Done! We now have a functional command class which requires the string filename, and a handler that reads that filename and... does the work!

Dispatching Embedded

All we need to do now is dispatch the new command. And... technically we could do this in two different places. First, you might be thinking that, in ImagePostController, we could dispatch two different commands right here.

But... I don't love that. The controller is already saying DeleteImagePost. It shouldn't need to issue any other commands. If we choose to break that logic down into smaller pieces, that's up to the handler. In other words, we're going to dispatch this new command from within the command handler. Inception!

Instead of calling $this->photoManager->deleteImage() directly, change the type-hint on that argument to autowire MessageBusInterface $messageBus. Update the code in the constructor... and the property name.

... lines 1 - 9
use Symfony\Component\Messenger\MessageBusInterface;
... line 11
class DeleteImagePostHandler implements MessageHandlerInterface
{
private $messageBus;
... lines 15 - 16
public function __construct(MessageBusInterface $messageBus, EntityManagerInterface $entityManager)
{
$this->messageBus = $messageBus;
$this->entityManager = $entityManager;
}
... lines 22 - 32
}

Now, easy: remove the old code and start with: $filename = $imagePost->getFilename(). Then, let's delete it from the database and, at the bottom, $this->messageBus->dispatch(new DeletePhotoFile($filename)).

... lines 1 - 5
use App\Message\DeletePhotoFile;
... lines 7 - 11
class DeleteImagePostHandler implements MessageHandlerInterface
{
... lines 14 - 22
public function __invoke(DeleteImagePost $deleteImagePost)
{
$imagePost = $deleteImagePost->getImagePost();
$filename = $imagePost->getFilename();
$this->entityManager->remove($imagePost);
$this->entityManager->flush();
$this->messageBus->dispatch(new DeletePhotoFile($filename));
}
}

And... this should... just work: everything is still being handled synchronously.

Let's try it next, think a bit about what happens if part of a handler fails, and make half of the delete process async.

Leave a comment!

8
Login or Register to join the conversation
Kris Avatar
Kris Avatar Kris | posted 6 months ago | edited

Is there any reason why DelayStamp for a message dispatched in the handler doesn't work for me.
When I dispatch Message from the controller DelayStamp works.

Reply

Hey Kris,

That's unexpected, the DelayStamp should just work (and actually, it does work for me). How do you start the worker? I'm wondering if you use any special config for it
Also, remember that the delay time it's in milliseconds

Cheers!

Reply

Hi Ryan, I have a question. from what i understand the idea of ​​taking the load off the web server would be to make the service asynchronous so that the response is returned as quickly as possible to the client and finish the request faster, with that the (same server) can process the rest of the work as delete
the photo file without this process being linked to a web request, am I right? I believed that we would have to separate our code on different servers to perform each step as if they were microservices, but it was already clear to me how things should work.

Reply

Hey Renato,

Yes, you're right. We don't need out users to wait for the actual photo deleted, we can say that it's deleted by kind of "scheduled" it to be deleted in the system. Then later, the messenger will handle that scheduled delete request eventually deleting the file, but it will be a background process, and we don't care how long it will last, the user will get the response almost immediately. Moreover, if something when wrong - we could retry the deleting (or any other process we scheduled).

Also, it's important to understand that with this strategy you don't care about how many simultaneous requests you get the same second because all scheduled deletions will be performed one by one by the Messenger's worker, i.e. no any pick highloads on the server. Without the Messenger, if we know that the process (like deleting a photo from the server) is resource consuming - you may down the whole website when you suddently got a high pick of users requesting file deletions.

So, Messenger clearly has some benefits handling time or resource consuming operations.

And yes, you can have a separate server that will handle all those tasks in the queue if you need, but as I said, thanks to the fact that messenger's worker will handle each queue one by one you can safely do this on the same server as well without causing server overloading. But yes, it depends on your specific case, on how time/resource consuming your operations are, and how many queues you have.

I hope it's clearer to you now.

Cheers!

Reply

FANTASTIC!!!!!!!!!!!!!!

YOUR EXPLANATION MADE EVERYTHING CLEAR! THANK YOU SO MUCH

Reply

Hey Renato,

Awesome! I am really happy to hear it :)

Cheers!

Reply
HJT Avatar

Great tutorial and followed it step by step using Symfony 6. The "versions" tab explicitly says it will work upto Symfony 5. I'm facing problems with Symfony 6 when changing it to async. Synchronously it works perfectly (dispatch to 1, then dispatch to 2 and on success dispaches to 3 and 4), but in async only the first message is handled succesfully. The message of the 2nd dispatch does not even endup on the queue (Doctrine). Doesn't this work on Symfony 6 ? Should I change to a workflow setup?

Reply
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