Message, Handler & debug:messenger

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Our app has one other small superpower. If for some reason you're not happy with your Ponka image... I'm not even sure how that would be possible... you can delete it. When you click that button, it sends an AJAX request that hits this delete() action.

And... that really does two things. First, $photoManager->deleteImage() takes care of physically deleting the image from the filesystem. I added a sleep() for dramatic effect, but deleting something from the filesystem could be a bit heavy if the files were stored in the cloud, like on S3.

And second, the controller deletes the ImagePost from the database. But... thinking about these two steps... the only thing we need to do immediately is delete the image from the database. If we only did that and the user refreshed the page, it would be gone. And then... if we deleted the actual file a few seconds... or minutes or even days later... that would be totally fine! But... more on doing fancy asynchronous stuff in a few minutes.

Creating DeleteImagePost

Right now, let's refactor all this deleting logic into the command bus pattern we just learned. First, we need the message, or "command" class. Let's copy AddPonkaToImage, paste and call it DeleteImagePost.php. Update the class name and then... um... do nothing! Coincidentally, this message class will look exactly the same: the handler will need to know which ImagePost to delete.

... lines 1 - 2
namespace App\Message;
use App\Entity\ImagePost;
class DeleteImagePost
{
private $imagePost;
public function __construct(ImagePost $imagePost)
{
$this->imagePost = $imagePost;
}
public function getImagePost(): ImagePost
{
return $this->imagePost;
}
}

Creating DeleteImagePostHandler

Time for step 2 - the handler! Create a new PHP class and call it DeleteImagePostHandler. Like before, give this a public function __invoke() with a DeleteImagePost type-hint as the only argument.

... lines 1 - 2
namespace App\MessageHandler;
use App\Message\DeleteImagePost;
class DeleteImagePostHandler
{
public function __invoke(DeleteImagePost $deleteImagePost)
{
}
}

Now, it's the same process as before: copy the first three lines of the controller, delete them, and paste them into the handler. This time, we need two services.

... lines 1 - 6
class DeleteImagePostHandler
{
public function __invoke(DeleteImagePost $deleteImagePost)
{
$photoManager->deleteImage($imagePost->getFilename());
$entityManager->remove($imagePost);
$entityManager->flush();
}
}

Add public function __construct() with PhotoFileManager $photoManager and EntityManagerInterface $entityManager. I'll hit Alt + Enter and click initialize fields to create both of those properties and set them.

... lines 1 - 5
use App\Photo\PhotoFileManager;
use Doctrine\ORM\EntityManagerInterface;
class DeleteImagePostHandler
{
private $photoManager;
private $entityManager;
public function __construct(PhotoFileManager $photoManager, EntityManagerInterface $entityManager)
{
$this->photoManager = $photoManager;
$this->entityManager = $entityManager;
}
... lines 19 - 27
}

Down here, use $this->photoManager, $this->entityManager and one more $this->entityManager. And, like before, we need to know which ImagePost we're deleting. Prep that with $imagePost = $deleteImagePost->getImagePost().

... lines 1 - 8
class DeleteImagePostHandler
{
... lines 11 - 19
public function __invoke(DeleteImagePost $deleteImagePost)
{
$imagePost = $deleteImagePost->getImagePost();
$this->photoManager->deleteImage($imagePost->getFilename());
$this->entityManager->remove($imagePost);
$this->entityManager->flush();
}
}

Dispatching the Message

Ding! That's my... it's done sound! Because, we have a message, a handler and Symfony should know that they're linked together. The last step is to send the message. In the controller... we don't need these last two arguments anymore... we only need MessageBusInterface $messageBus. And then, this is wonderful, our entire controller is: $messageBus->dispatch(new DeleteImagePost($imagePost)).

... lines 1 - 6
use App\Message\DeleteImagePost;
... lines 8 - 21
class ImagePostController extends AbstractController
{
... lines 24 - 69
public function delete(ImagePost $imagePost, MessageBusInterface $messageBus)
{
$messageBus->dispatch(new DeleteImagePost($imagePost));
return new Response(null, 204);
}
... lines 76 - 93
}

Pretty cool, right? Let's see if it all works. Move over, click the "x" and... hmm... it didn't disappear. And... it looks like it was a 500 error! Through the power of the profiler, we can click the little link to jump straight to a big, beautiful, HTML version of that exception. Interesting:

Command Bus: Each Message should have One Handler

No handler for message App\Message\DeleteImagePost

That's interesting. Before we figure out what went wrong, I want to mention one thing: in a command bus, each message normally has exactly one handler: not two and not zero. And that's why Messenger gives us a helpful error if it can't find that handler. We'll talk more about this later and bend these rules when we talk about event buses.

Debugging the Missing Handler

Anyways... why does Messenger think that DeleteImagePost doesn't have a handler? Can't it see the DeleteImagePostHandler class? Find your terminal and run:

php bin/console debug:messenger

Woh! It only sees our one handler class! What this command really does is this: it finds all the "handler" classes in the system, then prints the "message" that it handles next to it. So... this confirms that, for some reason, Messenger doesn't see our handler!

And... you may have spotted my mistake! To find all the handlers, Symfony looks in the src/ directory for classes that implement MessageHandlerInterface. And... I forgot that part! Add implements MessageHandlerInterface.

... lines 1 - 7
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class DeleteImagePostHandler implements MessageHandlerInterface
{
... lines 12 - 28
}

Run debug:messenger again:

php bin/console debug:messenger

Now it sees it! Let's try it again: close up the profiler, try hitting "x" and... this time it works!

Status report: we have two messages and each has a handler that's potentially doing some pretty heavy work, like image manipulation or talking across a network if files are stored in the cloud. It's time to talk about transports: the key concept behind taking this work and doing it asynchronously so that our users don't have to wait for all that heavy work to finish before getting a response.

Leave a comment!

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
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "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.9.10
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/serializer-pack": "^1.0", // v1.0.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": {
        "symfony/debug-pack": "^1.0", // v1.0.7
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/test-pack": "^1.0" // v1.0.6
    }
}