Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Serializar mensajes como 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

Una vez que empiezas a usar RabbitMQ, se hace posible un flujo de trabajo totalmente diferente... un flujo de trabajo que es especialmente común con sistemas más grandes. La idea es que el código que envía un mensaje puede no ser el mismo código que consume y maneja ese mensaje. Nuestra aplicación es responsable tanto de enviar los mensajes a RabbitMQ como, aquí en el terminal, de consumir los mensajes de la cola y manejarlos.

¿Pero qué pasa si queremos enviar uno o más mensajes a RabbitMQ con la expectativa de que algún otro sistema -quizás algún código escrito en un lenguaje diferente y desplegado en un servidor diferente- lo consuma y lo maneje? ¿Cómo podemos hacerlo?

Bueno... a alto nivel... ¡es fácil! Si quisiéramos enviar cosas a este transporte async... pero no pensáramos consumir esos mensajes, ¡no tendríamos que cambiar nada en nuestro código! No, simplemente... no consumiríamos mensajes de ese transporte al utilizar el comando messenger:consume. Podríamos seguir consumiendo mensajes de otros transportes, sólo que no leeríamos estos... porque sabemos que alguien más lo hará. ¡Ya está! ¡Victoria! ¡Café!

¿Cómo se formatean nuestros mensajes?

Pero... si fueras a enviar datos a otro sistema, ¿cómo formatearías normalmente esos datos? Bueno, para usar un ejemplo más familiar, cuando envías datos a una ruta de la API, normalmente los formateas como JSON... o quizás XML. Lo mismo ocurre en el mundo de las colas. Puedes enviar un mensaje a RabbitMQ en cualquier formato... siempre que quien consuma ese mensaje entienda el formato. Así que... ¿qué formato estamos utilizando ahora? ¡Vamos a averiguarlo!

Iré a la cola messages_normal... y para estar seguros, vaciémosla. Los mensajes enviados al transporte async acabarán en esta cola... y las clases ImagePostDeleteEvent se dirigen a ella. Bien, volvemos a nuestra aplicación, borramos una foto y luego, mirando nuestra cola, en un momento... ¡ahí está! Nuestra cola contiene el único mensaje nuevo.

Veamos cómo es exactamente este mensaje. Abajo, hay un punto para sacar un mensaje. Pero... por alguna razón... esto no me ha funcionado. Para evitarlo, abriré mis herramientas de red, haré clic en "Obtener mensaje(s)" de nuevo... y miraré la petición AJAX que acaba de hacer. Abre los datos devueltos y pasa el ratón por encima de la propiedad payload.

Sí, este es el aspecto de nuestro mensaje en la cola: este es el cuerpo del mensaje. ¿Qué es ese feo formato? Es un objeto PHP serializado! Cuando Messenger consume esto, sabe que debe utilizar la función unserialize para volver a convertirlo en un objeto... y así, ¡este formato funciona de maravilla!

Pero si esperamos que una aplicación PHP diferente consuma esto... des-serializarlo no funcionará porque estas clases probablemente no existirán. Y si el código que manejará esto está escrito en un lenguaje diferente, pfff, ni siquiera tendrán la oportunidad de leer y entender este formato específico de PHP.

La cuestión es: usar la serialización de PHP funciona muy bien cuando la aplicación que envía el mensaje también lo maneja. Pero funciona horriblemente cuando ese no es el caso. En su lugar, probablemente querrás utilizar JSON o XML.

Utilizar el serializador de Symfony

Afortunadamente, utilizar un formato diferente es fácil. Voy a purgar ese mensaje de la cola una vez más. Muévete y abre config/packages/messenger.yaml. Una de las claves que puedes tener debajo de cada transporte se llamaserializer. Ponlo en una cadena especial: messenger.transport.symfony_serializer.

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

Cuando se envía un mensaje a un transporte -ya sea Doctrine, AMQP o cualquier otro-, éste utiliza un "serializador" para codificar ese mensaje en un formato de cadena que pueda ser enviado. Más tarde, cuando lee un mensaje de un transporte, utiliza ese mismo serializador para descodificar los datos de nuevo en el objeto mensaje.

Messenger viene con dos "serializadores" de fábrica. El primero es el serializador de PHP... que es el predeterminado. El segundo es el "Serializador Symfony", que utiliza el componente Serializador de Symfony. Ese es el servicio de serializador al que acabamos de cambiar. Si no tienes ya instalado el componente serializador, asegúrate de instalarlo con:

composer require "serializer:^1.0"

El serializador de Symfony es genial porque es realmente bueno convirtiendo objetos en JSON o XML, y utiliza JSON por defecto. Así que... ¡vamos a ver qué pasa! Retrocede y elimina otra foto. De vuelta en el gestor de Rabbit, utilizaré el mismo truco que antes para ver cómo es ese mensaje.

Woh. ¡Esto es fascinante! El payload es ahora... súper sencillo: sólo una clave filenameestablecida en el nombre del archivo. Esta es la representación JSON de la clase de mensaje, que es ImagePostDeletedEvent. Abre eso:src/Message/Event/ImagePostDeletedEvent.php. ¡Sí! El serializador de Symfony ha convertido la única propiedad de este objeto en JSON.

No vamos a profundizar demasiado en el componente serializador de Symfony, pero si quieres saber más, profundizamos mucho más en nuestro Tutorial de la Plataforma API.

En cualquier caso, esta sencilla estructura JSON es algo que cualquier otro sistema podría entender. Así que... ¡somos lo máximo!

Pero... sólo como reto... si intentáramos consumir este mensaje desde nuestra aplicación Symfony... ¿funcionaría? No estoy seguro. Si se consume este mensaje, ¿cómo sabría el serializador que esta simple cadena JSON debe descodificarse en un objeto ImagePostDeletedEvent? La respuesta... está en otro lugar del mensaje: las cabeceras. Eso a continuación.

Leave a comment!

3
Login or Register to join the conversation
Roman P. Avatar
Roman P. Avatar Roman P. | posted hace 3 años | 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 hace 3 años | 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!

Este tutorial está construido con Symfony 4.3, pero funcionará bien en Symfony 4.4 o 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