Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

JSON, Message Headers & Serializer Options

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

In addition to the payload, a message in RabbitMQ can also have "headers". Check that key out on our message. Woh! This contains a big JSON structure of the original class name and the data and class names of the stamps attached to the message!

Why did Messenger do this? Well, find your terminal and consume the async transport:

php bin/console messenger:consume -vv async

This still works. Internally, the Symfony serializer uses the info on the headers to figure out how to take this simple JSON string and turn it into the correct object. It used the type header to know that the JSON should become an ImagePostDeletedEvent object and then looped over the stamps and turned each of those back into a stamp object for the envelope.

The really nice thing about using the Symfony serializer in Messenger is that the payload is this simple, pure JSON structure that can be consumed by any application in any language. It does contain some PHP class info on the headers, but another app can just ignore that. But thanks to those headers, if the same app does both send and consume a message, the Symfony serializer can still be used.

Shouldn't we Always use the Symfony Serializer?

But wait... if that's true - if the Symfony serializer creates messages that can be consumed by external systems or by our same app - then why isn't it the default serializer in Messenger? An excellent question! The reason is that the Symfony serializer requires your classes to follow a few rules in order to be serialized and unserialized correctly - like each property needs a setter method or a constructor argument where the name matches the property name. If your class doesn't follow those rules, you can end up with a property that is set on the original object, but suddenly becomes null when it's read from the transport. No fun.

In other words, the PHP serializer is easier and more dependable when everything is done by the same app.

Configuring the Symfony Serializer

Anyways, if you are using the Symfony serializer, there are also a few things that can be configured. Find your terminal and run:

php bin/console config:dump framework messenger

Check out that symfony_serializer key. This is where you configure the behavior of the serializer: the format - json, xml or something else, and the context, which is an array of options for the serializer.

Of course, you can also create a totally custom serializer service. And if you have the opposite workflow to what we just described - one where your app consumes messages that were sent to Rabbit from some other system - a custom serializer is exactly what you need. Let's talk about that next.

Leave a comment!

2
Login or Register to join the conversation
Mokhtar T. Avatar
Mokhtar T. Avatar Mokhtar T. | posted 1 year ago

Hello Ryan

sorry to put this question here but as part of serializing message from and external service you mentioned that in case we have an invalid message we should throw MessageEncodingException and this one will kill the process of our consumer and supervisor will restart it but by default supervisor has 3 retires so if we have more than 3 invalid message the queue will switch to idle status and our consumer won't working how to prevent that ? I'm thinking to increase supervisor retries but it looks anugly solution do you have any othe idea ? thanks in advance

Reply

Hey Mokhtar T.!

Hmm, that's interesting. So it sounds like you have a situation where it might be normal to receive invalid messages from external services? This wasn't something I've really thought about before. But, if you do have this, I see your point: 3 invalid messages in a row will cause your worker process to fail so rapidly, that you would probably hit the startretries limit. Here's what I'd recommend:

A) If you can, try avoiding this. I know, this seems obvious - and there is probably some reason why you have this situation. But in general, knowing that you may routinely receive invalid messages to your queue seems odd to me.

B) If you can't avoid it, you could set startsecs to 0 to avoid it thinking that the restart was unsuccessful.

C) When you throw an exception during the decoding process, the normal Symfony console error system is invoked. This means that you should be able to listen to this event - https://symfony.com/doc/current/components/console/events.html#the-consoleevents-error-event or https://symfony.com/doc/current/components/console/events.html#the-consoleevents-terminate-event - and... do something... like add a pause (a literal sleep(1) ) so that the process waits long enough before exiting that supervisor knows that it DID start up successfully. A little odd... but I'm not sure if there is anything else you can do from these listeners.

D) If you're writing a custom deserializer, you could, I think, return a dummy Message object that has a handle that... does nothing. In other words, you WOULD always return a valid message object... but if there was a deserialization problem, then it's a dummy message whose handle does nothing.

So... a few "decent" options - I hope this will help.

Cheers!

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