symfony deserialize nested objects
Asked Answered
T

3

6

I have used the Symfony serializer to serialize my Recherche object. In a Recherche object, I have sub objects : Categorie and Lieu.

When I deserialize my Recherche object, all the sub objects are transformed in arrays. I would like them to be objects again.

This is how I have serialized my object:

$encoders = array(new JsonEncoder());
$normalizer = new ObjectNormalizer();
$normalizer->setIgnoredAttributes(array('parent', 'enfants'));
$normalizer->setCircularReferenceHandler(function ($object) {
    return $object->getCode();
});
$normalizers = array($normalizer);
$serializer = new Serializer($normalizers, $encoders);
$rechercheJson= $serializer->serialize($recherche, 'json');

And this is how I deserialize it :

$encoders = array(new JsonEncoder());
$normalizer = new ObjectNormalizer();
$normalizer->setIgnoredAttributes(array('parent', 'enfants'));
$normalizer->setCircularReferenceHandler(function ($object) {
    return $object->getCode();
});
$normalizers = array($normalizer);
$serializer = new Serializer($normalizers, $encoders);
$recherche = $serializer->deserialize($recherche_json, Recherche::class, 'json');

I think maybe there is something to do with normalizer, but I can't find anything that helps me in the docs.

Anyone has an idea to help ?

Thanks !

EDIT : After seeing this post : Denormalize nested structure in objects with symfony 2 serializer

I tried this :

$encoders = array(new JsonEncoder());
            $normalizer = new ObjectNormalizer(null, null, null, new SerializationPropertyTypeExtractor());
            $normalizer->setIgnoredAttributes(array('parent', 'enfants'));
            $normalizer->setCircularReferenceHandler(function ($object) {
                return $object->getCode();
            });
            $normalizers = array($normalizer,  new ArrayDenormalizer());
            $serializer = new Serializer($normalizers, $encoders);
            $recherche = $serializer->deserialize($recherche_json, Recherche::class, 'json');

And the SerializationPropertyTypeExtractor:

class SerializationPropertyTypeExtractor implements PropertyTypeExtractorInterface {
    /**
     * {@inheritdoc}
     */
    public function getTypes($class, $property, array $context = array())
    {
        if (!is_a($class, Recherche::class, true)) {
            return null;
        }

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

        if ('lieu' === $property)
        {
            return [new Type(Type::BUILTIN_TYPE_OBJECT, true, LieuRecherche::class)];
        }
        if ('categorie' === $property)
        {
            return [new Type(Type::BUILTIN_TYPE_OBJECT, true, Categorie::class)];
        }

        return null;
    }
}

And this works well !

Thermaesthesia answered 11/4, 2018 at 15:22 Comment(2)
I usually use the strategy pattern with symfony's serializer component. It allows you to define exactly what a normalized and denormalized object/array should look like. I'll send an example in a bit.Laurustinus
Yep, an example could be good as I don't understand :-)Thermaesthesia
N
24

I had a similar issue and tried to solve the problem with a custom PropertyTypeExtractor. Since I have many Entities with nested Objects this felt quite cumbersome also it doesn't work when the nested Object has nested Object again.

I found a better solution using the PhpDocExtractor and the ReflectionExtractor, which extracts the property info for you.

$encoder = [new JsonEncoder()];
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = [new ArrayDenormalizer(), new ObjectNormalizer(null, null, null, $extractor)];
$serializer = new Serializer($normalizer, $encoder);
$result = $serializer->deserialize($data,someEntity::class,'json');

This does all the work for me. I hope this will help someone.

Northington answered 21/2, 2020 at 15:23 Comment(0)
L
0

Ref: my comment. I know you are using the serializer in the classic way but I recommend you apply the recommended strategy pattern, that way you have full control over the component and can expand easily.

So the idea is you create separate classes for normalization and denormaliztion like so:

./src/Denormalizer

<?php

declare(strict_types=1);

