Doing Work in the Handler

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.

Inside our controller, after we save the new file to the filesystem, we're creating a new AddPonkaToImage object and dispatching it to the message bus... or technically the "command" bus... because we're currently using it as a command bus. The end result is that the bus calls the __invoke() method on our handler and passes it that object. Messenger understands the connection between the message object and handler thanks to the argument type-hint and this interface.

Command Bus: Beautifully Disappointing

By the way, you might be thinking:

Wait... the whole point of a "command" bus is to... just "call" this __invoke() method for me? Couldn't I just... ya know... call it myself and skip a layer?

And... yes! It's that simple! It should feel completely underwhelming at first!

But having that "layer", the "bus", in the middle gives us two nice things. First, out code is more decoupled: the code that creates the "command" - our controller in this case - doesn't know or care about our handler. It dispatches the message and moves on. And second, this simple change is going to allow us to execute handlers asynchronously. More on that soon.

Moving code into the Handler

Back to work: all the code to add Ponka to the image is still done inside our controller: this gets an updated version of the image with Ponka inside, another service actually saves the new image onto the filesystem, and this last bit - $imagePost->markAsPonkaAdded() - updates a date field on the entity. It's only a few lines of code... but that's a lot of work!

Copy all of this, remove it, and I'll take my comments out too. Paste all of that into the handler. Ok, no surprise, we have some undefined variables. $ponkaficator, $photoManager and $entityManager are all services.

... lines 1 - 7
class AddPonkaToImageHandler implements MessageHandlerInterface
{
public function __invoke(AddPonkaToImage $addPonkaToImage)
{
$updatedContents = $ponkaficator->ponkafy(
$photoManager->read($imagePost->getFilename())
);
$photoManager->update($imagePost->getFilename(), $updatedContents);
$imagePost->markAsPonkaAdded();
$entityManager->flush();
}
}

In the controller... on top, we were autowiring those services into the controller method. We don't need $ponkaficator anymore.

... lines 1 - 20
class ImagePostController extends AbstractController
{
... lines 23 - 37
public function create(Request $request, ValidatorInterface $validator, PhotoFileManager $photoManager, EntityManagerInterface $entityManager, MessageBusInterface $messageBus)
{
... lines 40 - 63
}
... lines 65 - 95
}

Anyways, how can we get those services in our handler? Here's the really cool thing: the "message" class - AddPonkaToImage is a simple, "model" class. It's kind of like an entity: it doesn't live in the container and we don't autowire it into our classes. If we need an AddPonkaToImage object, we say: new AddPonkaToImage(). If we decide to give that class any constructor arguments - more on that soon - we pass them right here.

But the handler classes are services. And that means we can use, good, old-fashioned dependency injection to get any services we need.

Add public function __construct() with, let's see here, PhotoPonkaficator $ponkaficator, PhotoFileManager $photoManager and... we need the entity manager: EntityManagerInterface $entityManager.

... lines 1 - 5
use App\Photo\PhotoFileManager;
use App\Photo\PhotoPonkaficator;
use Doctrine\ORM\EntityManagerInterface;
... lines 9 - 10
class AddPonkaToImageHandler implements MessageHandlerInterface
{
... lines 13 - 16
public function __construct(PhotoPonkaficator $ponkaficator, PhotoFileManager $photoManager, EntityManagerInterface $entityManager)
{
... lines 19 - 21
}
... lines 23 - 32
}

I'll hit Alt + Enter and select Initialize Fields to create those properties and set them.

... lines 1 - 10
class AddPonkaToImageHandler implements MessageHandlerInterface
{
private $ponkaficator;
private $photoManager;
private $entityManager;
public function __construct(PhotoPonkaficator $ponkaficator, PhotoFileManager $photoManager, EntityManagerInterface $entityManager)
{
$this->ponkaficator = $ponkaficator;
$this->photoManager = $photoManager;
$this->entityManager = $entityManager;
}
... lines 23 - 32
}

Now... let's use them: $this->ponkaficator, $this->photoManager, $this->photoManager again... and $this->entityManager.

