Description
Symfony version(s) affected: ^4.3
Description
I am using Symfony Serializer to deserialize complex XML documents into custom objects. Some XML nodes have optional attributes that may be present or not.
For example, the "price" node can have a "currency" attribute or not:
<price currency="EUR">66</price>
or
<price>55</price>
are both valid.
I am using MetadataAwareNameConverter to map our class attributes to xml attributes.
In the first case, the Price object is correctly created and both attributes are filled in, but in the second case the Price object is created by both attributes are left NULL.
How to reproduce
Our Price class is this one:
use Symfony\Component\Serializer\Annotation\SerializedName;
class Price {
/**
* @SerializedName("@currency")
* @var string
*/
private $currency;
/**
* @SerializedName("#")
* @var string
*/
private $amount;
public function getCurrency() {
return $this->currency;
}
public function setCurrency(string $currency) {
$this->currency = $currency;
}
public function getAmount() {
return $this->amount;
}
public function setAmount(string $amount) {
$this->amount = $amount;
}
}
This test case shows the correct behaviour when "currency" attribute is present:
function testWorks() {
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$encoders = [new XmlEncoder()];
$normalizers = [new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)];
$serializer = new Serializer($normalizers, $encoders);
$xml = '<price currency="EUR">55</price>';
$object = $serializer->deserialize($xml, Price::class, 'xml');
$this->assertEquals('EUR', $object->getCurrency());
$this->assertEquals(55, $object->getAmount());
}
but this test case fails (Amount is also NULL!):
function testFails() {
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$encoders = [new XmlEncoder()];
$normalizers = [new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)];
$serializer = new Serializer($normalizers, $encoders);
$xml = '<price>55</price>';
$object = $serializer->deserialize($xml, Price::class, 'xml');
$this->assertNull($object->getCurrency());
$this->assertEquals(55, $object->getAmount());
}
Possible Solution
I have found that when denormalizing the attributes, the name converter receives an array with key '0' as property name. I have implemented a CustomMetadataAwareNameConverter that replaces the property name '0' by '#', and it works now..
This is the CustomMetadataAwareNameConverter class:
class CustomMetadataAwareNameConverter implements AdvancedNameConverterInterface {
/**
* The real MetadataAwareNameConverter
*
* @var MetadataAwareNameConverter
*/
private $converter;
public function __construct(ClassMetadataFactoryInterface $metadataFactory, NameConverterInterface $fallbackNameConverter = null) {
$this->converter = new MetadataAwareNameConverter($metadataFactory, $fallbackNameConverter);
}
public function normalize($propertyName, string $class = null, string $format = null, array $context = []) {
return $this->converter->normalize($propertyName, $class, $format, $context);
}
public function denormalize($propertyName, string $class = null, string $format = null, array $context = []) {
return $this->converter->denormalize($propertyName == '0' ? '#' : $propertyName, $class, $format, $context);
}
}
and the test that previously failed works now:
function testFixed() {
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$metadataAwareNameConverter = new CustomMetadataAwareNameConverter($classMetadataFactory);
$encoders = [new XmlEncoder()];
$normalizers = [new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)];
$serializer = new Serializer($normalizers, $encoders);
$xml = '<price>55</price>';
$object = $serializer->deserialize($xml, Price::class, 'xml');
$this->assertNull($object->getCurrency());
$this->assertEquals(55, $object->getAmount());
}