namespace App\Denormalizer;

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
 * Class ConfigDenormalizer
 * @package App\Denormalizer
 */
class ConfigDenormalizer implements DenormalizerInterface
{

    /**
     * @param mixed $data
     * @param string $class
     * @param null $format
     * @param array $context
     * @return array
     */
    public function denormalize($data = [], $class, $format = null, array $context = array())
    {
        $result = [];
        foreach($data as $key => $config) {
            $result[ $config['attribute'] ] = $config['valorem'];
        }
        return $result;
    }

    /**
     * @param mixed $data
     * @param string $type
     * @param null $format
     * @return bool
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        return $type === self::class;
    }
}

./src/Normalizer

<?php

declare(strict_types=1);

namespace App\Normalizer;

use Symfony\Component\Form\Form;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
 * Class WidgetConfigNormalizer
 * @package App\Normalizer
 */
class WidgetAttributeNormalizer implements NormalizerInterface
{
    /**
     * @param object $form
     * @param null $format
     * @param array $context
     * @return array
     */
    public function normalize($form, $format = null, array $context = array()): array
    {
        $result = [];

        foreach($form->getData() as $key => $value) {
            $result[] = [
                'attribute' => (string) $key,
                'valorem' => (string) $value,
            ];
        }

        return $result;
    }

    /**
     * @param mixed $form
     * @param null $format
     * @return bool
     */
    public function supportsNormalization($form, $format = null)
    {
        if($form instanceof Form) {
            return $form->getName() === 'widgetAttribute';
        }
    }

}

and you would call it like this:

//denormalize
$this->denormalizer->denormalize(
    json_decode($response->getContent(), true),
    ConfigDenormalizer::class
);

//normalize
$form = $this->formFactory->create(myConfigFormType::class);
$form->submit($data);
$this->normalizer->normalize($form);

Or if you wanted to use the serializer (note we don't need to json_decode):

//deserialize
$array = $this->serializer->deserialize(
    $response->getContent(),
    ConfigDenormalizer::class,
    'json'
);

//serialize
$json = $this->serialize->serialize($form, 'json');

Of course in your denormalizer you could convert your array into a plain old php object (entity). or just output an array, the choice is yours.

This way all you have yo do is inject the SerializerInterface into your Controllers like this:

use Symfony\Component\Serializer\SerializerInterface;

class EmbedController extends Controller
{
    /**
    * @var SerializerInterface
    */
    private $serializer;

    public function __construct(SerializerInterface $serializer)
    {
        $this->serializer = $serializer;
    }

}

This method makes unit testing much easier too as everything is decoupled so can be mocked up. :)

Its also worth checking the docs: https://symfony.com/doc/current/components/serializer.html

Especially the image at the top, its a great memory prompt as to which way around you should be using each class.

Hope this helps.

Laurustinus answered 11/4, 2018 at 15:52 Comment(4)
If you are using Autowiring you shouldn't need to define your (de)normalizers as services or tag them either, this should just work out of the box. as long as you have done composer require serializer ;)Laurustinus
Thanks Edward. If I understand well, you do almost all the job by yourself, right ? The Symfony serializer is almost useless, no ?Thermaesthesia
It's up to you, if you want to serialize or deserialize without custom (de)normalizers you use the serializer service directly $json = $this->serializer->serialize($object, 'json'); but in your question you wanted a custom structure, to do this you need custom (de)normalizers.Laurustinus
I found how to do that. As you see in the edit part of my question, I have tested a solution, which didn't seem to work, but finally work well. Thank for your helpThermaesthesia
D
0

If you are using Symfony itself (with dependency injection) and not just the serializer on its own, it's probably available to you preconfigured, just add SerializerInterface $serializer to your constructor/method parameters, as documented here.

I spent a lot of time trying (unsuccessfully) to set up the serializer manually before I discovered that it was already available to me, with everything already configured...

Denise answered 14/3, 2023 at 20:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.