... lines 1 - 10
class AddPonkaToImageHandler implements MessageHandlerInterface
{
... lines 13 - 23
public function __invoke(AddPonkaToImage $addPonkaToImage)
{
$updatedContents = $this->ponkaficator->ponkafy(
$this->photoManager->read($imagePost->getFilename())
);
$this->photoManager->update($imagePost->getFilename(), $updatedContents);
... line 30
$this->entityManager->flush();
}
}

Message Class Data

Nice! This leaves us with just one undefined variable: the actual $imagePost that we need to add Ponka to. Let's see... in the controller, we create this ImagePost entity object... which is pretty simple: it holds the filename on the filesystem... and a few other minor pieces of info. This is what we store in the database.

Back in AddPonkaToImageHandler, at a high level, this class needs to know which ImagePost it's supposed to be working on. How can we pass that information from the controller to the handler? By putting it on the message class! Remember, this is our class, and it can hold whatever data we want.

So now that we've discovered that our handler needs the ImagePost object, add a public function __construct() with one argument: ImagePost $imagePost. I'll do my usual Alt+Enter and select "Initialize fields" to create and set that property.

... lines 1 - 4
use App\Entity\ImagePost;
class AddPonkaToImage
{
private $imagePost;
public function __construct(ImagePost $imagePost)
{
$this->imagePost = $imagePost;
}
... lines 15 - 19
}

Down below, we'll need a way to read that property. Add a getter: public function getImagePost() with an ImagePost return type. Inside, return $this->imagePost.

... lines 1 - 4
use App\Entity\ImagePost;
class AddPonkaToImage
{
private $imagePost;
public function __construct(ImagePost $imagePost)
{
$this->imagePost = $imagePost;
}
... lines 15 - 19
}

And really... you can make this class look however you want: we could have made this a public property with no need for a constructor or getter. Or you could replace the constructor with a setImagePost(). This is the way I like to do it... but it doesn't matter: as long as it holds the data you want to pass to the handler... you're good!

Anyways, now we're dangerous! Back in ImagePostController, down here, AddPonkaToImage now needs an argument. Pass it $imagePost.

... lines 1 - 20
class ImagePostController extends AbstractController
{
... lines 23 - 37
public function create(Request $request, ValidatorInterface $validator, PhotoFileManager $photoManager, EntityManagerInterface $entityManager, MessageBusInterface $messageBus)
{
... lines 40 - 59
$message = new AddPonkaToImage($imagePost);
... lines 61 - 63
}
... lines 65 - 95
}

Then, over in the handler, finish this with $imagePost = $addPonkaToImage->getImagePost().

... lines 1 - 10
class AddPonkaToImageHandler implements MessageHandlerInterface
{
... lines 13 - 23
public function __invoke(AddPonkaToImage $addPonkaToImage)
{
$imagePost = $addPonkaToImage->getImagePost();
... lines 27 - 32
}
}

I love it! So that's the power of the message class: it really is like you're writing a message to someone that says:

I want you to do a task and here's all the information that you need to know to do that task.

Then, you hand that off to the message bus, it calls the handler, and the handler has all the info it needs to do that work. It's a simple... but really neat idea.

Let's make sure it all works: move over and refresh just to be safe. Upload a new image and... it still works!

Next: there's already one other job we can move to a command-handler system: deleting an image.

