Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Format input and output fields with JMSSerializer (handle single property)

I would like to handle a single object property on serialization and deserialization with JMSSerializer. Suposing we have this class :

class Task {

    const STATUS_PENDING = 0;
    const STATUS_OVER    = 1;

    protected $status;

    /* getter and setter */

    public function getStatusLabel()
    {
        return ['pending', 'over'][$this->getStatus()];
    }

    public static function getStatusFromLabel($label)
    {
        return [
            'pending' => self::STATUS_PENDING,
            'over'    => self::STATUS_OVER
        ][$label];
    }
}

I would like to return instances of Task threw a REST API (using FOSRestBundle). The problem is that I don't want to return the raw value of the $status attribute, but the "label" value.

Configuring my serialization like this :

Task:
    exclusion_policy: ALL
    properties:
        status:
            expose: true
            type: string

The JMS Serializer considers the raw value which is 0 or 1, but I want to send 'pending' or 'over' in my serialized object (using getStatusLabel). And do the reverse job on deserialization (using getStatusFromLabel).

I thaught about a virtual_properties but it works only in serilization direction.

I tried with a custom handler looking like this :

class TaskHandler implements SubscribingHandlerInterface
{
    public static function getSubscribingMethods()
    {
        return [
            [
                'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => 'Task',
                'method' => 'serializeToArray',
            ]
        ];
    }

    public function serializeToArray(JsonSerializationVisitor $visitor, Task $task, array $type, Context $context)
    {
        $task->setStatus($task->getStatusLabel());
        return $visitor->getNavigator()->accept($task, $type, $context);
    }

But it obviously doesn't work !

How could I call my custom getters in both serilization and deserialization directions ?

like image 355
maphe Avatar asked Dec 19 '13 17:12

maphe


1 Answers

I finally found the answer.

First I had to create an event subsciber like this :

use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\Events;
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;

class TaskSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            [
                'event' => Events::PRE_SERIALIZE,
                'format' => 'json',
                'class' => 'Task', // fully qualified name here
                'method' => 'onPreSerializeTaskJson',
            ],
            [
                'event' => Events::PRE_DESERIALIZE,
                'format' => 'json',
                'class' => 'Task',
                'method' => 'onPreDeserializeTaskJson',
            ]
        ];
    }

    public function onPreSerializeTaskJson(PreSerializeEvent $event)
    {
        /** @var Task $task */
        $task = $event->getObject();

        $task->setStatus($task->getStatusLabel());
    }

    public function onPreDeserializeTaskJson(PreDeserializeEvent $event)
    {
        $data = $event->getData();

        $data['status'] = Task::getStatusFromLabel($data['status']);

        $event->setData($data);
    }
}

What I'm doing here :

  • Before serialization, I set the status value of my Task object with the label
  • Before deserialization, I change the value of the serialized object from the label to the raw integer value

For this solution, field has to be exposed (expose: true or @Expose) to the serializer.

Then I declared the subscriber as a service in Symfony with the tag jms_serializer.event_subscriber.

serializer.subscriber.task:
    class: %serializer.subscriber.task.class% # TaskSubscriber class path
    tags:
        - { name: jms_serializer.event_subscriber }

And it works.

It's the best way I found to both serialize and deserialize. It's also possible to manipulate data on post_serialize and post_deserialize events. For example, add a new field on a serialized object :

use JMS\Serializer\EventDispatcher\ObjectEvent;

public function onPostSerializeTaskJson(ObjectEvent $event)
{
    /** @var Task $task */
    $task = $event->getObject();

    $event->getVisitor()->addData('nb_related', count($task->getRelatedTasks()));
}
like image 106
maphe Avatar answered Oct 09 '22 17:10

maphe