Denormalize nested structure in objects with Symfony 2 serializer
Asked Answered
C

4

16

I'm working on a Symfony 2 project with version 2.8 and I'm using the build-in component Serializer -> http://symfony.com/doc/current/components/serializer.html

I have a JSON structure provided by a web service. After deserialization, I want to denormalize my content in objects. Here is my structure (model/make in a car application context).

[{
"0": {
    "id": 0,
    "code": 1,
    "model": "modelA",
    "make": {
        "id": 0,
        "code": 1,
        "name": "makeA"
    }
  }
} , {
 "1": {
    "id": 1,
    "code": 2,
    "model": "modelB",
    "make": {
        "id": 0,
        "code": 1,
        "name": "makeA"
    }
  }
}]

My idea is to populate a VehicleModel object which contains a reference to a VehicleMake object.

class VehicleModel {
    public $id;
    public $code;
    public $model;
    public $make; // VehicleMake
}

Here is what I do:

// Retrieve data in JSON
$data = ...
$serializer = new Serializer([new ObjectNormalizer(), new ArrayDenormalizer()], [new JsonEncoder()]);
$models = $serializer->deserialize($data, '\Namespace\VehicleModel[]', 'json');

In result, my object VehicleModel is correctly populated but $make is logically a key/value array. Here I want a VehicleMake instead.

Is there a way to do that?

Calves answered 14/10, 2016 at 1:55 Comment(0)
I
10

The ObjectNormalizer needs more configuration. You will at least need to supply the fourth parameter of type PropertyTypeExtractorInterface.

Here's a (rather hacky) example:

<?php
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

$a = new VehicleModel();
$a->id = 0;
$a->code = 1;
$a->model = 'modalA';
$a->make = new VehicleMake();
$a->make->id = 0;
$a->make->code = 1;
$a->make->name = 'makeA';

$b = new VehicleModel();
$b->id = 1;
$b->code = 2;
$b->model = 'modelB';
$b->make = new VehicleMake();
$b->make->id = 0;
$b->make->code = 1;
$b->make->name = 'makeA';

$data = [$a, $b];

$serializer = new Serializer(
    [new ObjectNormalizer(null, null, null, new class implements PropertyTypeExtractorInterface {
        /**
         * {@inheritdoc}
         */
        public function getTypes($class, $property, array $context = array())
        {
            if (!is_a($class, VehicleModel::class, true)) {
                return null;
            }

            if ('make' !== $property) {
                return null;
            }

            return [
                new Type(Type::BUILTIN_TYPE_OBJECT, true, VehicleMake::class)
            ];
        }
    }), new ArrayDenormalizer()],
    [new JsonEncoder()]
);

$json = $serializer->serialize($data, 'json');
print_r($json);

$models = $serializer->deserialize($json, VehicleModel::class . '[]', 'json');
print_r($models);

Note that in your example json, the first entry has an array as value for make. I took this to be a typo, if it's deliberate, please leave a comment.

To make this more automatic you might want to experiment with the PhpDocExtractor.

Isolation answered 14/10, 2016 at 8:36 Comment(5)
You're right I have a typo in my json. I updated my question.Calves
ObjectNormaliser require only 3 arguments in the constructor and the third one implements PropertyAccessorInterface, right ?Calves
Oh, I only tested this on sf3. So there might have been a change in the api. If in v2.8 there is no way to add a type extractor, then this answer might be not suitable for you.Isolation
Ok it's only available in major version 3.0Calves
I'm using symfony 2.8 and i'm facing the same problem here. I've made an external bundle using symfony 3.2 while developing and when i've imported the bundle into a symfony 2.8 project the deserialization is not recursive. The feature is only available on symfony >3.1 versions [github.com/symfony/symfony/blob/3.1/src/Symfony/Component/… source code on symfony 3.1) [symfony.com/doc/current/components/… recursive denormalization docs)Tristich
B
4

In cases when you need more flexibility in denormalization it's good to create your own denormalizers.