Leave a comment!

  • 2020-04-21 Diego Aguiar

    Hey Ahmedbhs

    > So either we exude asynchonsly or synchronously we consume the same memory, but arsenic is a better use for a server to handle a lot of demands.

    If your worker lives in the same server as your application, then yes. When the message gets consume your server may suffer an overload but at least the initial request was already processed, in other words, your user got a faster UX

    > if we send a notification to 1000 subscribers what's the best way is it to or outside. Do we send one message contain all user to the handler and then make a loop Inside the handler:

    Massive email campaigns are hard to manage, usually is recommended to use a third party service for doing that but if you still want to do it. I would recommend to send like 5 or 10 emails per message

    Cheers!

  • 2020-04-19 Ahmedbhs

    If we have a NotificationSendHandler a handler that gonna send a mail to the correct user. The PHP code for sending a mail that is covered by the handler gonna allocated memory In the PHP server not Inside the broker rather rabbitmq. So either we exude asynchonsly or synchronously we consume the same memory, but arsenic is a better use for a server to handle a lot of demands.

    Just a question : if we send a notification to 1000 subscribers what's the best way is it to or outside. Do we send one message contain all user to the handler and then make a loop Inside the handler:

    public function __invoke(SendNotification $notification)
    {
    foreach ($message->getUsers() as $user) {
    echo 'The mail has been sent with success !\r\n';
    $message = (new \Swift_Message('Mail object'))
    ->setFrom('send@gmail.com')
    ->setTo($user->getEmail())
    ->setBody(
    $notification->getMessage()
    );
    $this->mailer->send($message);

    or we should have a message per notification and per user !! ?

  • 2020-02-07 Diego Aguiar

    Excellent!
    You can report issues related to Symfony CLI here https://github.com/symfony/cli

    Cheers!

  • 2020-02-06 Skylar Scotlynn Gutman

    Thank you Diego.

    I solved the problem. First, I updated and upgraded my ubuntu install and upgraded to php 7.4. Then I updated my Symfony CLI file to the latest version. voila. It now works and is serving the correct content for the url requested.

    Of course, there is always consequences. My Windows 10 /WSL/Ubuntu install does not support TCP_INFO and now the Symfony CLI app is logging "failed to retrieve TCP_INFO for socket: Protocol not available (92)" I don't even know where to look for Symfony CLI help. But I can just let it run as it is working. yeay!

  • 2020-02-05 Diego Aguiar

    Hey Skylar Scotlynn Gutman

    Try running the web server without SSL. I would also check the Content-Type header of the response because if you are returning JSON, then it should be set to application/json. Let me know if it worked :)

  • 2020-02-05 Skylar Scotlynn Gutman

    Hi, Thank you for the reply. I traced the error to my system. When I use the Symfony Serve, the XHR response to the /image request is the symfony profiler information. Some how on my system it is not working correctly. When I turn the symfony profiler off, then the ponka app works fine. I have no idea how to fix it. I am using a windows 10 with WSL ubuntu linux. Some where that is causing the issue.

    Thanks for your help.

  • 2020-02-05 Victor Bocharsky

    Hey Skylar,

    Hm, let's try to debug it together... Do you have any spots where you call that "find" property? Basically, you can search for "find" text in your JS files. Most probably you just use incorrect object, etc.

    Also, please, make sure you followed all the steps in README file of downloaded course code. In particular, make sure you ran "composer install", "yarn install" and "yarn encore dev" before starting working with project. Btw, did you download the course code and started from start/ directory?

    Cheers!

  • 2020-02-04 Skylar Scotlynn Gutman

    Hello,

    After completing the code in this video, I get this JS error:

    vue.esm.js:628 [Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'find' of undefined" found in
    ---> <imageuploader> at assets/js/components/ImageUploader.vue
    <imageapp> at assets/js/components/ImageApp.vue
    <root>

    I am not sure how to trouble shoot this. Can you help?

    this is the JSON that is returned from /api/images:
    {"id":7,"originalFilename":"818-banner.jpg","ponkaAddedAt":"2020-02-04T12:16:39-05:00","createdAt":"2020-02-04T12:16:37-05:00","@id":"\/api\/images\/7","url":"\/uploads\/images\/818-banner-5e39a6f5b44a0.jpeg"}

  • 2020-02-03 weaverryan

    Hey Gustavo!

    Hmm. Typically, if you need a service, you should add a constructor and use dependency injection for each service you need. Service subscribers are a special feature that should generally only be used in some edge-cases when there are performance concerns... and these are rare :). Here's some info on this (in case you haven't seen it): https://symfonycasts.com/sc...

    So generally... I would kinda say "no" you shouldn't do this. But if you think you have a special case, let me know.

    Cheers!

  • 2020-02-03 Gustavo

    Hello,
    Is it okay if I have my Handler implement the ServiceSubscriberInterface to get the services (traits, specifically) from my Service folder?