From 6532bdadfe9e2e11e4923a0b4f66ff714e1aa729 Mon Sep 17 00:00:00 2001 From: Marcin Czarnecki Date: Sun, 30 Jul 2023 13:39:45 +0200 Subject: [PATCH 01/11] feat(union): Add deserialisation of Union types from JSON --- src/Handler/UnionHandler.php | 99 +++++++++++++++++++ src/Metadata/Driver/TypedPropertiesDriver.php | 23 +++++ src/SerializerBuilder.php | 5 + .../Driver/UnionTypedPropertiesDriverTest.php | 14 ++- .../Serializer/BaseSerializationTestCase.php | 18 +--- tests/Serializer/JsonSerializationTest.php | 14 +++ tests/Serializer/XmlSerializationTest.php | 16 +++ 7 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 src/Handler/UnionHandler.php diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php new file mode 100644 index 000000000..b0d84a70a --- /dev/null +++ b/src/Handler/UnionHandler.php @@ -0,0 +1,99 @@ + 'union', + 'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION, + 'format' => 'json', + 'method' => 'deserializeUnion', + ], + [ + 'type' => 'union', + 'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION, + 'format' => 'xml', + 'method' => 'deserializeUnion', + ], + [ + 'type' => 'union', + 'format' => 'json', + 'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION, + 'method' => 'serializeUnion', + ], + [ + 'type' => 'union', + 'format' => 'xml', + 'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION, + 'method' => 'serializeUnion', + ], + ]; + } + + public function serializeUnion( + SerializationVisitorInterface $visitor, + $data, + array $type, + SerializationContext $context + ) { + return $this->matchSimpleType($data, $type, $context); + } + + /** + * @param int|string|\SimpleXMLElement $data + * @param array $type + */ + public function deserializeUnion(DeserializationVisitorInterface $visitor, $data, array $type, DeserializationContext $context) + { + 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($data, array $type, Context $context) + { + $dataType = gettype($data); + $alternativeName = null; + switch ($dataType) { + case 'boolean': + $alternativeName = 'bool'; + break; + case 'integer': + $alternativeName = 'int'; + break; + case 'double': + $alternativeName = 'float'; + break; + case 'array': + case 'string': + break; + default: + throw new RuntimeException(); + } + + foreach ($type['params'] as $possibleType) { + if ($possibleType['name'] === $dataType || $possibleType['name'] === $alternativeName) { + return $context->getNavigator()->accept($data, $possibleType); + } + } + } +} diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php index c3ebbefe4..951199b23 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -93,6 +93,11 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata $type = $reflectionType->getName(); $propertyMetadata->setType($this->typeParser->parse($type)); + } elseif ($this->shouldTypeHintUnion($reflectionType)) { + $propertyMetadata->setType([ + 'name' => 'union', + 'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->getTypes()), + ]); } } catch (ReflectionException $e) { continue; @@ -139,4 +144,22 @@ private function shouldTypeHint(?ReflectionType $reflectionType): bool return class_exists($reflectionType->getName()) || interface_exists($reflectionType->getName()); } + + /** + * @phpstan-assert-if-true \ReflectionUnionType $reflectionType + */ + private function shouldTypeHintUnion(?ReflectionType $reflectionType) + { + if (!$reflectionType instanceof \ReflectionUnionType) { + return false; + } + + foreach ($reflectionType->getTypes() as $type) { + if ($this->shouldTypeHint($type)) { + return true; + } + } + + return false; + } } diff --git a/src/SerializerBuilder.php b/src/SerializerBuilder.php index 47fa2d299..2fe6281c4 100644 --- a/src/SerializerBuilder.php +++ b/src/SerializerBuilder.php @@ -38,6 +38,7 @@ use JMS\Serializer\Handler\HandlerRegistryInterface; use JMS\Serializer\Handler\IteratorHandler; use JMS\Serializer\Handler\StdClassHandler; +use JMS\Serializer\Handler\UnionHandler; use JMS\Serializer\Naming\CamelCaseNamingStrategy; use JMS\Serializer\Naming\PropertyNamingStrategyInterface; use JMS\Serializer\Naming\SerializedNameAnnotationStrategy; @@ -283,6 +284,10 @@ public function addDefaultHandlers(): self $this->handlerRegistry->registerSubscribingHandler(new EnumHandler()); } + if (PHP_VERSION_ID >= 80000) { + $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); + } + return $this; } diff --git a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php index f4d1363f6..150bf2245 100644 --- a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php +++ b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php @@ -27,7 +27,19 @@ public function testInferUnionTypesShouldResultInNoType() $m = $this->resolve(UnionTypedProperties::class); self::assertEquals( - null, + [ + 'name' => 'union', + 'params' => [ + [ + 'name' => 'string', + 'params' => [], + ], + [ + 'name' => 'int', + 'params' => [], + ], + ], + ], $m->propertyMetadata['data']->type ); } diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index c30ba90af..9ce0d5cdb 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -30,6 +30,7 @@ use JMS\Serializer\Handler\IteratorHandler; use JMS\Serializer\Handler\StdClassHandler; use JMS\Serializer\Handler\SymfonyUidHandler; +use JMS\Serializer\Handler\UnionHandler; use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\SerializationContext; use JMS\Serializer\Serializer; @@ -1989,20 +1990,6 @@ public function testSerializingUnionTypedProperties() self::assertEquals(static::getContent('data_integer'), $this->serialize($object)); } - public function testThrowingExceptionWhenDeserializingUnionProperties() - { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); - - return; - } - - $this->expectException(RuntimeException::class); - - $object = new TypedProperties\UnionTypedProperties(10000); - self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), TypedProperties\UnionTypedProperties::class)); - } - public function testSerializingUnionDocBlockTypesProperties() { $object = new UnionTypedDocBLockProperty(10000); @@ -2021,7 +2008,7 @@ public function testThrowingExceptionWhenDeserializingUnionDocBlockTypes() $this->expectException(RuntimeException::class); $object = new UnionTypedDocBLockProperty(10000); - self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), TypedProperties\UnionTypedProperties::class)); + self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedDocBLockProperty::class)); } public function testIterable(): void @@ -2138,6 +2125,7 @@ protected function setUp(): void $this->handlerRegistry->registerSubscribingHandler(new IteratorHandler()); $this->handlerRegistry->registerSubscribingHandler(new SymfonyUidHandler()); $this->handlerRegistry->registerSubscribingHandler(new EnumHandler()); + $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); $this->handlerRegistry->registerHandler( GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'AuthorList', diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index af8c57996..12cebabd3 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -10,6 +10,7 @@ use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\GraphNavigatorInterface; +use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\SerializationContext; use JMS\Serializer\Tests\Fixtures\Author; use JMS\Serializer\Tests\Fixtures\AuthorList; @@ -17,6 +18,7 @@ use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyArrayAndHash; use JMS\Serializer\Tests\Fixtures\ObjectWithInlineArray; use JMS\Serializer\Tests\Fixtures\Tag; +use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; use JMS\Serializer\Visitor\SerializationVisitorInterface; @@ -434,6 +436,18 @@ public static function getTypeHintedArraysAndStdClass() ]; } + public function testDeserializingUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $object = new UnionTypedProperties(10000); + self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedProperties::class)); + } + /** * @param array $array * @param string $expected diff --git a/tests/Serializer/XmlSerializationTest.php b/tests/Serializer/XmlSerializationTest.php index 3701d95e3..7495b88c9 100644 --- a/tests/Serializer/XmlSerializationTest.php +++ b/tests/Serializer/XmlSerializationTest.php @@ -12,6 +12,7 @@ use JMS\Serializer\Handler\DateHandler; use JMS\Serializer\Handler\HandlerRegistryInterface; use JMS\Serializer\Metadata\ClassMetadata; +use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\Metadata\StaticPropertyMetadata; use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerBuilder; @@ -43,6 +44,7 @@ use JMS\Serializer\Tests\Fixtures\PersonLocation; use JMS\Serializer\Tests\Fixtures\SimpleClassObject; use JMS\Serializer\Tests\Fixtures\SimpleSubClassObject; +use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\XmlDeserializationVisitorFactory; use JMS\Serializer\Visitor\Factory\XmlSerializationVisitorFactory; use JMS\Serializer\XmlSerializationVisitor; @@ -597,6 +599,20 @@ public function testSerialisationWithPrecisionForFloat(): void ); } + public function testThrowingExceptionWhenDeserializingUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $this->expectException(RuntimeException::class); + + $object = new UnionTypedProperties(10000); + self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedProperties::class)); + } + private function xpathFirstToString(\SimpleXMLElement $xml, $xpath) { $nodes = $xml->xpath($xpath); From 8b2faac655990c0d5c55fdc66e54900b4e73c6c7 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 3 Jul 2024 10:52:12 -0400 Subject: [PATCH 02/11] handle primitive unions. --- src/Handler/UnionHandler.php | 105 +++++++++++------- .../UnionTypedDocBLockProperty.php | 2 +- .../TypedProperties/UnionTypedProperties.php | 2 +- .../Driver/UnionTypedPropertiesDriverTest.php | 8 ++ .../Serializer/BaseSerializationTestCase.php | 2 +- tests/Serializer/JsonSerializationTest.php | 24 ++++ 6 files changed, 100 insertions(+), 43 deletions(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index b0d84a70a..8e1f60009 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -14,42 +14,35 @@ final class UnionHandler implements SubscribingHandlerInterface { + static $aliases = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float']; /** * {@inheritdoc} */ public static function getSubscribingMethods() { - return [ - [ - 'type' => 'union', - 'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION, - 'format' => 'json', - 'method' => 'deserializeUnion', - ], - [ + $methods = []; + $formats = ['json', 'xml']; + + foreach ($formats as $format) { + $methods[] = [ 'type' => 'union', + 'format' => $format, 'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION, - 'format' => 'xml', 'method' => 'deserializeUnion', - ], - [ + ]; + $methods[] = [ 'type' => 'union', - 'format' => 'json', + 'format' => $format, 'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'method' => 'serializeUnion', - ], - [ - 'type' => 'union', - 'format' => 'xml', - 'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION, - 'method' => 'serializeUnion', - ], - ]; + ]; + } + return $methods; } public function serializeUnion( SerializationVisitorInterface $visitor, - $data, + mixed $data, array $type, SerializationContext $context ) { @@ -57,10 +50,10 @@ public function serializeUnion( } /** - * @param int|string|\SimpleXMLElement $data + * @param mixed $data * @param array $type */ - public function deserializeUnion(DeserializationVisitorInterface $visitor, $data, array $type, DeserializationContext $context) + public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed $data, array $type, DeserializationContext $context) { if ($data instanceof \SimpleXMLElement) { throw new RuntimeException('XML deserialisation into union types is not supported yet.'); @@ -69,25 +62,13 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, $data return $this->matchSimpleType($data, $type, $context); } - private function matchSimpleType($data, array $type, Context $context) + private function matchSimpleType(mixed $data, array $type, Context $context) { - $dataType = gettype($data); + $dataType = $this->determineType($data, $type, $context->getFormat()); $alternativeName = null; - switch ($dataType) { - case 'boolean': - $alternativeName = 'bool'; - break; - case 'integer': - $alternativeName = 'int'; - break; - case 'double': - $alternativeName = 'float'; - break; - case 'array': - case 'string': - break; - default: - throw new RuntimeException(); + + if (isset($aliases[$dataType])) { + $alternativeName = $aliases[$dataType]; } foreach ($type['params'] as $possibleType) { @@ -96,4 +77,48 @@ private function matchSimpleType($data, array $type, Context $context) } } } + + /** + * ReflectionUnionType::getTypes() returns the types sorted according to these rules: + * - Classes, interfaces, traits, iterable (replaced by Traversable), ReflectionIntersectionType objects, parent and self: + * these types will be returned first, in the order in which they were declared. + * - static and all built-in types (iterable replaced by array) will come next. They will always be returned in this order: + * static, callable, array, string, int, float, bool (or false or true), null. + * + * For determining types of primitives, it is necessary to reorder primitives so that they are tested from lowest specificity to highest: + * i.e. null, true, false, int, float, bool, string + */ + private function reorderTypes(array $type): array + { + if ($type['params']) { + uasort($type['params'], function($a, $b) { + $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6]; + return $order[$a['name']] || 7 <=> $order[$b['name']] || 7; + }); + } + return $type; + } + + private function determineType(mixed $data, array $type, string $format): string { + foreach ($this->reorderTypes($type)['params'] as $possibleType) { + if ($this->testPrimitive($data, $possibleType['name'], $format)) { + return $possibleType['name']; + } + } + } + private function testPrimitive(mixed $data, string $type, string $format): bool { + switch($type) { + case 'integer': + case 'int': + return (string)(int)$data === (string)$data; + case 'double': + case 'float': + return (string)(float)$data === (string)$data; + case 'bool': + case 'boolean': + return (string)(bool)$data === (string)$data; + case 'string': + return (string)$data === (string)$data; + } + } } diff --git a/tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php b/tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php index bee1063ea..aa58c58d7 100644 --- a/tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php +++ b/tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php @@ -7,7 +7,7 @@ class UnionTypedDocBLockProperty { /** - * @var int|string + * @var int|bool|float|string */ private $data; diff --git a/tests/Fixtures/TypedProperties/UnionTypedProperties.php b/tests/Fixtures/TypedProperties/UnionTypedProperties.php index 75bc6379a..86ab59b6e 100644 --- a/tests/Fixtures/TypedProperties/UnionTypedProperties.php +++ b/tests/Fixtures/TypedProperties/UnionTypedProperties.php @@ -6,7 +6,7 @@ class UnionTypedProperties { - private string|int $data; + private int|bool|float|string $data; public function __construct($data) { diff --git a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php index 150bf2245..a68d833fd 100644 --- a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php +++ b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php @@ -38,6 +38,14 @@ public function testInferUnionTypesShouldResultInNoType() 'name' => 'int', 'params' => [], ], + [ + 'name' => 'float', + 'params' => [], + ], + [ + 'name' => 'bool', + 'params' => [], + ], ], ], $m->propertyMetadata['data']->type diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 9ce0d5cdb..52aa5fc67 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -2008,7 +2008,7 @@ public function testThrowingExceptionWhenDeserializingUnionDocBlockTypes() $this->expectException(RuntimeException::class); $object = new UnionTypedDocBLockProperty(10000); - self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedDocBLockProperty::class)); + $deserialized = $this->deserialize(static::getContent('data_integer'), UnionTypedDocBLockProperty::class); } public function testIterable(): void diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 12cebabd3..3ad0f581b 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -142,6 +142,9 @@ protected static function getContent($key) $outputs['uninitialized_typed_props'] = '{"virtual_role":{},"id":1,"role":{},"tags":[]}'; $outputs['custom_datetimeinterface'] = '{"custom":"2021-09-07"}'; $outputs['data_integer'] = '{"data":10000}'; + $outputs['data_float'] = '{"data":1.236}'; + $outputs['data_bool'] = '{"data":false}'; + $outputs['data_string'] = '{"data":"foo"}'; $outputs['uid'] = '"66b3177c-e03b-4a22-9dee-ddd7d37a04d5"'; $outputs['object_with_enums'] = '{"ordinary":"Clubs","backed":"C","ordinary_array":["Clubs","Spades"],"backed_array":["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"]}'; @@ -446,6 +449,27 @@ public function testDeserializingUnionProperties() $object = new UnionTypedProperties(10000); self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedProperties::class)); + + $object = new UnionTypedProperties(1.236); + self::assertEquals($object, $this->deserialize(static::getContent('data_float'), UnionTypedProperties::class)); + + $object = new UnionTypedProperties(false); + self::assertEquals($object, $this->deserialize(static::getContent('data_bool'), UnionTypedProperties::class)); + + $object = new UnionTypedProperties('foo'); + self::assertEquals($object, $this->deserialize(static::getContent('data_string'), UnionTypedProperties::class)); + } + + public function testSerializeUnionProperties() + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $serialized = $this->serialize(new UnionTypedProperties(10000)); + self::assertEquals(static::getContent('data_integer'), $serialized); } /** From cea81c1ca5dbe8d329a1ed6e9a704784016521af Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 3 Jul 2024 12:43:34 -0400 Subject: [PATCH 03/11] docblock typo --- .../UnionTypedDocBLockProperty.php | 18 ------------------ tests/Metadata/Driver/DocBlockDriverTest.php | 4 ++-- .../Driver/UnionTypedPropertiesDriverTest.php | 2 +- tests/Serializer/BaseSerializationTestCase.php | 13 +++++++++---- tests/Serializer/Doctrine/IntegrationTest.php | 2 +- 5 files changed, 13 insertions(+), 26 deletions(-) delete mode 100644 tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php diff --git a/tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php b/tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php deleted file mode 100644 index aa58c58d7..000000000 --- a/tests/Fixtures/DocBlockType/UnionTypedDocBLockProperty.php +++ /dev/null @@ -1,18 +0,0 @@ -data = $data; - } -} diff --git a/tests/Metadata/Driver/DocBlockDriverTest.php b/tests/Metadata/Driver/DocBlockDriverTest.php index e608e78a0..476344cc8 100644 --- a/tests/Metadata/Driver/DocBlockDriverTest.php +++ b/tests/Metadata/Driver/DocBlockDriverTest.php @@ -45,7 +45,7 @@ use JMS\Serializer\Tests\Fixtures\DocBlockType\Phpstan\ProductType; use JMS\Serializer\Tests\Fixtures\DocBlockType\SingleClassFromDifferentNamespaceTypeHint; use JMS\Serializer\Tests\Fixtures\DocBlockType\SingleClassFromGlobalNamespaceTypeHint; -use JMS\Serializer\Tests\Fixtures\DocBlockType\UnionTypedDocBLockProperty; +use JMS\Serializer\Tests\Fixtures\DocBlockType\UnionTypedDocBlockProperty; use JMS\Serializer\Tests\Fixtures\DocBlockType\VirtualPropertyGetter; use PHPUnit\Framework\TestCase; @@ -360,7 +360,7 @@ public function testInferTypeForNonCollectionFromDifferentNamespaceType() public function testInferTypeForNonUnionDocblockType() { - $m = $this->resolve(UnionTypedDocBLockProperty::class); + $m = $this->resolve(UnionTypedDocBlockProperty::class); self::assertEquals( null, diff --git a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php index a68d833fd..f93a85008 100644 --- a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php +++ b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php @@ -22,7 +22,7 @@ protected function setUp(): void } } - public function testInferUnionTypesShouldResultInNoType() + public function testInferUnionTypesShouldResultInManyTypes() { $m = $this->resolve(UnionTypedProperties::class); diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 52aa5fc67..5d02494c4 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -64,7 +64,7 @@ use JMS\Serializer\Tests\Fixtures\Discriminator\Serialization\ExtendedUser; use JMS\Serializer\Tests\Fixtures\Discriminator\Serialization\User; use JMS\Serializer\Tests\Fixtures\DiscriminatorGroup\Car as DiscriminatorGroupCar; -use JMS\Serializer\Tests\Fixtures\DocBlockType\UnionTypedDocBLockProperty; +use JMS\Serializer\Tests\Fixtures\DocBlockType\UnionTypedDocBlockProperty; use JMS\Serializer\Tests\Fixtures\ExclusionStrategy\AlwaysExcludeExclusionStrategy; use JMS\Serializer\Tests\Fixtures\FirstClassListCollection; use JMS\Serializer\Tests\Fixtures\Garage; @@ -1992,9 +1992,14 @@ public function testSerializingUnionTypedProperties() public function testSerializingUnionDocBlockTypesProperties() { - $object = new UnionTypedDocBLockProperty(10000); + $object = new UnionTypedDocBlockProperty(10000); self::assertEquals(static::getContent('data_integer'), $this->serialize($object)); + + $object = new UnionTypedDocBlockProperty(1.236); + + self::assertEquals(static::getContent('data_float'), $this->serialize($object)); + } public function testThrowingExceptionWhenDeserializingUnionDocBlockTypes() @@ -2007,8 +2012,8 @@ public function testThrowingExceptionWhenDeserializingUnionDocBlockTypes() $this->expectException(RuntimeException::class); - $object = new UnionTypedDocBLockProperty(10000); - $deserialized = $this->deserialize(static::getContent('data_integer'), UnionTypedDocBLockProperty::class); + $object = new UnionTypedDocBlockProperty(10000); + $deserialized = $this->deserialize(static::getContent('data_integer'), UnionTypedDocBlockProperty::class); } public function testIterable(): void diff --git a/tests/Serializer/Doctrine/IntegrationTest.php b/tests/Serializer/Doctrine/IntegrationTest.php index 579e52b3b..7e271b905 100644 --- a/tests/Serializer/Doctrine/IntegrationTest.php +++ b/tests/Serializer/Doctrine/IntegrationTest.php @@ -67,9 +67,9 @@ public function testDiscriminatorIsInferredFromDoctrine() $teacher = new Teacher(); $class = new Clazz($teacher, [$student1, $student2]); + $em->persist($teacher); $em->persist($student1); $em->persist($student2); - $em->persist($teacher); $em->persist($class); $em->flush(); $em->clear(); From acb0b470633a38157348488cf793ec348649c1ba Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 3 Jul 2024 13:26:43 -0400 Subject: [PATCH 04/11] add missed file --- tests/Serializer/xml/data_float.xml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/Serializer/xml/data_float.xml diff --git a/tests/Serializer/xml/data_float.xml b/tests/Serializer/xml/data_float.xml new file mode 100644 index 000000000..d2da18a86 --- /dev/null +++ b/tests/Serializer/xml/data_float.xml @@ -0,0 +1,4 @@ + + + 1.236 + From 8869cbe873956869007358e4c80f7767819ce9db Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 3 Jul 2024 13:39:24 -0400 Subject: [PATCH 05/11] non primitive fallback bug --- src/Handler/UnionHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index 8e1f60009..5618cc40e 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -93,7 +93,7 @@ private function reorderTypes(array $type): array if ($type['params']) { uasort($type['params'], function($a, $b) { $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6]; - return $order[$a['name']] || 7 <=> $order[$b['name']] || 7; + return (array_key_exists($a['name'], $order) ? $order[$a['name']] : 7) <=> (array_key_exists($b['name'], $order) ? $order[$b['name']] : 7); }); } return $type; From ef5413c2fe173fa48fc9070b49809ac307cb7750 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 3 Jul 2024 15:56:03 -0400 Subject: [PATCH 06/11] code formatting and missed file. --- src/Handler/UnionHandler.php | 28 +++++++++++++------ .../UnionTypedDocBlockProperty.php | 18 ++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 tests/Fixtures/DocBlockType/UnionTypedDocBlockProperty.php diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index 5618cc40e..4b2af5ada 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -15,6 +15,7 @@ final class UnionHandler implements SubscribingHandlerInterface { static $aliases = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float']; + /** * {@inheritdoc} */ @@ -37,6 +38,7 @@ public static function getSubscribingMethods() 'method' => 'serializeUnion', ]; } + return $methods; } @@ -84,41 +86,49 @@ private function matchSimpleType(mixed $data, array $type, Context $context) * these types will be returned first, in the order in which they were declared. * - static and all built-in types (iterable replaced by array) will come next. They will always be returned in this order: * static, callable, array, string, int, float, bool (or false or true), null. - * + * * For determining types of primitives, it is necessary to reorder primitives so that they are tested from lowest specificity to highest: * i.e. null, true, false, int, float, bool, string */ private function reorderTypes(array $type): array { if ($type['params']) { - uasort($type['params'], function($a, $b) { + uasort($type['params'], static function ($a, $b) { $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6]; + return (array_key_exists($a['name'], $order) ? $order[$a['name']] : 7) <=> (array_key_exists($b['name'], $order) ? $order[$b['name']] : 7); }); } + return $type; } - private function determineType(mixed $data, array $type, string $format): string { + private function determineType(mixed $data, array $type, string $format): string + { foreach ($this->reorderTypes($type)['params'] as $possibleType) { if ($this->testPrimitive($data, $possibleType['name'], $format)) { return $possibleType['name']; } } } - private function testPrimitive(mixed $data, string $type, string $format): bool { - switch($type) { + + private function testPrimitive(mixed $data, string $type, string $format): bool + { + switch ($type) { case 'integer': case 'int': - return (string)(int)$data === (string)$data; + return (string) (int) $data === (string) $data; + case 'double': case 'float': - return (string)(float)$data === (string)$data; + return (string) (float) $data === (string) $data; + case 'bool': case 'boolean': - return (string)(bool)$data === (string)$data; + return (string) (bool) $data === (string) $data; + case 'string': - return (string)$data === (string)$data; + return (string) $data === (string) $data; } } } diff --git a/tests/Fixtures/DocBlockType/UnionTypedDocBlockProperty.php b/tests/Fixtures/DocBlockType/UnionTypedDocBlockProperty.php new file mode 100644 index 000000000..f7720b504 --- /dev/null +++ b/tests/Fixtures/DocBlockType/UnionTypedDocBlockProperty.php @@ -0,0 +1,18 @@ +data = $data; + } +} From b92dd329266950281751f8a276392cd4019f89f4 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 3 Jul 2024 16:02:24 -0400 Subject: [PATCH 07/11] mistake --- tests/Serializer/Doctrine/IntegrationTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Serializer/Doctrine/IntegrationTest.php b/tests/Serializer/Doctrine/IntegrationTest.php index cd6ee84d7..fcee0228a 100644 --- a/tests/Serializer/Doctrine/IntegrationTest.php +++ b/tests/Serializer/Doctrine/IntegrationTest.php @@ -69,6 +69,7 @@ public function testDiscriminatorIsInferredFromDoctrine() $class = new Clazz($teacher, [$student1, $student2]); $em->persist($teacher); + $em->flush(); $em->persist($student1); $em->persist($student2); $em->persist($class); From e6b1a49a5aac8919886af1c696cf2ac5f0cb7479 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Thu, 4 Jul 2024 10:11:12 -0400 Subject: [PATCH 08/11] phpstan --- src/Handler/UnionHandler.php | 16 ++++++++-------- .../Driver/UnionTypedPropertiesDriverTest.php | 2 +- tests/Serializer/BaseSerializationTestCase.php | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index 4b2af5ada..16324d97f 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -14,7 +14,7 @@ final class UnionHandler implements SubscribingHandlerInterface { - static $aliases = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float']; + private static $aliases = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float']; /** * {@inheritdoc} @@ -51,10 +51,6 @@ public function serializeUnion( return $this->matchSimpleType($data, $type, $context); } - /** - * @param mixed $data - * @param array $type - */ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed $data, array $type, DeserializationContext $context) { if ($data instanceof \SimpleXMLElement) { @@ -69,8 +65,8 @@ private function matchSimpleType(mixed $data, array $type, Context $context) $dataType = $this->determineType($data, $type, $context->getFormat()); $alternativeName = null; - if (isset($aliases[$dataType])) { - $alternativeName = $aliases[$dataType]; + if (isset(static::$aliases[$dataType])) { + $alternativeName = static::$aliases[$dataType]; } foreach ($type['params'] as $possibleType) { @@ -103,13 +99,15 @@ private function reorderTypes(array $type): array return $type; } - private function determineType(mixed $data, array $type, string $format): string + private function determineType(mixed $data, array $type, string $format): string|null { foreach ($this->reorderTypes($type)['params'] as $possibleType) { if ($this->testPrimitive($data, $possibleType['name'], $format)) { return $possibleType['name']; } } + + return null; } private function testPrimitive(mixed $data, string $type, string $format): bool @@ -130,5 +128,7 @@ private function testPrimitive(mixed $data, string $type, string $format): bool case 'string': return (string) $data === (string) $data; } + + return false; } } diff --git a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php index 8802ab9e0..138efc1b4 100644 --- a/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php +++ b/tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php @@ -50,7 +50,7 @@ public function testInferUnionTypesShouldResultInManyTypes() ], ], ], - $m->propertyMetadata['data']->type + $m->propertyMetadata['data']->type, ); } diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 52b04cd0d..7c089a498 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -1987,7 +1987,6 @@ public function testSerializingUnionDocBlockTypesProperties() $object = new UnionTypedDocBlockProperty(1.236); self::assertEquals(static::getContent('data_float'), $this->serialize($object)); - } public function testThrowingExceptionWhenDeserializingUnionDocBlockTypes() From 40e9e51d9022fe98f436fc5b78c63126bb9f1ac4 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Thu, 4 Jul 2024 17:21:41 -0400 Subject: [PATCH 09/11] only use UnionHandler with PHP > 8 --- src/Handler/UnionHandler.php | 2 +- tests/Serializer/BaseSerializationTestCase.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index 16324d97f..b4450abda 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -99,7 +99,7 @@ private function reorderTypes(array $type): array return $type; } - private function determineType(mixed $data, array $type, string $format): string|null + private function determineType(mixed $data, array $type, string $format): ?string { foreach ($this->reorderTypes($type)['params'] as $possibleType) { if ($this->testPrimitive($data, $possibleType['name'], $format)) { diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 7c089a498..2e72da251 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -2117,7 +2117,9 @@ protected function setUp(): void $this->handlerRegistry->registerSubscribingHandler(new IteratorHandler()); $this->handlerRegistry->registerSubscribingHandler(new SymfonyUidHandler()); $this->handlerRegistry->registerSubscribingHandler(new EnumHandler()); - $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); + if (PHP_VERSION_ID >= 80000) { + $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); + } $this->handlerRegistry->registerHandler( GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'AuthorList', From 480e92f90be5128a6e95321b7a58267d51d901a6 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Mon, 8 Jul 2024 10:52:53 -0400 Subject: [PATCH 10/11] move reorder types to properties driver --- src/Handler/UnionHandler.php | 25 +---------------- src/Metadata/Driver/TypedPropertiesDriver.php | 27 +++++++++++++++++-- .../Serializer/BaseSerializationTestCase.php | 1 + 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index b4450abda..ad7f4339a 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -76,32 +76,9 @@ private function matchSimpleType(mixed $data, array $type, Context $context) } } - /** - * ReflectionUnionType::getTypes() returns the types sorted according to these rules: - * - Classes, interfaces, traits, iterable (replaced by Traversable), ReflectionIntersectionType objects, parent and self: - * these types will be returned first, in the order in which they were declared. - * - static and all built-in types (iterable replaced by array) will come next. They will always be returned in this order: - * static, callable, array, string, int, float, bool (or false or true), null. - * - * For determining types of primitives, it is necessary to reorder primitives so that they are tested from lowest specificity to highest: - * i.e. null, true, false, int, float, bool, string - */ - private function reorderTypes(array $type): array - { - if ($type['params']) { - uasort($type['params'], static function ($a, $b) { - $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6]; - - return (array_key_exists($a['name'], $order) ? $order[$a['name']] : 7) <=> (array_key_exists($b['name'], $order) ? $order[$b['name']] : 7); - }); - } - - return $type; - } - private function determineType(mixed $data, array $type, string $format): ?string { - foreach ($this->reorderTypes($type)['params'] as $possibleType) { + foreach ($type['params'] as $possibleType) { if ($this->testPrimitive($data, $possibleType['name'], $format)) { return $possibleType['name']; } diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php index b628ad80a..dd3ea1ec5 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -47,6 +47,29 @@ public function __construct(DriverInterface $delegate, ?ParserInterface $typePar $this->allowList = array_merge($allowList, $this->getDefaultWhiteList()); } + /** + * ReflectionUnionType::getTypes() returns the types sorted according to these rules: + * - Classes, interfaces, traits, iterable (replaced by Traversable), ReflectionIntersectionType objects, parent and self: + * these types will be returned first, in the order in which they were declared. + * - static and all built-in types (iterable replaced by array) will come next. They will always be returned in this order: + * static, callable, array, string, int, float, bool (or false or true), null. + * + * For determining types of primitives, it is necessary to reorder primitives so that they are tested from lowest specificity to highest: + * i.e. null, true, false, int, float, bool, string + */ + private function reorderTypes(array $type): array + { + if ($type['params']) { + uasort($type['params'], static function ($a, $b) { + $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6]; + + return (array_key_exists($a['name'], $order) ? $order[$a['name']] : 7) <=> (array_key_exists($b['name'], $order) ? $order[$b['name']] : 7); + }); + } + + return $type; + } + private function getDefaultWhiteList(): array { return [ @@ -90,10 +113,10 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata $propertyMetadata->setType($this->typeParser->parse($type)); } elseif ($this->shouldTypeHintUnion($reflectionType)) { - $propertyMetadata->setType([ + $propertyMetadata->setType($this->reorderTypes([ 'name' => 'union', 'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->getTypes()), - ]); + ])); } } catch (ReflectionException $e) { continue; diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 2e72da251..52626e14e 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -2120,6 +2120,7 @@ protected function setUp(): void if (PHP_VERSION_ID >= 80000) { $this->handlerRegistry->registerSubscribingHandler(new UnionHandler()); } + $this->handlerRegistry->registerHandler( GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'AuthorList', From ffa7b5ef33feba330c5417ee55678b428c6f7972 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Tue, 9 Jul 2024 09:26:21 -0400 Subject: [PATCH 11/11] simplify condition. --- src/Metadata/Driver/TypedPropertiesDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php index dd3ea1ec5..99f93cbaa 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -63,7 +63,7 @@ private function reorderTypes(array $type): array uasort($type['params'], static function ($a, $b) { $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6]; - return (array_key_exists($a['name'], $order) ? $order[$a['name']] : 7) <=> (array_key_exists($b['name'], $order) ? $order[$b['name']] : 7); + return ($order[$a['name']] ?? 7) <=> ($order[$b['name']] ?? 7); }); }