Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Serializing Messages as JSON

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

Once you start using RabbitMQ, a totally different workflow becomes possible... a workflow that's especially common with bigger systems. The idea is that the code that sends a message might not be the same code that consumes and handles that message. Our app is responsible for both sending the messages to RabbitMQ and, over here in the terminal, for consuming messages from the queue and handling them.

But what if we wanted to send one or more messages to RabbitMQ with the expectation that some other system - maybe some code written in a different language and deployed to a different server - will consume and handle it? How can we do that?

Well... on a high level... it's easy! If we wanted to send things to this async transport... but didn't plan to consume those messages, we wouldn't need to change anything in our code! Nope, we just... wouldn't consume messages from that transport when using the messenger:consume command. We could still consume messages from other transports - we just wouldn't read these ones... because we know someone else will. Done! Victory! Coffee!

How are our Messages Formatted?

But... if you were going to send data to another system, how would you normally format that data? Well, to use a more familiar example, when you send data to an API endpoint, you typically format that data as JSON... or maybe XML. The same is true in the queueing world. You can send a message to RabbitMQ in any format... as long as whoever is consuming that message understands the format. So... what format are we using now? Let's find out!

I'll go into the messages_normal queue... and just to be safe, let's empty this. Messages sent to the async transport will eventually end up in this queue... and the ImagePostDeleteEvent classes route there. Ok, back on our app, delete a photo then, looking at our queue, in a moment... there it is! Our queue contains the one new message.

Let's see exactly what this message looks like. Down below, there's a spot to fetch a message out. But... for some reason... this hasn't been working for me. To hack around this, I'll bring up my network tools, click "Get Message(s)" again... and look at the AJAX request this just made. Open up the returned data and hover over that payload property.

Yep, this is what our message looks like in the queue - this is the body of the message. What is that ugly format? It's a serialized PHP object! When Messenger consume this, it knows to use the unserialize function to get it back into an object... and so, this format works awesome!

But if we expect a different PHP application to consume this... unserializing it won't work because these classes probably won't exist. And if the code that will handle this is written in a different language, pfff, they won't even have a chance at reading and understanding this PHP-specific format.

The point is: using PHP serialization works great when the app that sends the message also handles it. But it works horribly when that's not the case. Instead, you'll probably want to use JSON or XML.

Using the Symfony Serializer

Fortunately, using a different format is easy. I'll purge that message out of the queue one more time. Move over and open config/packages/messenger.yaml. One of the keys that you're allowed to have below each transport is called serializer. Set this to a special string: messenger.transport.symfony_serializer.

framework:
messenger:
... lines 3 - 19
transports:
... line 21
async:
... line 23
serializer: messenger.transport.symfony_serializer
... lines 25 - 57

When a message is sent to a transport - whether that's Doctrine, AMQP or something else - it uses a "serializer" to encode that message into a string format that can be sent. Later, when it reads a message from a transport, it uses that same serializer to decode the data back into the message object.

Messenger comes with two "serializers" out-of-the-box. The first one is the PHP serializer... which is the default. The second is the "Symfony Serializer", which uses Symfony's Serializer component. That is the serializer service that we just switched to. If you don't already have the serializer component installed, make sure you install it with:

composer require "serializer:^1.0"

The Symfony serializer is great because it's really good at turning objects into JSON or XML, and it uses JSON by default. So... let's see what happens! Move back and delete another photo. Back in the Rabbit manager, I'll use the same trick as before to see what that message looks like.

Woh. This is fascinating! The payload is now... super simple: just a filename key set to the filename. This is the JSON representation of the message class, which is ImagePostDeletedEvent. Open that up: src/Message/Event/ImagePostDeletedEvent.php. Yep! The Symfony serializer turned this object's one property into JSON.

We're not going to go too deep into Symfony's serializer component, but if you want to know more, we go much deeper in our API Platform Tutorial.

Anyways, this simple JSON structure is something any other system could understand. So... we rock!

But... just as a challenge... if we did try to consume this message from our Symfony app... would it work? I'm not sure. If this message is consumed, how would the serializer know that this simple JSON string needs to decoded into an ImagePostDeletedEvent object? The answer... lies somewhere else in the message: the headers. That's next.

Leave a comment!

3
Login or Register to join the conversation
Roman P. Avatar
Roman P. Avatar Roman P. | posted 3 years ago | edited

Hi there Guys!
I've added this serializer to another project and faced a problem:


class SomeComand
{
    private int $id;

    public function __construct(int $id)
    {
        $this->id = $id;
    }

    public function id(): int
    {
        return $this->id;
    }
}

I don't want to use 'getter-setter' pattern and this is a problem for the serializer: the Serializer::encode method returns an empty array in body property
Is there any bypass for this?

Reply

Hey Roman P.!

Yea... the problem is that, by default. the Symfony serializer uses something called an ObjectNormalizer, which uses the PropertyAccessor component to figure out which fields to serialize. That is a fancy way of saying that it looks for public properties and getter methods. If you rename your method to getId() it will work perfectly. If you don't want to do this, you'll probably need to register your own custom serializer with a custom normalizer that is able to call the correct methods to get the data :).

Cheers!

Reply
Roman P. Avatar
Roman P. Avatar Roman P. | weaverryan | posted 3 years ago | edited

cool, I'll try this
thanks weaverryan =)
public properties equals no encapsulation

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