Search code examples
phpsymfonyserialization

Preprocess property during deserialization


Let say I have the following class:

final class Foo
{
    public string $bar;
}

The following data

{
    "bar": "SGVsbG8gd29ybGQ="
}

And the follwoing symfony/serializer:

return new Serializer(
    [
        new ArrayDenormalizer(),
        new ObjectNormalizer(
            propertyTypeExtractor: new PropertyInfoExtractor(
                typeExtractors: [
                    new ReflectionExtractor()
                ]
            )
        )
    ],
    [new JsonEncoder()]
);

When deserializing the data, an object with the correct data is created.

$object = $serializer->deserialize('{"bar": "SGVsbG8gd29ybGQ="}', Foo::class, 'json');

$object is of type Foo and the property $bar contains "SGVsbG8gd29ybGQ=".

I would like to "preprocess" the property $bar and decode (base64) the property and instead of having the raw value, I would like to have "Hello world". I tried using the context of a custom denormalizer, but had no success. Is there any way for achieving this?

Many thanks. Regards.


Solution

  • I finally found a simple and effective solution: use a normalizer which re-executes the normalization afterwards. To be sure that the decoding is not performed multiple times, a value in the context is added.

    <?php
    
    declare(strict_types=1);
    
    use Symfony\Component\Serializer\Exception\BadMethodCallException;
    use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
    use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
    use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
    
    final class FooDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
    {
        use DenormalizerAwareTrait;
    
        private const ALREADY_CALLED = 'FOO_PREPROCESS_ALREADY_CALLED';
    
    
        public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
        {
            if ($this->denormalizer === null) {
                throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!');
            }
    
            $data['bar'] = base64_decode($data['bar']);
            $context[self::ALREADY_CALLED] = true;
    
            return $this->denormalizer->denormalize($data, $type, $format, $context);
        }
    
        public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
        {
            if ($context[self::ALREADY_CALLED] ?? false) {
                return false;
            }
    
            return $type === Foo::class;
        }
    }
    

    This custom normalizer is added to the list:

    return new Serializer(
        [
            new FooDenormalizer(),
            new ArrayDenormalizer(),
            new ObjectNormalizer(
                propertyTypeExtractor: new PropertyInfoExtractor(
                    typeExtractors: [
                        new ReflectionExtractor()
                    ]
                )
            )
        ],
        [new JsonEncoder()]
    );
    

    When the deserialization is done, the normalizer will be called twice:

    • The first time the value is decoded
    • The second time it is skipped because the context contains self::ALREADY_CALLED with the value true