diff --git a/src/Annotation/Discriminator.php b/src/Annotation/Discriminator.php index f010036f8..0dbaafd2a 100644 --- a/src/Annotation/Discriminator.php +++ b/src/Annotation/Discriminator.php @@ -13,7 +13,7 @@ class Discriminator implements SerializerAttribute { use AnnotationUtilsTrait; - /** @var array */ + /** @var array */ public $map = []; /** @var string */ 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/Exception/PropertyMissingException.php b/src/Exception/PropertyMissingException.php new file mode 100644 index 000000000..1454c83f5 --- /dev/null +++ b/src/Exception/PropertyMissingException.php @@ -0,0 +1,9 @@ +visitor->startVisitingObject($metadata, $object, $type); foreach ($metadata->propertyMetadata as $propertyMetadata) { + $allowsNull = null === $propertyMetadata->type ? true : $this->allowsNull($propertyMetadata->type); if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) { continue; } @@ -212,12 +214,21 @@ public function accept($data, ?array $type = null) $this->context->pushPropertyMetadata($propertyMetadata); try { $v = $this->visitor->visitProperty($propertyMetadata, $data); + $this->accessor->setValue($object, $v, $propertyMetadata, $this->context); } catch (NotAcceptableException $e) { if (true === $propertyMetadata->hasDefault) { $cloned = clone $propertyMetadata; $cloned->setter = null; $this->accessor->setValue($object, $cloned->defaultValue, $cloned, $this->context); + } elseif (!$allowsNull && $this->visitor->getRequireAllRequiredProperties()) { + $this->visitor->endVisitingObject($metadata, $data, $type); + + throw new PropertyMissingException("Property $propertyMetadata->name is missing from data "); + } elseif ($this->visitor->getRequireAllRequiredProperties()) { + $this->visitor->endVisitingObject($metadata, $data, $type); + + throw $e; } } @@ -231,6 +242,22 @@ public function accept($data, ?array $type = null) } } + private function allowsNull(array $type) + { + $allowsNull = false; + if ('union' === $type['name']) { + foreach ($type['params'] as $param) { + if ('NULL' === $param['name']) { + $allowsNull = true; + } + } + } elseif ('NULL' === $type['name']) { + $allowsNull = true; + } + + return $allowsNull; + } + /** * @param mixed $data */ diff --git a/src/GraphNavigator/SerializationGraphNavigator.php b/src/GraphNavigator/SerializationGraphNavigator.php index 1de6e3bb7..f6fbed112 100644 --- a/src/GraphNavigator/SerializationGraphNavigator.php +++ b/src/GraphNavigator/SerializationGraphNavigator.php @@ -177,6 +177,18 @@ 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; + } + } default: if (null !== $data) { if ($this->context->isVisiting($data)) { diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index ad7f4339a..01d0e2b18 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -6,6 +6,11 @@ use JMS\Serializer\Context; use JMS\Serializer\DeserializationContext; +use JMS\Serializer\Exception\NonFloatCastableTypeException; +use JMS\Serializer\Exception\NonIntCastableTypeException; +use JMS\Serializer\Exception\NonStringCastableTypeException; +use JMS\Serializer\Exception\NonVisitableTypeException; +use JMS\Serializer\Exception\PropertyMissingException; use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\SerializationContext; @@ -15,6 +20,12 @@ final class UnionHandler implements SubscribingHandlerInterface { private static $aliases = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float']; + private bool $requireAllProperties; + + public function __construct(bool $requireAllProperties = false) + { + $this->requireAllProperties = $requireAllProperties; + } /** * {@inheritdoc} @@ -47,46 +58,115 @@ 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])) { - $alternativeName = static::$aliases[$dataType]; - } - foreach ($type['params'] as $possibleType) { - if ($possibleType['name'] === $dataType || $possibleType['name'] === $alternativeName) { - return $context->getNavigator()->accept($data, $possibleType); + $propertyMetadata = $context->getMetadataStack()->top(); + $finalType = null; + if (null !== $propertyMetadata->unionDiscriminatorField) { + if (!array_key_exists($propertyMetadata->unionDiscriminatorField, $data)) { + throw new NonVisitableTypeException("Union Discriminator Field '$propertyMetadata->unionDiscriminatorField' not found in data1"); + } + + $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 { + try { + $previousVisitorRequireSetting = $visitor->getRequireAllRequiredProperties(); + if ($this->requireAllProperties) { + $visitor->setRequireAllRequiredProperties($this->requireAllProperties); + } + + if ($this->isPrimitiveType($possibleType['name']) && (is_array($data) || !$this->testPrimitive($data, $possibleType['name'], $context->getFormat()))) { + continue; + } + + $accept = $context->getNavigator()->accept($data, $possibleType); + if ($this->requireAllProperties) { + $visitor->setRequireAllRequiredProperties($previousVisitorRequireSetting); + } + + return $accept; + } catch (NonVisitableTypeException $e) { + continue; + } catch (PropertyMissingException $e) { + continue; + } catch (NonStringCastableTypeException $e) { + continue; + } catch (NonIntCastableTypeException $e) { + continue; + } catch (NonFloatCastableTypeException $e) { + continue; + } } } + + 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/JsonDeserializationStrictVisitor.php b/src/JsonDeserializationStrictVisitor.php index 3c4e69a85..ecae984a9 100644 --- a/src/JsonDeserializationStrictVisitor.php +++ b/src/JsonDeserializationStrictVisitor.php @@ -19,6 +19,13 @@ final class JsonDeserializationStrictVisitor extends AbstractVisitor implements /** @var JsonDeserializationVisitor */ private $wrappedDeserializationVisitor; + /** + * THIS IS ONLY USED FOR UNION DESERIALIZATION WHICH IS NOT SUPPORTED IN XML + * + * @var bool + */ + private $requireAllRequiredProperties = false; + public function __construct( int $options = 0, int $depth = 512 @@ -26,6 +33,16 @@ public function __construct( $this->wrappedDeserializationVisitor = new JsonDeserializationVisitor($options, $depth); } + public function setRequireAllRequiredProperties(bool $requireAllRequiredProperties): void + { + $this->requireAllRequiredProperties = $requireAllRequiredProperties; + } + + public function getRequireAllRequiredProperties(): bool + { + return $this->requireAllRequiredProperties; + } + public function setNavigator(GraphNavigatorInterface $navigator): void { parent::setNavigator($navigator); diff --git a/src/JsonDeserializationVisitor.php b/src/JsonDeserializationVisitor.php index 625c8c722..c6e22bb98 100644 --- a/src/JsonDeserializationVisitor.php +++ b/src/JsonDeserializationVisitor.php @@ -33,13 +33,30 @@ final class JsonDeserializationVisitor extends AbstractVisitor implements Deseri */ private $currentObject; + /** + * @var bool + */ + private $requireAllRequiredProperties; + public function __construct( int $options = 0, - int $depth = 512 + int $depth = 512, + bool $requireAllRequiredProperties = false ) { $this->objectStack = new \SplStack(); $this->options = $options; $this->depth = $depth; + $this->requireAllRequiredProperties = $requireAllRequiredProperties; + } + + public function setRequireAllRequiredProperties(bool $requireAllRequiredProperties): void + { + $this->requireAllRequiredProperties = $requireAllRequiredProperties; + } + + public function getRequireAllRequiredProperties(): bool + { + return $this->requireAllRequiredProperties; } /** @@ -151,6 +168,9 @@ public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): s */ public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void { + $cur = $this->getCurrentObject() ? get_class($this->getCurrentObject()) : 'null'; + $objtype = $object ? get_class($object) : 'null'; + $stacksize = $this->objectStack->count(); $this->setCurrentObject($object); } @@ -194,7 +214,11 @@ public function visitProperty(PropertyMetadata $metadata, $data) public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object { $obj = $this->currentObject; + $prevObj = $this->objectStack->top(); + $prevObjType = $prevObj ? get_class($prevObj) : 'null'; + $this->revertCurrentObject(); + $stacksize = $this->objectStack->count(); return $obj; } 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..d4a5105ec 100644 --- a/src/Metadata/Driver/TypedPropertiesDriver.php +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -48,19 +48,48 @@ public function __construct(DriverInterface $delegate, ?ParserInterface $typePar } /** - * 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. + * In order to deserialize non-discriminated unions, each possible type is attempted in turn. + * Therefore, the types must be ordered from most specific to least specific, so that the most specific type is attempted first. * - * 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 + * ReflectionUnionType::getTypes() does not return types in that order, so we need to reorder them. + * + * This method reorders the types in the following order: + * - primitives in speficity order: null, true, false, int, float, bool, string + * - classes and interaces in order of most number of required properties */ private function reorderTypes(array $type): array { + $self = $this; if ($type['params']) { - uasort($type['params'], static function ($a, $b) { + uasort($type['params'], static function ($a, $b) use ($self) { + if (\class_exists($a['name']) && \class_exists($b['name'])) { + $aMetadata = $self->loadMetadataForClass(new \ReflectionClass($a['name'])); + $bMetadata = $self->loadMetadataForClass(new \ReflectionClass($b['name'])); + $aRequiredPropertyCount = 0; + $bRequiredPropertyCount = 0; + foreach ($aMetadata->propertyMetadata as $propertyMetadata) { + if ($propertyMetadata->type && !$self->allowsNull($propertyMetadata->type)) { + $aRequiredPropertyCount++; + } + } + + foreach ($bMetadata->propertyMetadata as $propertyMetadata) { + if ($propertyMetadata->type && !$self->allowsNull($propertyMetadata->type)) { + $bRequiredPropertyCount++; + } + } + + return $bRequiredPropertyCount <=> $aRequiredPropertyCount; + } + + if (\class_exists($a['name'])) { + return 1; + } + + if (\class_exists($b['name'])) { + return -1; + } + $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6]; return ($order[$a['name']] ?? 7) <=> ($order[$b['name']] ?? 7); @@ -70,6 +99,22 @@ private function reorderTypes(array $type): array return $type; } + private function allowsNull(array $type) + { + $allowsNull = false; + if ('union' === $type['name']) { + foreach ($type['params'] as $param) { + if ('NULL' === $param['name']) { + $allowsNull = true; + } + } + } elseif ('NULL' === $type['name']) { + $allowsNull = true; + } + + return $allowsNull; + } + private function getDefaultWhiteList(): array { return [ diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index ff41c949f..aeb88b952 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -33,6 +33,17 @@ class PropertyMetadata extends BasePropertyMetadata */ public $serializedName; + + /** + * @var string|null + */ + public $unionDiscriminatorField; + + /** + * @var array|null + */ + public $unionDiscriminatorMap; + /** * @var array|null */ @@ -196,6 +207,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 +241,8 @@ protected function serializeToArray(): array $this->untilVersion, $this->groups, $this->serializedName, + $this->unionDiscriminatorField, + $this->unionDiscriminatorMap, $this->type, $this->xmlCollection, $this->xmlCollectionInline, @@ -258,6 +277,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..8bc5bab7f 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(requireAllProperties: true)); } return $this; diff --git a/src/Visitor/DeserializationVisitorInterface.php b/src/Visitor/DeserializationVisitorInterface.php index fd16dc483..d924a9dbb 100644 --- a/src/Visitor/DeserializationVisitorInterface.php +++ b/src/Visitor/DeserializationVisitorInterface.php @@ -94,4 +94,8 @@ public function endVisitingObject(ClassMetadata $metadata, $data, array $type): * @return mixed */ public function getResult($data); + + public function setRequireAllRequiredProperties(bool $requireAllRequiredProperties): void; + + public function getRequireAllRequiredProperties(): bool; } diff --git a/src/XmlDeserializationVisitor.php b/src/XmlDeserializationVisitor.php index 1efa3c90c..b9cacfed0 100644 --- a/src/XmlDeserializationVisitor.php +++ b/src/XmlDeserializationVisitor.php @@ -54,6 +54,13 @@ final class XmlDeserializationVisitor extends AbstractVisitor implements NullAwa */ private $options; + /** + * THIS IS ONLY USED FOR UNION DESERIALIZATION WHICH IS NOT SUPPORTED IN XML + * + * @var bool + */ + private $requireAllRequiredProperties = false; + public function __construct( bool $disableExternalEntities = true, array $doctypeAllowList = [], @@ -67,6 +74,16 @@ public function __construct( $this->options = $options; } + public function setRequireAllRequiredProperties(bool $requireAllRequiredProperties): void + { + $this->requireAllRequiredProperties = $requireAllRequiredProperties; + } + + public function getRequireAllRequiredProperties(): bool + { + return $this->requireAllRequiredProperties; + } + /** * {@inheritdoc} */ 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..49661f997 --- /dev/null +++ b/tests/Fixtures/DiscriminatedComment.php @@ -0,0 +1,45 @@ +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..76e06b1fa --- /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..984017971 --- /dev/null +++ b/tests/Fixtures/MappedDiscriminatedComment.php @@ -0,0 +1,45 @@ +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..b60227b49 --- /dev/null +++ b/tests/Fixtures/TypedProperties/ComplexDiscriminatedUnion.php @@ -0,0 +1,26 @@ +data = $data; + } + + public function getData(): Author|Comment + { + return $this->data; + } +} + diff --git a/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php b/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php new file mode 100644 index 000000000..62eb0d197 --- /dev/null +++ b/tests/Fixtures/TypedProperties/ComplexUnionTypedProperties.php @@ -0,0 +1,25 @@ +data = $data; + } + + public function getData(): Author|Comment + { + return $this->data; + } +} + diff --git a/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php b/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php new file mode 100644 index 000000000..e7e17f52a --- /dev/null +++ b/tests/Fixtures/TypedProperties/MappedComplexDiscriminatedUnion.php @@ -0,0 +1,26 @@ + 'JMS\Serializer\Tests\Fixtures\DiscriminatedAuthor', 'comment' => 'JMS\Serializer\Tests\Fixtures\DiscriminatedComment'])] + private DiscriminatedAuthor|DiscriminatedComment $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData(): Author|Comment + { + return $this->data; + } +} + diff --git a/tests/Metadata/Driver/DocBlockDriverTest.php b/tests/Metadata/Driver/DocBlockDriverTest.php index b772333d6..0640e3140 100644 --- a/tests/Metadata/Driver/DocBlockDriverTest.php +++ b/tests/Metadata/Driver/DocBlockDriverTest.php @@ -412,4 +412,6 @@ public function testAlternativeNames() $m->propertyMetadata['boolean']->type, ); } + + } diff --git a/tests/Serializer/BaseSerializationTestCase.php b/tests/Serializer/BaseSerializationTestCase.php index 52626e14e..c375f8dfc 100644 --- a/tests/Serializer/BaseSerializationTestCase.php +++ b/tests/Serializer/BaseSerializationTestCase.php @@ -274,9 +274,9 @@ public function testDeserializeNullObject() self::assertNull($dObj->getNullProperty()); } - /** - * @dataProvider getTypes - */ + // /** + // * @dataProvider getTypes + // */ #[DataProvider('getTypes')] public function testNull($type) { @@ -292,9 +292,9 @@ public function testNull($type) $this->serialize(null, $context); } - /** - * @dataProvider getTypes - */ + // /** + // * @dataProvider getTypes + // */ #[DataProvider('getTypes')] public function testNullAllowed($type) { @@ -1254,9 +1254,9 @@ public function testFormErrors() self::assertEquals(static::getContent('form_errors'), $this->serialize($errors)); } - /** - * @dataProvider initialFormTypeProvider - */ + // /** + // * @dataProvider initialFormTypeProvider + // */ #[DataProvider('initialFormTypeProvider')] public function testNestedFormErrors($type) { @@ -1282,10 +1282,10 @@ public function testNestedFormErrors($type) self::assertEquals(static::getContent('nested_form_errors'), $this->serialize($form, $context)); } - /** - * @doesNotPerformAssertions - * @dataProvider initialFormTypeProvider - */ + // /** + // * @doesNotPerformAssertions + // * @dataProvider initialFormTypeProvider + // */ #[DataProvider('initialFormTypeProvider')] #[DoesNotPerformAssertions] public function testFormErrorsWithNonFormComponents($type) @@ -1987,6 +1987,7 @@ public function testSerializingUnionDocBlockTypesProperties() $object = new UnionTypedDocBlockProperty(1.236); self::assertEquals(static::getContent('data_float'), $this->serialize($object)); + } public function testThrowingExceptionWhenDeserializingUnionDocBlockTypes() @@ -2118,7 +2119,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(requireAllProperties: true)); } $this->handlerRegistry->registerHandler( diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 25cb0cf17..581dcd69b 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -13,13 +13,22 @@ use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\SerializationContext; use JMS\Serializer\Tests\Fixtures\Author; +use JMS\Serializer\Tests\Fixtures\MoreSpecificAuthor; use JMS\Serializer\Tests\Fixtures\AuthorList; +use JMS\Serializer\Tests\Fixtures\Comment; use JMS\Serializer\Tests\Fixtures\FirstClassMapCollection; 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\DiscriminatedAuthor; +use JMS\Serializer\Tests\Fixtures\DiscriminatedComment; +use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor; +use JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment; use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; +use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexUnionTypedProperties; +use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexDiscriminatedUnion; +use JMS\Serializer\Tests\Fixtures\TypedProperties\MappedComplexDiscriminatedUnion; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; use JMS\Serializer\Visitor\SerializationVisitorInterface; use PHPUnit\Framework\Attributes\DataProvider; @@ -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","type":"author"}}'; + $outputs['data_mapped_discriminated_comment'] = '{"data":{"author":{"full_name":"foo"},"text":"bar","type":"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"]}'; @@ -176,9 +192,7 @@ public static function getFirstClassMapCollectionsValues() ]; } - /** - * @dataProvider getFirstClassMapCollectionsValues - */ + #[DataProvider('getFirstClassMapCollectionsValues')] public function testFirstClassMapCollections(array $items, string $expected): void { @@ -390,9 +404,7 @@ public static function getTypeHintedArrays() ]; } - /** - * @dataProvider getTypeHintedArrays - */ + #[DataProvider('getTypeHintedArrays')] public function testTypeHintedArraySerialization(array $array, string $expected, ?SerializationContext $context = null) { @@ -449,7 +461,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 +471,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), 'bar'); + 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), 'bar')); + 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); } /**