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?

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

5 Comments

You're right I have a typo in my json. I updated my question.
ObjectNormaliser require only 3 arguments in the constructor and the third one implements PropertyAccessorInterface, right ?
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.
Ok it's only available in major version 3.0
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)
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());

Comments

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');

Comments

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"
    }
  ]
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.