If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOnce 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!
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.
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.
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!
// 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
}
}
Hi there Guys!
I've added this serializer to another project and faced a problem:
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 propertyIs there any bypass for this?