From 50d086ce1f045ca1472ed80e23389daca8b4ebd7 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 22 Nov 2023 06:10:11 +0100 Subject: [PATCH] [Serializer] Move discrimination to abstract --- .../Normalizer/AbstractObjectNormalizer.php | 18 +++++- .../Normalizer/GetSetMethodNormalizer.php | 4 ++ .../Normalizer/ObjectNormalizer.php | 16 ++--- .../Normalizer/PropertyNormalizer.php | 4 ++ .../Normalizer/GetSetMethodNormalizerTest.php | 63 +++++++++++++++++++ .../Normalizer/PropertyNormalizerTest.php | 43 +++++++++++++ 6 files changed, 136 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 141ed4bb019ad..ea23899e71caa 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -179,8 +179,15 @@ public function normalize($object, string $format = null, array $context = []) $attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context); + $discriminatorProperty = null; + if (null !== $this->classDiscriminatorResolver && null !== $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) { + $discriminatorProperty = $mapping->getTypeProperty(); + } + try { - $attributeValue = $this->getAttributeValue($object, $attribute, $format, $attributeContext); + $attributeValue = $attribute === $discriminatorProperty + ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) + : $this->getAttributeValue($object, $attribute, $format, $attributeContext); } catch (UninitializedPropertyException $e) { if ($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) { continue; @@ -386,8 +393,15 @@ public function denormalize($data, string $type, string $format = null, array $c } if ($attributeContext[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) { + $discriminatorProperty = null; + if (null !== $this->classDiscriminatorResolver && null !== $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) { + $discriminatorProperty = $mapping->getTypeProperty(); + } + try { - $attributeContext[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $attributeContext); + $attributeContext[self::OBJECT_TO_POPULATE] = $attribute === $discriminatorProperty + ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) + : $this->getAttributeValue($object, $attribute, $format, $attributeContext); } catch (NoSuchPropertyException $e) { } } diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index d9339df64df5c..484a8fd4b7aae 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -67,6 +67,10 @@ public function hasCacheableSupportsMethod(): bool */ private function supports(string $class): bool { + if (null !== $this->classDiscriminatorResolver && $this->classDiscriminatorResolver->getMappingForClass($class)) { + return true; + } + $class = new \ReflectionClass($class); $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 1bce3ebeb1562..eb3d9716a13ef 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -30,8 +30,6 @@ class ObjectNormalizer extends AbstractObjectNormalizer { protected $propertyAccessor; - private $discriminatorCache = []; - private $objectClassResolver; public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) @@ -128,16 +126,14 @@ protected function extractAttributes(object $object, string $format = null, arra */ protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []) { - $cacheKey = \get_class($object); - if (!\array_key_exists($cacheKey, $this->discriminatorCache)) { - $this->discriminatorCache[$cacheKey] = null; - if (null !== $this->classDiscriminatorResolver) { - $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object); - $this->discriminatorCache[$cacheKey] = null === $mapping ? null : $mapping->getTypeProperty(); - } + $discriminatorProperty = null; + if (null !== $this->classDiscriminatorResolver && null !== $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) { + $discriminatorProperty = $mapping->getTypeProperty(); } - return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute); + return $attribute === $discriminatorProperty + ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) + : $this->propertyAccessor->getValue($object, $attribute); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 38d81d9c9615e..03060344690b1 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -61,6 +61,10 @@ public function hasCacheableSupportsMethod(): bool */ private function supports(string $class): bool { + if (null !== $this->classDiscriminatorResolver && $this->classDiscriminatorResolver->getMappingForClass($class)) { + return true; + } + $class = new \ReflectionClass($class); // We look for at least one non-static property diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index c2d670cfe5838..cdbb2d6fb0c79 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -16,7 +16,9 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -498,6 +500,27 @@ protected function getNormalizerForSkipUninitializedValues(): NormalizerInterfac { return new GetSetMethodNormalizer(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); } + + public function testNormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, null, $discriminator); + + $this->assertSame(['type' => 'one', 'url' => 'URL_ONE'], $normalizer->normalize(new GetSetMethodDiscriminatedDummyOne())); + } + + public function testDenormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, null, $discriminator); + + $denormalized = new GetSetMethodDiscriminatedDummyTwo(); + $denormalized->setUrl('url'); + + $this->assertEquals($denormalized, $normalizer->denormalize(['type' => 'two', 'url' => 'url'], GetSetMethodDummyInterface::class)); + } } class GetSetDummy @@ -762,3 +785,43 @@ public function __call($key, $value) throw new \RuntimeException('__call should not be called. Called with: '.$key); } } + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "one" = GetSetMethodDiscriminatedDummyOne::class, + * "two" = GetSetMethodDiscriminatedDummyTwo::class, + * }) + */ +interface GetSetMethodDummyInterface +{ +} + +class GetSetMethodDiscriminatedDummyOne implements GetSetMethodDummyInterface +{ + private string $url = 'URL_ONE'; + + public function getUrl(): string + { + return $this->url; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } +} + +class GetSetMethodDiscriminatedDummyTwo implements GetSetMethodDummyInterface +{ + private string $url = 'URL_TWO'; + + public function getUrl(): string + { + return $this->url; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 2cf3a2ae0e6c8..f5b830c875fab 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -16,7 +16,9 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -457,6 +459,27 @@ protected function getNormalizerForSkipUninitializedValues(): NormalizerInterfac { return new PropertyNormalizer(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); } + + public function testNormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new PropertyNormalizer($classMetadataFactory, null, null, $discriminator); + + $this->assertSame(['type' => 'one', 'url' => 'URL_ONE'], $normalizer->normalize(new PropertyDiscriminatedDummyOne())); + } + + public function testDenormalizeWithDiscriminator() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new PropertyNormalizer($classMetadataFactory, null, null, $discriminator); + + $denormalized = new PropertyDiscriminatedDummyTwo(); + $denormalized->url = 'url'; + + $this->assertEquals($denormalized, $normalizer->denormalize(['type' => 'two', 'url' => 'url'], PropertyDummyInterface::class)); + } } class PropertyDummy @@ -560,3 +583,23 @@ public function getIntMatrix(): array return $this->intMatrix; } } + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "one" = PropertyDiscriminatedDummyOne::class, + * "two" = PropertyDiscriminatedDummyTwo::class, + * }) + */ +interface PropertyDummyInterface +{ +} + +class PropertyDiscriminatedDummyOne implements PropertyDummyInterface +{ + public string $url = 'URL_ONE'; +} + +class PropertyDiscriminatedDummyTwo implements PropertyDummyInterface +{ + public string $url = 'URL_TWO'; +}