From e7115897d7e6c5e8fc6108b694a11e5394c67cfc Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 10:37:02 -0600 Subject: [PATCH 1/9] only handle discriminated --- src/Annotation/UnionDiscriminator.php | 26 +++++ .../SerializationGraphNavigator.php | 14 +++ src/Handler/UnionHandler.php | 82 ++++++++++--- .../Driver/AnnotationOrAttributeDriver.php | 3 + src/Metadata/Driver/TypedPropertiesDriver.php | 3 + src/Metadata/PropertyMetadata.php | 20 ++++ src/SerializerBuilder.php | 2 +- tests/Fixtures/DiscriminatedAuthor.php | 40 +++++++ tests/Fixtures/DiscriminatedComment.php | 44 +++++++ tests/Fixtures/MappedDiscriminatedAuthor.php | 40 +++++++ tests/Fixtures/MappedDiscriminatedComment.php | 44 +++++++ tests/Fixtures/MoreSpecificAuthor.php | 41 +++++++ .../ComplexDiscriminatedUnion.php | 25 ++++ .../ComplexUnionTypedProperties.php | 24 ++++ .../MappedComplexDiscriminatedUnion.php | 25 ++++ .../Serializer/BaseSerializationTestCase.php | 2 +- tests/Serializer/JsonSerializationTest.php | 110 +++++++++++++++++- 17 files changed, 528 insertions(+), 17 deletions(-) create mode 100644 src/Annotation/UnionDiscriminator.php create mode 100644 tests/Fixtures/DiscriminatedAuthor.php create mode 100644 tests/Fixtures/DiscriminatedComment.php create mode 100644 tests/Fixtures/MappedDiscriminatedAuthor.php create mode 100644 tests/Fixtures/MappedDiscriminatedComment.php create mode 100644 tests/Fixtures/MoreSpecificAuthor.php create mode 100644 tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php create mode 100644 tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php create mode 100644 tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php diff --git a/src/Annotation/UnionDiscriminator.php b/src/Annotation/UnionDiscriminator.php new file mode 100644 index 000000000..ddaa9ba97 --- /dev/null +++ b/src/Annotation/UnionDiscriminator.php @@ -0,0 +1,26 @@ + */ + public $map = []; + + /** @var string */ + public $field = 'type'; + + public function __construct(array $values = [], string $field = 'type', array $map = []) + { + $this->loadAnnotationParameters(get_defined_vars()); + } +} diff --git a/src/GraphNavigator/SerializationGraphNavigator.php b/src/GraphNavigator/SerializationGraphNavigator.php index 1de6e3bb7..964119d77 100644 --- a/src/GraphNavigator/SerializationGraphNavigator.php +++ b/src/GraphNavigator/SerializationGraphNavigator.php @@ -177,6 +177,20 @@ public function accept($data, ?array $type = null) throw new RuntimeException($msg); + case 'union': + if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) { + try { + return \call_user_func($handler, $this->visitor, $data, $type, $this->context); + } catch (SkipHandlerException $e) { + // Skip handler, fallback to default behavior + } catch (NotAcceptableException $e) { + $this->context->stopVisiting($data); + + throw $e; + } + } + + break; default: if (null !== $data) { if ($this->context->isVisiting($data)) { diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index ad7f4339a..5772e437d 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -47,22 +47,25 @@ public function serializeUnion( mixed $data, array $type, SerializationContext $context - ) { - return $this->matchSimpleType($data, $type, $context); + ): mixed { + if ($this->isPrimitiveType(gettype($data))) { + return $this->matchSimpleType($data, $type, $context); + } else { + $resolvedType = [ + 'name' => get_class($data), + 'params' => [], + ]; + + return $context->getNavigator()->accept($data, $resolvedType); + } } - public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed $data, array $type, DeserializationContext $context) + public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed $data, array $type, DeserializationContext $context): mixed { if ($data instanceof \SimpleXMLElement) { throw new RuntimeException('XML deserialisation into union types is not supported yet.'); } - return $this->matchSimpleType($data, $type, $context); - } - - private function matchSimpleType(mixed $data, array $type, Context $context) - { - $dataType = $this->determineType($data, $type, $context->getFormat()); $alternativeName = null; if (isset(static::$aliases[$dataType])) { @@ -70,23 +73,74 @@ private function matchSimpleType(mixed $data, array $type, Context $context) } foreach ($type['params'] as $possibleType) { - if ($possibleType['name'] === $dataType || $possibleType['name'] === $alternativeName) { - return $context->getNavigator()->accept($data, $possibleType); + $finalType = null; + + if (!$context->getMetadataStack()->isEmpty()) { + $propertyMetadata = $context->getMetadataStack()->top(); + if (null !== $propertyMetadata->unionDiscriminatorField) { + if (!array_key_exists($propertyMetadata->unionDiscriminatorField, $data)) { + throw new NonVisitableTypeException('Union Discriminator Field \'' . $propertyMetadata->unionDiscriminatorField . '\' not found in data'); + } + + $lkup = $data[$propertyMetadata->unionDiscriminatorField]; + if (!empty($propertyMetadata->unionDiscriminatorMap)) { + if (array_key_exists($lkup, $propertyMetadata->unionDiscriminatorMap)) { + $finalType = [ + 'name' => $propertyMetadata->unionDiscriminatorMap[$lkup], + 'params' => [], + ]; + } else { + throw new NonVisitableTypeException('Union Discriminator Map does not contain key \'' . $lkup . '\''); + } + } else { + $finalType = [ + 'name' => $lkup, + 'params' => [], + ]; + } + } + } + + if (null !== $finalType && null !== $finalType['name']) { + return $context->getNavigator()->accept($data, $finalType); + } else { + foreach ($type['params'] as $possibleType) { + if ($possibleType['name'] === $dataType || $possibleType['name'] === $alternativeName) { + return $context->getNavigator()->accept($data, $possibleType); + } + } } } + + return null; } - private function determineType(mixed $data, array $type, string $format): ?string + private function matchSimpleType(mixed $data, array $type, Context $context): mixed { + $alternativeName = null; + foreach ($type['params'] as $possibleType) { - if ($this->testPrimitive($data, $possibleType['name'], $format)) { - return $possibleType['name']; + if ($this->isPrimitiveType($possibleType['name']) && !$this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { + continue; + } + + try { + return $context->getNavigator()->accept($data, $possibleType); + } catch (NonVisitableTypeException $e) { + continue; + } catch (PropertyMissingException $e) { + continue; } } return null; } + private function isPrimitiveType(string $type): bool + { + return in_array($type, ['int', 'integer', 'float', 'double', 'bool', 'boolean', 'string']); + } + private function testPrimitive(mixed $data, string $type, string $format): bool { switch ($type) { diff --git a/src/Metadata/Driver/AnnotationOrAttributeDriver.php b/src/Metadata/Driver/AnnotationOrAttributeDriver.php index 92becb724..61589aadf 100644 --- a/src/Metadata/Driver/AnnotationOrAttributeDriver.php +++ b/src/Metadata/Driver/AnnotationOrAttributeDriver.php @@ -24,6 +24,7 @@ use JMS\Serializer\Annotation\Since; use JMS\Serializer\Annotation\SkipWhenEmpty; use JMS\Serializer\Annotation\Type; +use JMS\Serializer\Annotation\UnionDiscriminator; use JMS\Serializer\Annotation\Until; use JMS\Serializer\Annotation\VirtualProperty; use JMS\Serializer\Annotation\XmlAttribute; @@ -258,6 +259,8 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat $propertyMetadata->xmlAttributeMap = true; } elseif ($annot instanceof MaxDepth) { $propertyMetadata->maxDepth = $annot->depth; + } elseif ($annot instanceof UnionDiscriminator) { + $propertyMetadata->setUnionDiscriminator($annot->field, $annot->map); } } diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php index 99f93cbaa..6227c851d 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -102,6 +102,9 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata foreach ($classMetadata->propertyMetadata as $propertyMetadata) { // If the inner driver provides a type, don't guess anymore. if ($propertyMetadata->type) { + if ('union' === $propertyMetadata->type['name']) { + $propertyMetadata->setType($this->reorderTypes($propertyMetadata->type)); + } continue; } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index ff41c949f..e52c4de49 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -33,6 +33,16 @@ class PropertyMetadata extends BasePropertyMetadata */ public $serializedName; + /** + * @var string|null + */ + public $unionDiscriminatorField; + + /** + * @var array|null + */ + public $unionDiscriminatorMap; + /** * @var array|null */ @@ -196,6 +206,12 @@ public function setAccessor(string $type, ?string $getter = null, ?string $sette $this->setter = $setter; } + public function setUnionDiscriminator(string $field, ?array $map): void + { + $this->unionDiscriminatorField = $field; + $this->unionDiscriminatorMap = $map; + } + public function setType(array $type): void { $this->type = $type; @@ -224,6 +240,8 @@ protected function serializeToArray(): array $this->untilVersion, $this->groups, $this->serializedName, + $this->unionDiscriminatorField, + $this->unionDiscriminatorMap, $this->type, $this->xmlCollection, $this->xmlCollectionInline, @@ -258,6 +276,8 @@ protected function unserializeFromArray(array $data): void $this->untilVersion, $this->groups, $this->serializedName, + $this->unionDiscriminatorField, + $this->unionDiscriminatorMap, $this->type, $this->xmlCollection, $this->xmlCollectionInline, diff --git a/src/SerializerBuilder.php b/src/SerializerBuilder.php index f5d9c5404..ed40ac728 100644 --- a/src/SerializerBuilder.php +++ b/src/SerializerBuilder.php @@ -285,7 +285,7 @@ public function addDefaultHandlers(): self } if (PHP_VERSION_ID >= 80000) { - $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); + $this->handlerRegistry->registerSubscribingHandler(new UnionHandler(true)); } return $this; diff --git a/tests/Fixtures/DiscriminatedAuthor.php b/tests/Fixtures/DiscriminatedAuthor.php new file mode 100644 index 000000000..d3d13d4b3 --- /dev/null +++ b/tests/Fixtures/DiscriminatedAuthor.php @@ -0,0 +1,40 @@ +name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getType() + { + return $this->type; + } +} diff --git a/tests/Fixtures/DiscriminatedComment.php b/tests/Fixtures/DiscriminatedComment.php new file mode 100644 index 000000000..29f1cd52a --- /dev/null +++ b/tests/Fixtures/DiscriminatedComment.php @@ -0,0 +1,44 @@ +author = $author; + $this->text = $text; + } + + public function getAuthor() + { + return $this->author; + } + + public function getType() + { + return $this->type; + } +} diff --git a/tests/Fixtures/MappedDiscriminatedAuthor.php b/tests/Fixtures/MappedDiscriminatedAuthor.php new file mode 100644 index 000000000..30bef937f --- /dev/null +++ b/tests/Fixtures/MappedDiscriminatedAuthor.php @@ -0,0 +1,40 @@ +name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getObjectType() + { + return $this->objectType; + } +} diff --git a/tests/Fixtures/MappedDiscriminatedComment.php b/tests/Fixtures/MappedDiscriminatedComment.php new file mode 100644 index 000000000..3eef5b30d --- /dev/null +++ b/tests/Fixtures/MappedDiscriminatedComment.php @@ -0,0 +1,44 @@ +author = $author; + $this->text = $text; + } + + public function getAuthor() + { + return $this->author; + } + + public function getObjectType() + { + return $this->objectType; + } +} diff --git a/tests/Fixtures/MoreSpecificAuthor.php b/tests/Fixtures/MoreSpecificAuthor.php new file mode 100644 index 000000000..c48974712 --- /dev/null +++ b/tests/Fixtures/MoreSpecificAuthor.php @@ -0,0 +1,41 @@ +name = $name; + $this->isMoreSpecific = $isMoreSpecific; + } + + public function getName() + { + return $this->name; + } + + public function getIsMoreSpecific() + { + return $this->isMoreSpecific; + } +} diff --git a/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php b/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php new file mode 100644 index 000000000..34cf0514c --- /dev/null +++ b/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php @@ -0,0 +1,25 @@ +data = $data; + } + + public function getData(): DiscriminatedAuthor|DiscriminatedComment + { + return $this->data; + } +} diff --git a/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php b/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php new file mode 100644 index 000000000..e5061d256 --- /dev/null +++ b/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php @@ -0,0 +1,24 @@ +data = $data; + } + + public function getData(): Author|Comment|MoreSpecificAuthor + { + return $this->data; + } +} diff --git a/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php b/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php new file mode 100644 index 000000000..117d7d360 --- /dev/null +++ b/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php @@ -0,0 +1,25 @@ + 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor', 'comment' => 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment'])] + private MappedDiscriminatedAuthor|MappedDiscriminatedComment $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData(): MappedDiscriminatedAuthor|MappedDiscriminatedComment + { + return $this->data; + } +} diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 52626e14e..5c14706e5 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -2118,7 +2118,7 @@ protected function setUp(): void $this->handlerRegistry->registerSubscribingHandler(new SymfonyUidHandler()); $this->handlerRegistry->registerSubscribingHandler(new EnumHandler()); if (PHP_VERSION_ID >= 80000) { - $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); + $this->handlerRegistry->registerSubscribingHandler(new UnionHandler(true)); } $this->handlerRegistry->registerHandler( diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 25cb0cf17..24f63e7a4 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -14,11 +14,20 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\Tests\Fixtures\Author; use JMS\Serializer\Tests\Fixtures\AuthorList; +use JMS\Serializer\Tests\Fixtures\Comment; +use JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor; +use JMS\Serializer\Tests\Fixtures\DiscriminatedComment; use JMS\Serializer\Tests\Fixtures\FirstClassMapCollection; +use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor; +use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment; +use JMS\Serializer\Tests\Fixtures\MoreSpecificAuthor; use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyArrayAndHash; use JMS\Serializer\Tests\Fixtures\ObjectWithInlineArray; use JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty; use JMS\Serializer\Tests\Fixtures\Tag; +use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexDiscriminatedUnion; +use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexUnionTypedProperties; +use JMS\Serializer\Tests\Fixtures\TypedProperties\MappedComplexDiscriminatedUnion; use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; use JMS\Serializer\Visitor\SerializationVisitorInterface; @@ -147,6 +156,13 @@ protected static function getContent($key) $outputs['data_float'] = '{"data":1.236}'; $outputs['data_bool'] = '{"data":false}'; $outputs['data_string'] = '{"data":"foo"}'; + $outputs['data_author'] = '{"data":{"full_name":"foo"}}'; + $outputs['data_more_specific_author'] = '{"data":{"full_name":"foo","is_more_specific":true}}'; + $outputs['data_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar"}}'; + $outputs['data_discriminated_author'] = '{"data":{"full_name":"foo","type":"JMS\\\Serializer\\\Tests\\\Fixtures\\\DiscriminatedAuthor"}}'; + $outputs['data_discriminated_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar","type":"JMS\\\Serializer\\\Tests\\\Fixtures\\\DiscriminatedComment"}}'; + $outputs['data_mapped_discriminated_author'] = '{"data":{"full_name":"foo","objectType":"author"}}'; + $outputs['data_mapped_discriminated_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar","objectType":"comment"}}'; $outputs['uid'] = '"66b3177c-e03b-4a22-9dee-ddd7d37a04d5"'; $outputs['object_with_enums'] = '{"ordinary":"Clubs","backed_value":"C","backed_without_param":"C","ordinary_array":["Clubs","Spades"],"backed_array":["C","H"],"backed_array_without_param":["C","H"],"ordinary_auto_detect":"Clubs","backed_auto_detect":"C","backed_int_auto_detect":3,"backed_int":3,"backed_name":"C","backed_int_forced_str":3}'; $outputs['object_with_autodetect_enums'] = '{"ordinary_array_auto_detect":["Clubs","Spades"],"backed_array_auto_detect":["C","H"],"mixed_array_auto_detect":["Clubs","H"]}'; @@ -449,7 +465,7 @@ public function testDeserializingUnionProperties() self::assertEquals($object, $this->deserialize(static::getContent('data_string'), UnionTypedProperties::class)); } - public function testSerializeUnionProperties() + public function testSerializingUnionProperties() { if (PHP_VERSION_ID < 80000) { $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); @@ -459,6 +475,98 @@ public function testSerializeUnionProperties() $serialized = $this->serialize(new UnionTypedProperties(10000)); self::assertEquals(static::getContent('data_integer'), $serialized); + + $serialized = $this->serialize(new UnionTypedProperties(1.236)); + self::assertEquals(static::getContent('data_float'), $serialized); + + $serialized = $this->serialize(new UnionTypedProperties(false)); + self::assertEquals(static::getContent('data_bool'), $serialized); + + $serialized = $this->serialize(new UnionTypedProperties('foo')); + self::assertEquals(static::getContent('data_string'), $serialized); + } + + public function testDeserializingNonDiscriminatedComplexUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $authorUnion = new ComplexUnionTypedProperties(new Author('foo')); + self::assertEquals($authorUnion, $this->deserialize(static::getContent('data_author'), ComplexUnionTypedProperties::class)); + + $commentUnion = new ComplexUnionTypedProperties(new Comment(new Author('foo'), 'bar')); + self::assertEquals($commentUnion, $this->deserialize(static::getContent('data_comment'), ComplexUnionTypedProperties::class)); + + $moreSpecificAuthor = new ComplexUnionTypedProperties(new MoreSpecificAuthor('foo', true)); + self::assertEquals($moreSpecificAuthor, $this->deserialize(static::getContent('data_more_specific_author'), ComplexUnionTypedProperties::class)); + } + + public function testSerializingNonDiscriminatedComplexUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $serialized = $this->serialize(new ComplexUnionTypedProperties(new Author('foo'))); + self::assertEquals(static::getContent('data_author'), $serialized); + + $serialized = $this->serialize(new ComplexUnionTypedProperties(new Comment(new Author('foo'), 'bar'))); + self::assertEquals(static::getContent('data_comment'), $serialized); + + $serialized = $this->serialize(new ComplexUnionTypedProperties(new MoreSpecificAuthor('foo', true))); + self::assertEquals(static::getContent('data_more_specific_author'), $serialized); + } + + public function testDeserializingComplexDiscriminatedUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $authorUnion = new ComplexDiscriminatedUnion(new DiscriminatedAuthor('foo')); + self::assertEquals($authorUnion, $this->deserialize(static::getContent('data_discriminated_author'), ComplexDiscriminatedUnion::class)); + + $commentUnion = new ComplexDiscriminatedUnion(new DiscriminatedComment(new Author('foo'), 'bar')); + + self::assertEquals($commentUnion, $this->deserialize(static::getContent('data_discriminated_comment'), ComplexDiscriminatedUnion::class)); + } + + public function testDeserializingMappedComplexDiscriminatedUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $authorUnion = new MappedComplexDiscriminatedUnion(new MappedDiscriminatedAuthor('foo')); + self::assertEquals($authorUnion, $this->deserialize(static::getContent('data_mapped_discriminated_author'), MappedComplexDiscriminatedUnion::class)); + + $commentUnion = new MappedComplexDiscriminatedUnion(new MappedDiscriminatedComment(new Author('foo'), 'bar')); + + self::assertEquals($commentUnion, $this->deserialize(static::getContent('data_mapped_discriminated_comment'), MappedComplexDiscriminatedUnion::class)); + } + + public function testSerializeingComplexDiscriminatedUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $serialized = $this->serialize(new ComplexDiscriminatedUnion(new DiscriminatedAuthor('foo'))); + self::assertEquals(static::getContent('data_discriminated_author'), $serialized); + + $serialized = $this->serialize(new ComplexDiscriminatedUnion(new DiscriminatedComment(new Author('foo'), 'bar'))); + self::assertEquals(static::getContent('data_discriminated_comment'), $serialized); } /** From 3466c01aaf7682f0d1b498e13599776cf0d64526 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 10:56:45 -0600 Subject: [PATCH 2/9] tests --- .../SerializationGraphNavigator.php | 4 --- src/Handler/UnionHandler.php | 5 +-- tests/Serializer/JsonSerializationTest.php | 36 ------------------- 3 files changed, 1 insertion(+), 44 deletions(-) diff --git a/src/GraphNavigator/SerializationGraphNavigator.php b/src/GraphNavigator/SerializationGraphNavigator.php index 964119d77..52d1543da 100644 --- a/src/GraphNavigator/SerializationGraphNavigator.php +++ b/src/GraphNavigator/SerializationGraphNavigator.php @@ -183,10 +183,6 @@ public function accept($data, ?array $type = null) return \call_user_func($handler, $this->visitor, $data, $type, $this->context); } catch (SkipHandlerException $e) { // Skip handler, fallback to default behavior - } catch (NotAcceptableException $e) { - $this->context->stopVisiting($data); - - throw $e; } } diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index 5772e437d..ea4266d67 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -105,7 +105,7 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed return $context->getNavigator()->accept($data, $finalType); } else { foreach ($type['params'] as $possibleType) { - if ($possibleType['name'] === $dataType || $possibleType['name'] === $alternativeName) { + if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { return $context->getNavigator()->accept($data, $possibleType); } } @@ -128,14 +128,11 @@ private function matchSimpleType(mixed $data, array $type, Context $context): mi return $context->getNavigator()->accept($data, $possibleType); } catch (NonVisitableTypeException $e) { continue; - } catch (PropertyMissingException $e) { - continue; } } return null; } - private function isPrimitiveType(string $type): bool { return in_array($type, ['int', 'integer', 'float', 'double', 'bool', 'boolean', 'string']); diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 24f63e7a4..e7dba60a7 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -486,42 +486,6 @@ public function testSerializingUnionProperties() self::assertEquals(static::getContent('data_string'), $serialized); } - public function testDeserializingNonDiscriminatedComplexUnionProperties() - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); - - return; - } - - $authorUnion = new ComplexUnionTypedProperties(new Author('foo')); - self::assertEquals($authorUnion, $this->deserialize(static::getContent('data_author'), ComplexUnionTypedProperties::class)); - - $commentUnion = new ComplexUnionTypedProperties(new Comment(new Author('foo'), 'bar')); - self::assertEquals($commentUnion, $this->deserialize(static::getContent('data_comment'), ComplexUnionTypedProperties::class)); - - $moreSpecificAuthor = new ComplexUnionTypedProperties(new MoreSpecificAuthor('foo', true)); - self::assertEquals($moreSpecificAuthor, $this->deserialize(static::getContent('data_more_specific_author'), ComplexUnionTypedProperties::class)); - } - - public function testSerializingNonDiscriminatedComplexUnionProperties() - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); - - return; - } - - $serialized = $this->serialize(new ComplexUnionTypedProperties(new Author('foo'))); - self::assertEquals(static::getContent('data_author'), $serialized); - - $serialized = $this->serialize(new ComplexUnionTypedProperties(new Comment(new Author('foo'), 'bar'))); - self::assertEquals(static::getContent('data_comment'), $serialized); - - $serialized = $this->serialize(new ComplexUnionTypedProperties(new MoreSpecificAuthor('foo', true))); - self::assertEquals(static::getContent('data_more_specific_author'), $serialized); - } - public function testDeserializingComplexDiscriminatedUnionProperties() { if (PHP_VERSION_ID < 80000) { From 411c72c8de3bd4e2b02338715c866730be3b8306 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 11:53:02 -0600 Subject: [PATCH 3/9] iter --- src/SerializerBuilder.php | 2 +- .../ComplexUnionTypedProperties.php | 24 ------------------- .../Serializer/BaseSerializationTestCase.php | 2 +- tests/Serializer/JsonSerializationTest.php | 1 - 4 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php diff --git a/src/SerializerBuilder.php b/src/SerializerBuilder.php index ed40ac728..f5d9c5404 100644 --- a/src/SerializerBuilder.php +++ b/src/SerializerBuilder.php @@ -285,7 +285,7 @@ public function addDefaultHandlers(): self } if (PHP_VERSION_ID >= 80000) { - $this->handlerRegistry->registerSubscribingHandler(new UnionHandler(true)); + $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); } return $this; diff --git a/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php b/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php deleted file mode 100644 index e5061d256..000000000 --- a/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php +++ /dev/null @@ -1,24 +0,0 @@ -data = $data; - } - - public function getData(): Author|Comment|MoreSpecificAuthor - { - return $this->data; - } -} diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 5c14706e5..52626e14e 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -2118,7 +2118,7 @@ protected function setUp(): void $this->handlerRegistry->registerSubscribingHandler(new SymfonyUidHandler()); $this->handlerRegistry->registerSubscribingHandler(new EnumHandler()); if (PHP_VERSION_ID >= 80000) { - $this->handlerRegistry->registerSubscribingHandler(new UnionHandler(true)); + $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); } $this->handlerRegistry->registerHandler( diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index e7dba60a7..a465d3e6a 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -157,7 +157,6 @@ protected static function getContent($key) $outputs['data_bool'] = '{"data":false}'; $outputs['data_string'] = '{"data":"foo"}'; $outputs['data_author'] = '{"data":{"full_name":"foo"}}'; - $outputs['data_more_specific_author'] = '{"data":{"full_name":"foo","is_more_specific":true}}'; $outputs['data_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar"}}'; $outputs['data_discriminated_author'] = '{"data":{"full_name":"foo","type":"JMS\\\Serializer\\\Tests\\\Fixtures\\\DiscriminatedAuthor"}}'; $outputs['data_discriminated_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar","type":"JMS\\\Serializer\\\Tests\\\Fixtures\\\DiscriminatedComment"}}'; From 687d62c0fb69fadea11f241bc7b99fba319ffd07 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 11:56:05 -0600 Subject: [PATCH 4/9] iter --- src/Handler/UnionHandler.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index ea4266d67..3e170aedb 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -11,6 +11,7 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\Visitor\DeserializationVisitorInterface; use JMS\Serializer\Visitor\SerializationVisitorInterface; +use JMS\Serializer\Exception\NonVisitableTypeException; final class UnionHandler implements SubscribingHandlerInterface { @@ -66,12 +67,6 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed throw new RuntimeException('XML deserialisation into union types is not supported yet.'); } - $alternativeName = null; - - if (isset(static::$aliases[$dataType])) { - $alternativeName = static::$aliases[$dataType]; - } - foreach ($type['params'] as $possibleType) { $finalType = null; @@ -117,8 +112,6 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed private function matchSimpleType(mixed $data, array $type, Context $context): mixed { - $alternativeName = null; - foreach ($type['params'] as $possibleType) { if ($this->isPrimitiveType($possibleType['name']) && !$this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { continue; From 9532fb6ac9d8ef26b6211a726e0d791b35f2b42d Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 11:56:40 -0600 Subject: [PATCH 5/9] style --- src/Handler/UnionHandler.php | 3 ++- src/Metadata/Driver/TypedPropertiesDriver.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index 3e170aedb..f65c8a81a 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -6,12 +6,12 @@ use JMS\Serializer\Context; use JMS\Serializer\DeserializationContext; +use JMS\Serializer\Exception\NonVisitableTypeException; use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\SerializationContext; use JMS\Serializer\Visitor\DeserializationVisitorInterface; use JMS\Serializer\Visitor\SerializationVisitorInterface; -use JMS\Serializer\Exception\NonVisitableTypeException; final class UnionHandler implements SubscribingHandlerInterface { @@ -126,6 +126,7 @@ private function matchSimpleType(mixed $data, array $type, Context $context): mi return null; } + private function isPrimitiveType(string $type): bool { return in_array($type, ['int', 'integer', 'float', 'double', 'bool', 'boolean', 'string']); diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php index 6227c851d..47741eb34 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -105,6 +105,7 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata if ('union' === $propertyMetadata->type['name']) { $propertyMetadata->setType($this->reorderTypes($propertyMetadata->type)); } + continue; } From b5cc1ce37b15ad6ed3360346f9dbc21065f0dc12 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Fri, 26 Jul 2024 12:05:40 -0600 Subject: [PATCH 6/9] unusued imports --- tests/Serializer/JsonSerializationTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index a465d3e6a..2650276a7 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -14,19 +14,16 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\Tests\Fixtures\Author; use JMS\Serializer\Tests\Fixtures\AuthorList; -use JMS\Serializer\Tests\Fixtures\Comment; use JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor; use JMS\Serializer\Tests\Fixtures\DiscriminatedComment; use JMS\Serializer\Tests\Fixtures\FirstClassMapCollection; use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor; use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment; -use JMS\Serializer\Tests\Fixtures\MoreSpecificAuthor; use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyArrayAndHash; use JMS\Serializer\Tests\Fixtures\ObjectWithInlineArray; use JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty; use JMS\Serializer\Tests\Fixtures\Tag; use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexDiscriminatedUnion; -use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexUnionTypedProperties; use JMS\Serializer\Tests\Fixtures\TypedProperties\MappedComplexDiscriminatedUnion; use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; From 34439ca9ce20cd1740fc838710a52ceceaa9cbdf Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 12 Aug 2024 12:47:18 -0400 Subject: [PATCH 7/9] PR feedback --- src/Handler/UnionHandler.php | 69 +++++++++++-------- src/Metadata/Driver/TypedPropertiesDriver.php | 45 +++++++----- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index f65c8a81a..26c578363 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -67,42 +67,57 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed throw new RuntimeException('XML deserialisation into union types is not supported yet.'); } - foreach ($type['params'] as $possibleType) { - $finalType = null; + $finalType = null; + if (1 === count($type['params'])) { + if ($data[$type['params'][0]]) { + $lookupField = $type['params'][0]; - if (!$context->getMetadataStack()->isEmpty()) { - $propertyMetadata = $context->getMetadataStack()->top(); - if (null !== $propertyMetadata->unionDiscriminatorField) { - if (!array_key_exists($propertyMetadata->unionDiscriminatorField, $data)) { - throw new NonVisitableTypeException('Union Discriminator Field \'' . $propertyMetadata->unionDiscriminatorField . '\' not found in data'); - } + if (!array_key_exists($lookupField, $data)) { + throw new NonVisitableTypeException('Union Discriminator Field \'' . $lookupField . '\' not found in data'); + } - $lkup = $data[$propertyMetadata->unionDiscriminatorField]; - if (!empty($propertyMetadata->unionDiscriminatorMap)) { - if (array_key_exists($lkup, $propertyMetadata->unionDiscriminatorMap)) { - $finalType = [ - 'name' => $propertyMetadata->unionDiscriminatorMap[$lkup], - 'params' => [], - ]; - } else { - throw new NonVisitableTypeException('Union Discriminator Map does not contain key \'' . $lkup . '\''); - } - } else { + $lkup = $data[$lookupField]; + $finalType = [ + 'name' => $lkup, + 'params' => [], + ]; + } + } elseif (2 === count($type['params'])) { + if (is_array($type['params'][1]) && !array_key_exists('name', $type['params'][1])) { + $lookupField = $type['params'][0]; + $unionMap = $type['params'][1]; + + if (!array_key_exists($lookupField, $data)) { + throw new NonVisitableTypeException('Union Discriminator Field \'' . $lookupField . '\' not found in data'); + } + + $lkup = $data[$lookupField]; + if (!empty($unionMap)) { + if (array_key_exists($lkup, $unionMap)) { $finalType = [ - 'name' => $lkup, + 'name' => $unionMap[$lkup], 'params' => [], ]; + } else { + throw new NonVisitableTypeException('Union Discriminator Map does not contain key \'' . $lkup . '\''); } + } else { + $finalType = [ + 'name' => $lkup, + 'params' => [], + ]; } } + } - if (null !== $finalType && null !== $finalType['name']) { - return $context->getNavigator()->accept($data, $finalType); - } else { - foreach ($type['params'] as $possibleType) { - if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { - return $context->getNavigator()->accept($data, $possibleType); - } + if (null !== $finalType && null !== $finalType['name']) { + return $context->getNavigator()->accept($data, $finalType); + } else { + foreach ($type['params'] as $possibleType) { + $finalType = null; + + if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { + return $context->getNavigator()->accept($data, $possibleType); } } } diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php index 47741eb34..3b2d7d08b 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -103,27 +103,40 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata // If the inner driver provides a type, don't guess anymore. if ($propertyMetadata->type) { if ('union' === $propertyMetadata->type['name']) { + // If the property has a unionDiscriminator annotation, overwrite the types array with the discriminator. $propertyMetadata->setType($this->reorderTypes($propertyMetadata->type)); } - - continue; + } else { + try { + $reflectionType = $this->getReflectionType($propertyMetadata); + + if ($this->shouldTypeHint($reflectionType)) { + $type = $reflectionType->getName(); + + $propertyMetadata->setType($this->typeParser->parse($type)); + } elseif ($this->shouldTypeHintUnion($reflectionType)) { + $propertyMetadata->setType($this->reorderTypes([ + 'name' => 'union', + 'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->getTypes()), + ])); + } + } catch (ReflectionException $e) { + continue; + } } - try { - $reflectionType = $this->getReflectionType($propertyMetadata); - - if ($this->shouldTypeHint($reflectionType)) { - $type = $reflectionType->getName(); - - $propertyMetadata->setType($this->typeParser->parse($type)); - } elseif ($this->shouldTypeHintUnion($reflectionType)) { - $propertyMetadata->setType($this->reorderTypes([ - 'name' => 'union', - 'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->getTypes()), - ])); + // Update the type if a unionDiscriminator annotation is present. + $params = []; + if ($propertyMetadata->unionDiscriminatorField) { + $params[] = $propertyMetadata->unionDiscriminatorField; + if ($propertyMetadata->unionDiscriminatorMap) { + $params[] = $propertyMetadata->unionDiscriminatorMap; } - } catch (ReflectionException $e) { - continue; + + $propertyMetadata->setType([ + 'name' => 'union', + 'params' => $params, + ]); } } From dbd19ba73c89730531ae3416485a8d88269f51d2 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 12 Aug 2024 14:41:12 -0400 Subject: [PATCH 8/9] add docs for UnionDiscriminator --- doc/reference/annotations.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/doc/reference/annotations.rst b/doc/reference/annotations.rst index 180120708..d6cc0255c 100644 --- a/doc/reference/annotations.rst +++ b/doc/reference/annotations.rst @@ -9,6 +9,8 @@ JMS serializer now supports PHP 8 attributes, with a few caveats: - There is an edge case when setting this exact serialization group ``#[Groups(['value' => 'any value here'])]``. (when there is only one item in th serialization groups array and has as key ``value`` the attribute will not work as expected, please use the alternative syntax ``#[Groups(groups: ['value' => 'any value here'])]`` that works with no issues), +- Some support for unions exists. For unions of primitive types, the system will try to resolve them automatically. For +classes that contain union attributes, the ``#[UnionDiscriminator]`` attribute must be used to specify the type of the union. Converting your annotations to attributes ----------------------------------------- @@ -384,6 +386,31 @@ to the least super type: `groups` is optional and is used as exclusion policy. +#[UnionDiscriminator] + +This attribute allows deserialization of unions. The ``#[UnionDiscriminator]`` attribute has to be applied +to an attribute that can be one of many types. + +.. code-block :: php + + class Vehicle { + #[UnionDiscriminator(field: 'typeDiscriminator')] + private Manual|Automatic $transmission; + } + +In the case of this example, both Manual and Automatic should contain a string attribute named `typeDiscriminator`. If the `typeDiscriminator` field +will always contain the fully qualified clasname, then the appropriate type will be selected for deserialization. + +If, however, the field contains a string that is not the fully qualified classname, then the `map` option can be used to map the +string to the appropriate class. + +.. code-block :: php + + class Vehicle { + #[UnionDiscriminator(field: 'type', map: ['manual' => 'FullyQualified/Path/Manual', 'automatic' => 'FullyQualified/Path/Automatic'])] + private Manual|Automatic $transmission; + } + #[Type] ~~~~~~~ This attribute can be defined on a property to specify the type of that property. From 3f4f69f11f10d8d9649ca7fdb8805c223ac3db3b Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Tue, 13 Aug 2024 10:50:39 -0400 Subject: [PATCH 9/9] require map and refactor where the union setType occurs --- doc/reference/annotations.rst | 17 ++----- src/Handler/UnionHandler.php | 18 +------- .../Driver/AnnotationOrAttributeDriver.php | 4 ++ src/Metadata/Driver/TypedPropertiesDriver.php | 21 +++------ src/Metadata/PropertyMetadata.php | 2 +- tests/Fixtures/DiscriminatedAuthor.php | 8 ++-- tests/Fixtures/DiscriminatedComment.php | 9 ++-- tests/Fixtures/MappedDiscriminatedAuthor.php | 40 ----------------- tests/Fixtures/MappedDiscriminatedComment.php | 44 ------------------- .../ComplexDiscriminatedUnion.php | 2 +- .../MappedComplexDiscriminatedUnion.php | 25 ----------- tests/Serializer/JsonSerializationTest.php | 25 +---------- 12 files changed, 30 insertions(+), 185 deletions(-) delete mode 100644 tests/Fixtures/MappedDiscriminatedAuthor.php delete mode 100644 tests/Fixtures/MappedDiscriminatedComment.php delete mode 100644 tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php diff --git a/doc/reference/annotations.rst b/doc/reference/annotations.rst index d6cc0255c..a6728af27 100644 --- a/doc/reference/annotations.rst +++ b/doc/reference/annotations.rst @@ -387,6 +387,7 @@ to the least super type: `groups` is optional and is used as exclusion policy. #[UnionDiscriminator] +~~~~~~~~~~~~~~~~~~~~~ This attribute allows deserialization of unions. The ``#[UnionDiscriminator]`` attribute has to be applied to an attribute that can be one of many types. @@ -394,22 +395,12 @@ to an attribute that can be one of many types. .. code-block :: php class Vehicle { - #[UnionDiscriminator(field: 'typeDiscriminator')] + #[UnionDiscriminator(field: 'typeField', map: ['manual' => 'FullyQualified/Path/Manual', 'automatic' => 'FullyQualified/Path/Automatic'])] private Manual|Automatic $transmission; } -In the case of this example, both Manual and Automatic should contain a string attribute named `typeDiscriminator`. If the `typeDiscriminator` field -will always contain the fully qualified clasname, then the appropriate type will be selected for deserialization. - -If, however, the field contains a string that is not the fully qualified classname, then the `map` option can be used to map the -string to the appropriate class. - -.. code-block :: php - - class Vehicle { - #[UnionDiscriminator(field: 'type', map: ['manual' => 'FullyQualified/Path/Manual', 'automatic' => 'FullyQualified/Path/Automatic'])] - private Manual|Automatic $transmission; - } +In the case of this example, both Manual and Automatic should contain a string attribute named `typeField`. The value of that field will be passed +to the `map` option to determine which class to instantiate. #[Type] ~~~~~~~ diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index 26c578363..086a3548f 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -68,22 +68,8 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed } $finalType = null; - if (1 === count($type['params'])) { - if ($data[$type['params'][0]]) { - $lookupField = $type['params'][0]; - - if (!array_key_exists($lookupField, $data)) { - throw new NonVisitableTypeException('Union Discriminator Field \'' . $lookupField . '\' not found in data'); - } - - $lkup = $data[$lookupField]; - $finalType = [ - 'name' => $lkup, - 'params' => [], - ]; - } - } elseif (2 === count($type['params'])) { - if (is_array($type['params'][1]) && !array_key_exists('name', $type['params'][1])) { + if (2 === count($type['params'])) { + if (!is_array($type['params'][0]) || !array_key_exists('name', $type['params'][0])) { $lookupField = $type['params'][0]; $unionMap = $type['params'][1]; diff --git a/src/Metadata/Driver/AnnotationOrAttributeDriver.php b/src/Metadata/Driver/AnnotationOrAttributeDriver.php index 61589aadf..e477fbf00 100644 --- a/src/Metadata/Driver/AnnotationOrAttributeDriver.php +++ b/src/Metadata/Driver/AnnotationOrAttributeDriver.php @@ -261,6 +261,10 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat $propertyMetadata->maxDepth = $annot->depth; } elseif ($annot instanceof UnionDiscriminator) { $propertyMetadata->setUnionDiscriminator($annot->field, $annot->map); + $propertyMetadata->setType([ + 'name' => 'union', + 'params' => [$annot->field, $annot->map], + ]); } } diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php index 3b2d7d08b..560069c4c 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -103,8 +103,11 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata // If the inner driver provides a type, don't guess anymore. if ($propertyMetadata->type) { if ('union' === $propertyMetadata->type['name']) { - // If the property has a unionDiscriminator annotation, overwrite the types array with the discriminator. - $propertyMetadata->setType($this->reorderTypes($propertyMetadata->type)); + // If this union is discriminated, the params will not contain types. + // If the union isn't discriminated, the params will contain types, and we should reorder them + if (is_array($propertyMetadata->type['params'][0]) && $propertyMetadata->type['params'][0]['name']) { + $propertyMetadata->setType($this->reorderTypes($propertyMetadata->type)); + } } } else { try { @@ -124,20 +127,6 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata continue; } } - - // Update the type if a unionDiscriminator annotation is present. - $params = []; - if ($propertyMetadata->unionDiscriminatorField) { - $params[] = $propertyMetadata->unionDiscriminatorField; - if ($propertyMetadata->unionDiscriminatorMap) { - $params[] = $propertyMetadata->unionDiscriminatorMap; - } - - $propertyMetadata->setType([ - 'name' => 'union', - 'params' => $params, - ]); - } } return $classMetadata; diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index e52c4de49..ccd096a58 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -206,7 +206,7 @@ public function setAccessor(string $type, ?string $getter = null, ?string $sette $this->setter = $setter; } - public function setUnionDiscriminator(string $field, ?array $map): void + public function setUnionDiscriminator(string $field, array $map): void { $this->unionDiscriminatorField = $field; $this->unionDiscriminatorMap = $map; diff --git a/tests/Fixtures/DiscriminatedAuthor.php b/tests/Fixtures/DiscriminatedAuthor.php index d3d13d4b3..f646362f8 100644 --- a/tests/Fixtures/DiscriminatedAuthor.php +++ b/tests/Fixtures/DiscriminatedAuthor.php @@ -19,9 +19,11 @@ class DiscriminatedAuthor /** * @Type("string") + * @SerializedName("objectType") */ #[Type(name: 'string')] - private $type = 'JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor'; + #[SerializedName(name: 'objectType')] + private $objectType = 'author'; public function __construct($name) { @@ -33,8 +35,8 @@ public function getName() return $this->name; } - public function getType() + public function getObjectType() { - return $this->type; + return $this->objectType; } } diff --git a/tests/Fixtures/DiscriminatedComment.php b/tests/Fixtures/DiscriminatedComment.php index 29f1cd52a..e3251fc94 100644 --- a/tests/Fixtures/DiscriminatedComment.php +++ b/tests/Fixtures/DiscriminatedComment.php @@ -5,6 +5,7 @@ namespace JMS\Serializer\Tests\Fixtures; use JMS\Serializer\Annotation\Type; +use JMS\Serializer\Annotation\SerializedName; class DiscriminatedComment { @@ -22,9 +23,11 @@ class DiscriminatedComment /** * @Type("string") + * @SerializedName("objectType") */ #[Type(name: 'string')] - private $type = 'JMS\Serializer\Tests\Fixtures\DiscriminatedComment'; + #[SerializedName(name: 'objectType')] + private $objectType = 'comment'; public function __construct(?Author $author, $text) { @@ -37,8 +40,8 @@ public function getAuthor() return $this->author; } - public function getType() + public function getObjectType() { - return $this->type; + return $this->objectType; } } diff --git a/tests/Fixtures/MappedDiscriminatedAuthor.php b/tests/Fixtures/MappedDiscriminatedAuthor.php deleted file mode 100644 index 30bef937f..000000000 --- a/tests/Fixtures/MappedDiscriminatedAuthor.php +++ /dev/null @@ -1,40 +0,0 @@ -name = $name; - } - - public function getName() - { - return $this->name; - } - - public function getObjectType() - { - return $this->objectType; - } -} diff --git a/tests/Fixtures/MappedDiscriminatedComment.php b/tests/Fixtures/MappedDiscriminatedComment.php deleted file mode 100644 index 3eef5b30d..000000000 --- a/tests/Fixtures/MappedDiscriminatedComment.php +++ /dev/null @@ -1,44 +0,0 @@ -author = $author; - $this->text = $text; - } - - public function getAuthor() - { - return $this->author; - } - - public function getObjectType() - { - return $this->objectType; - } -} diff --git a/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php b/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php index 34cf0514c..06994780b 100644 --- a/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php +++ b/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php @@ -10,7 +10,7 @@ class ComplexDiscriminatedUnion { - #[UnionDiscriminator(field: 'type')] + #[UnionDiscriminator(field: 'objectType', map: ['author' => 'JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor', 'comment' => 'JMS\Serializer\Tests\Fixtures\DiscriminatedComment'])] private DiscriminatedAuthor|DiscriminatedComment $data; public function __construct($data) diff --git a/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php b/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php deleted file mode 100644 index 117d7d360..000000000 --- a/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php +++ /dev/null @@ -1,25 +0,0 @@ - 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor', 'comment' => 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment'])] - private MappedDiscriminatedAuthor|MappedDiscriminatedComment $data; - - public function __construct($data) - { - $this->data = $data; - } - - public function getData(): MappedDiscriminatedAuthor|MappedDiscriminatedComment - { - return $this->data; - } -} diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 2650276a7..c6ebc11f7 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -17,14 +17,11 @@ use JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor; use JMS\Serializer\Tests\Fixtures\DiscriminatedComment; use JMS\Serializer\Tests\Fixtures\FirstClassMapCollection; -use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor; -use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment; use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyArrayAndHash; use JMS\Serializer\Tests\Fixtures\ObjectWithInlineArray; use JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty; use JMS\Serializer\Tests\Fixtures\Tag; use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexDiscriminatedUnion; -use JMS\Serializer\Tests\Fixtures\TypedProperties\MappedComplexDiscriminatedUnion; use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; use JMS\Serializer\Visitor\SerializationVisitorInterface; @@ -155,10 +152,8 @@ protected static function getContent($key) $outputs['data_string'] = '{"data":"foo"}'; $outputs['data_author'] = '{"data":{"full_name":"foo"}}'; $outputs['data_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar"}}'; - $outputs['data_discriminated_author'] = '{"data":{"full_name":"foo","type":"JMS\\\Serializer\\\Tests\\\Fixtures\\\DiscriminatedAuthor"}}'; - $outputs['data_discriminated_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar","type":"JMS\\\Serializer\\\Tests\\\Fixtures\\\DiscriminatedComment"}}'; - $outputs['data_mapped_discriminated_author'] = '{"data":{"full_name":"foo","objectType":"author"}}'; - $outputs['data_mapped_discriminated_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar","objectType":"comment"}}'; + $outputs['data_discriminated_author'] = '{"data":{"full_name":"foo","objectType":"author"}}'; + $outputs['data_discriminated_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar","objectType":"comment"}}'; $outputs['uid'] = '"66b3177c-e03b-4a22-9dee-ddd7d37a04d5"'; $outputs['object_with_enums'] = '{"ordinary":"Clubs","backed_value":"C","backed_without_param":"C","ordinary_array":["Clubs","Spades"],"backed_array":["C","H"],"backed_array_without_param":["C","H"],"ordinary_auto_detect":"Clubs","backed_auto_detect":"C","backed_int_auto_detect":3,"backed_int":3,"backed_name":"C","backed_int_forced_str":3}'; $outputs['object_with_autodetect_enums'] = '{"ordinary_array_auto_detect":["Clubs","Spades"],"backed_array_auto_detect":["C","H"],"mixed_array_auto_detect":["Clubs","H"]}'; @@ -498,22 +493,6 @@ public function testDeserializingComplexDiscriminatedUnionProperties() self::assertEquals($commentUnion, $this->deserialize(static::getContent('data_discriminated_comment'), ComplexDiscriminatedUnion::class)); } - public function testDeserializingMappedComplexDiscriminatedUnionProperties() - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); - - return; - } - - $authorUnion = new MappedComplexDiscriminatedUnion(new MappedDiscriminatedAuthor('foo')); - self::assertEquals($authorUnion, $this->deserialize(static::getContent('data_mapped_discriminated_author'), MappedComplexDiscriminatedUnion::class)); - - $commentUnion = new MappedComplexDiscriminatedUnion(new MappedDiscriminatedComment(new Author('foo'), 'bar')); - - self::assertEquals($commentUnion, $this->deserialize(static::getContent('data_mapped_discriminated_comment'), MappedComplexDiscriminatedUnion::class)); - } - public function testSerializeingComplexDiscriminatedUnionProperties() { if (PHP_VERSION_ID < 80000) {