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)
0 Comments:
Post a Comment
Note: Only a member of this blog may post a comment.