$serializer = new Serializer(
  [
    new ArrayNormalizer(), 
    new VehicleDenormalizer(), 
    new VehicleMakeDenormalizer()
  ], [
    new JsonEncoder()
  ]
);
$models = $serializer->deserialize(
  $data, 
  '\Namespace\VehicleModel[]', 
  'json'
);

Here the rough code of such denormalizer

class VehicleDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
    {
      public function denormalize($data, $class, $format, $context) 
      {
        $vehicle = new VehicleModel();
        ...
        $vehicleMake = $this->denormalizer->denormalize(
          $data->make,
          VehicleMake::class,
          $format,
          $context
        );
        $vehicle->setMake($vehicleMake);
        ...
      }
    }

I only have doubts on should we rely on $this->denormalizer->denormalize (which works properly just because we use Symfony\Component\Serializer\Serializer) or we must explicitly inject VehicleMakeDenormalizer into VehicleDenormalizer

$vehicleDenormalizer = new VehicleDenormalizer();
$vehicleDenormalizer->setVehicleMakeDenormalizer(new VehicleMakeDenormalizer());
Benzophenone answered 16/12, 2016 at 7:30 Comment(0)
V
1

The easiest way would be to use the ReflectionExtractor if your Vehicle class has some type hints.

class VehicleModel {
    public $id;
    public $code;
    public $model;
    /** @var VehicleMake */
    public $make;
}

You can pass the Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor as argument to the ObjectNormalizer when you initialize the Serializer

$serializer = new Serializer([new ObjectNormalizer(null, null, null, new ReflectionExtractor()), new ArrayDenormalizer()], [new JsonEncoder()]);
$models = $serializer->deserialize($data, '\Namespace\VehicleModel[]', 'json');
Vaudois answered 2/9, 2020 at 21:49 Comment(0)
U
1

In Symfony4+, you can inject the serializer and it will do the job for you based on either your phpdoc (eg @var) or type hinting. Phpdoc seems safer as it manages collections of objects.

Example:

App\Model\Skill.php

<?php

namespace App\Model;

class Skill
{
    public $name = 'Taxi Driver';

    /** @var Category */
    public $category;

    /** @var Person[] */
    public $people = [];
}

App\Model\Category.php

<?php

namespace App\Model;

class Category
{
    public $label = 'Transports';
}

App\Model\Person.php

<?php

namespace App\Model;

class Person
{
    public $firstname;
}

App\Command\TestCommand.php

<?php

namespace App\Command;

use App\Model\Category;
use App\Model\Person;
use App\Model\Skill;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Serializer\SerializerInterface;

class TestCommand extends Command
{
    /**
     * @var SerializerInterface
     */
    private $serializer;

    public function __construct(SerializerInterface $serializer)
    {
        parent::__construct();

        $this->serializer = $serializer;
    }

    protected function configure()
    {
        parent::configure();

        $this
            ->setName('test')
            ->setDescription('Does stuff');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $personA            = new Person();
        $personA->firstname = 'bruno';
        $personB            = new Person();
        $personB->firstname = 'alice';

        $badge           = new Skill();
        $badge->name     = 'foo';
        $badge->category = new Category();
        $badge->people   = [$personA, $personB];

        $output->writeln(
            $serialized = $this->serializer->serialize($badge, 'json')
        );

        $test = $this->serializer->deserialize($serialized, Skill::class, 'json');

        dump($test);

        return 0;
    }
}

Will give the following expected result:

{"name":"foo","category":{"label":"Transports"},"people":[{"firstname":"bruno"},{"firstname":"alice"}]}

^ App\Model\BadgeFacade^ {#2531
  +name: "foo"
  +category: App\Model\CategoryFacade^ {#2540
    +label: "Transports"
  }
  +people: array:2 [
    0 => App\Model\PersonFacade^ {#2644
      +firstname: "bruno"
    }
    1 => App\Model\PersonFacade^ {#2623
      +firstname: "alice"
    }
  ]
}
Ultramicroscopic answered 21/1, 2021 at 7:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.