PHPFixing
  • Privacy Policy
  • TOS
  • Ask Question
  • Contact Us
  • Home
  • PHP
  • Programming
  • SQL Injection
  • Web3.0

Thursday, May 12, 2022

[FIXED] How to deserialize a nested array of objects declared on the constructor via promoted properties, with Symfony Serializer?

 May 12, 2022     serialization, symfony, symfony5     No comments   

Issue

Take the following DTO classes:

class UserDTO {
    /**
     * @param AddressDTO[] $addressBook
     */
    public function __construct(
        public string $name,
        public int $age,
        public ?AddressDTO $billingAddress,
        public ?AddressDTO $shippingAddress,
        public array $addressBook,
    ) {
    }
}

class AddressDTO {
    public function __construct(
        public string $street,
        public string $city,
    ) {
    }
}

I'd like to serialize and deserialize them to/from JSON.

I'm using the following Serializer configuration:

$encoders = [new JsonEncoder()];

$extractor = new PropertyInfoExtractor([], [
    new PhpDocExtractor(),
    new ReflectionExtractor(),
]);

$normalizers = [
    new ObjectNormalizer(null, null, null, $extractor),
    new ArrayDenormalizer(),
];

$serializer = new Serializer($normalizers, $encoders);

But when serializing/deserializing this object:

$address = new AddressDTO('Rue Paradis', 'Marseille');
$user = new UserDTO('John', 25, $address, null, [$address]);

$jsonContent = $serializer->serialize($user, 'json');
dd($serializer->deserialize($jsonContent, UserDTO::class, 'json'));

I get the following result:

UserDTO^ {#54
  +name: "John"
  +age: 25
  +billingAddress: AddressDTO^ {#48
    +street: "Rue Paradis"
    +city: "Marseille"
  }
  +shippingAddress: null
  +addressBook: array:1 [
    0 => array:2 [
      "street" => "Rue Paradis"
      "city" => "Marseille"
    ]
  ]
}

When I would expect:

UserDTO^ {#54
  +name: "John"
  +age: 25
  +billingAddress: AddressDTO^ {#48
    +street: "Rue Paradis"
    +city: "Marseille"
  }
  +shippingAddress: null
  +addressBook: array:1 [
    0 => AddressDTO^ {#48
      +street: "Rue Paradis"
      +city: "Marseille"
    }
  ]
}

As you can see, $addressBook is deserialized as an array of array, instead of an array of AddressDTO. I expected the PhpDocExtractor to read the @param AddressDTO[] from the constructor, but this does not work.

It only works if I make $addressBook a public property documented with @var.

Is there a way to make it work with a simple @param on the constructor?

(Non-)working-demo: https://phpsandbox.io/n/gentle-mountain-mmod-rnmqd


What I've read and tried:

  • Extract types of constructor parameters from docblock comment
  • symfony deserialize nested objects
  • How can I deserialize an array of objects in Symfony Serializer?

None of the proposed solutions seem to work for me.


Solution

Apparently the issue is that the PhpDocExtractor does not extract properties from constructors. You need to use a specific extractor for this:

use Symfony\Component\PropertyInfo;
use Symfony\Component\Serializer;

$phpDocExtractor = new PropertyInfo\Extractor\PhpDocExtractor();
$typeExtractor   = new PropertyInfo\PropertyInfoExtractor(
    typeExtractors: [ new PropertyInfo\Extractor\ConstructorExtractor([$phpDocExtractor]), $phpDocExtractor,]
);

$serializer = new Serializer\Serializer(
    normalizers: [
                    new Serializer\Normalizer\ObjectNormalizer(propertyTypeExtractor: $typeExtractor),
                    new Serializer\Normalizer\ArrayDenormalizer(),
                 ],
    encoders:    ['json' => new Serializer\Encoder\JsonEncoder()]
);

With this you'll get the desired results. Took me a bit to figure it out. The multiple denormalizer/extractor chains always get me.


Alternatively, for more complex os specialized situations, you could create your own custom denormalizer:

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait

class UserDenormalizer
    implements DenormalizerInterface, DenormalizerAwareInterface
{

    use DenormalizerAwareTrait;

    public function denormalize($data, string $type, string $format = null, array $context = [])
    {
        $addressBook = array_map(fn($address) => $this->denormalizer->denormalize($address, AddressDTO::class), $data['addressBook']);

        return new UserDTO(
            name:            $data['name'],
            age:             $data['age'],
            billingAddress:  $this->denormalizer->denormalize($data['billingAddress'], AddressDTO::class),
            shippingAddress: $this->denormalizer->denormalize($data['shippingAddress'], AddressDTO::class),
            addressBook:     $addressBook
        );
    }

    public function supportsDenormalization($data, string $type, string $format = null)
    {
        return $type === UserDTO::class;
    }
}

Setup would become this:

$extractor = new PropertyInfoExtractor([], [
    new PhpDocExtractor(),
    new ReflectionExtractor(),
    
]);

$userDenormalizer = new UserDenormalizer();
$normalizers      = [
    $userDenormalizer,
    new ObjectNormalizer(null, null, null, $extractor),
    new ArrayDenormalizer(),

];
$serializer       = new Serializer($normalizers, [new JsonEncoder()]);
$userDenormalizer->setDenormalizer($serializer);

Output becomes what you would expect:

^ UserDTO^ {#39
  +name: "John"
  +age: 25
  +billingAddress: AddressDTO^ {#45
    +street: "Rue Paradis"
    +city: "Marseille"
  }
  +shippingAddress: null
  +addressBook: array:2 [
    0 => AddressDTO^ {#46
      +street: "Rue Paradis"
      +city: "Marseille"
    }
  ]
}


Answered By - yivi
Answer Checked By - Katrina (PHPFixing Volunteer)
  • Share This:  
  •  Facebook
  •  Twitter
  •  Stumble
  •  Digg
Newer Post Older Post Home

0 Comments:

Post a Comment

Note: Only a member of this blog may post a comment.

Total Pageviews

Featured Post

Why Learn PHP Programming

Why Learn PHP Programming A widely-used open source scripting language PHP is one of the most popular programming languages in the world. It...

Subscribe To

Posts
Atom
Posts
Comments
Atom
Comments

Copyright © PHPFixing