diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7d83a..c21e995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ # 3.0.0 (unreleased) +* Update to liip/metadata-parser 2.x + * The `LiipMetadataAnnotationParser` got renamed to `LiipMetadataAttributeParser` which means the `@Preferred` + annotation is no longer allowed and has to be replaced by the `#[Preferred]` attribute. + * Setting a (different) naming strategy via the `PropertyCollection::useIdenticalNamingStrategy` is no longer + possible, instead the naming strategy now has to be passed as the second argument when creating a new instance + of the `Parser` class + * For further changes in this library, take a look at the + [changelog from metadata-parser](https://github.com/liip/metadata-parser/blob/2.x/CHANGELOG.md) +* Add discriminator support + # 2.x # 2.6.2 diff --git a/README.md b/README.md index 37123dd..f645f8d 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ use Liip\MetadataParser\Builder; use Liip\MetadataParser\Parser; use Liip\MetadataParser\RecursionChecker; use Liip\MetadataParser\ModelParser\JMSParser; -use Liip\MetadataParser\ModelParser\LiipMetadataAnnotationParser; +use Liip\MetadataParser\ModelParser\LiipMetadataAttributeParser; use Liip\MetadataParser\ModelParser\PhpDocParser; use Liip\MetadataParser\ModelParser\ReflectionParser; use Liip\Serializer\DeserializerGenerator; @@ -75,7 +75,7 @@ $parsers = [ new ReflectionParser(), new PhpDocParser(), new JMSParser(new AnnotationReader()), - new LiipMetadataAnnotationParser(new AnnotationReader()), + new LiipMetadataAttributeParser(), ]; $builder = new Builder(new Parser($parsers), new RecursionChecker(null, [])); diff --git a/composer.json b/composer.json index b907984..f725ab3 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "require": { "php": "^8.0", "ext-json": "*", - "liip/metadata-parser": "^1.2", + "liip/metadata-parser": "^2.0", "pnz/json-exception": "^1.0", "symfony/filesystem": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", "symfony/finder": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", @@ -25,7 +25,7 @@ }, "require-dev": { "doctrine/collections": "^1.6", - "friendsofphp/php-cs-fixer": "^3.23", + "friendsofphp/php-cs-fixer": "^3.90.0", "jms/serializer": "^1.13 || ^2 || ^3", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.3", diff --git a/src/DeserializerGenerator.php b/src/DeserializerGenerator.php index 69debe3..ff8bd77 100644 --- a/src/DeserializerGenerator.php +++ b/src/DeserializerGenerator.php @@ -7,10 +7,12 @@ use Liip\MetadataParser\Builder; use Liip\MetadataParser\Metadata\ClassMetadata; use Liip\MetadataParser\Metadata\PropertyMetadata; -use Liip\MetadataParser\Metadata\PropertyTypeArray; +use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypeDateTime; +use Liip\MetadataParser\Metadata\PropertyTypeIterable; use Liip\MetadataParser\Metadata\PropertyTypePrimitive; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\Reducer\TakeBestReducer; use Liip\Serializer\Configuration\ClassToGenerate; @@ -89,6 +91,11 @@ private function generateCodeForClass( ModelPath $modelPath, array $stack = [], ): string { + $discriminatorMetadata = $classMetadata->getDiscriminatorMetadata(); + if (null !== $discriminatorMetadata && $discriminatorMetadata->baseClass == $classMetadata->getClassName()) { + return $this->generateCodeForDiscriminatorClass($classMetadata, $arrayPath, $modelPath, $stack); + } + $stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1; $constructorArgumentNames = []; @@ -143,6 +150,29 @@ private function generateCodeForClass( return $this->templating->renderClass((string) $modelPath, $classMetadata->getClassName(), $constructorArguments, $code, $initCode); } + /** + * @param array $stack + */ + private function generateCodeForDiscriminatorClass( + ClassMetadata $classMetadata, + ArrayPath $arrayPath, + ModelPath $modelPath, + array $stack = [], + ): string { + $code = ''; + $discriminatorMetadata = $classMetadata->getDiscriminatorMetadata(); + $discriminatorFieldPath = $arrayPath->withFieldName($discriminatorMetadata->propertyName); + foreach ($discriminatorMetadata->classMap as $typeValue => $class) { + $code .= $this->templating->renderDiscriminatorConditional( + (string) $discriminatorFieldPath, + $typeValue, + $this->generateCodeForClass($discriminatorMetadata->getMetadataForClass($class), $arrayPath, $modelPath, $stack) + ); + } + + return $code; + } + /** * @param array $stack */ @@ -204,7 +234,7 @@ private function generateInnerCodeForFieldType( $type = $propertyMetadata->getType(); switch ($type) { - case $type instanceof PropertyTypeArray: + case $type instanceof PropertyTypeIterable: if ($type->isTraversable()) { return $this->generateCodeForArrayCollection($propertyMetadata, $type, $arrayPath, $modelPropertyPath, $stack); } @@ -229,16 +259,74 @@ private function generateInnerCodeForFieldType( case $type instanceof PropertyTypeClass: return $this->generateCodeForClass($type->getClassMetadata(), $arrayPath, $modelPropertyPath, $stack); + case $type instanceof PropertyTypeUnion: + return $this->generateCodeForUnion($type, $arrayPath, $modelPropertyPath, $stack); + default: throw new \Exception('Unexpected type '.$type::class.' at '.$modelPropertyPath); } } + /** + * @param array $stack + */ + private function generateCodeForUnion( + PropertyTypeUnion $type, + ArrayPath $arrayPath, + ModelPath $modelPath, + array $stack, + ): string { + $code = ''; + + $types = $type->getTypes(); + $typesWithoutPrimitives = array_filter($types, static function (PropertyType $subType): bool { + return !($subType instanceof PropertyTypePrimitive || $subType instanceof PropertyTypeIterable); + }); + + $fieldName = $type->getFieldName(); + if (null !== $fieldName) { + $discriminatorFieldPath = $arrayPath->withFieldName($fieldName); + + foreach ($type->getTypeMap() as $typeValue => $class) { + $classType = $type->getTypeByClassName($class); + $code .= $this->templating->renderDiscriminatorConditional( + (string) $discriminatorFieldPath, + $typeValue, + $this->generateCodeForClass($classType->getClassMetadata(), $arrayPath, $modelPath, $stack) + ); + } + + return $code; + } + + if (0 !== \count($typesWithoutPrimitives)) { + throw new \Exception('Found union type that contains primitives and non primitives, which is currently not supported.'); + } + + $amountOfTypes = \count($types); + foreach ($types as $key => $subType) { + $phpType = 'array'; + if ($subType instanceof PropertyTypePrimitive) { + $phpType = $subType->getTypeName(); + } + + $withElseBlock = $key !== ($amountOfTypes - 1); + $code .= $this->templating->renderPrimitiveConditional( + $phpType, + (string) $arrayPath, + $this->templating->renderAssignJsonDataToFieldWithCast($phpType, (string) $modelPath, (string) $arrayPath), + $withElseBlock + ); + } + + return $code; + } + /** * @param array $stack */ private function generateCodeForArray( - PropertyTypeArray $type, + PropertyTypeIterable $type, ArrayPath $arrayPath, ModelPath $modelPath, array $stack, @@ -254,7 +342,7 @@ private function generateCodeForArray( $subType = $type->getSubType(); switch ($subType) { - case $subType instanceof PropertyTypeArray: + case $subType instanceof PropertyTypeIterable: $innerCode = $this->generateCodeForArray($subType, $arrayPropertyPath, $modelPropertyPath, $stack); break; @@ -284,7 +372,7 @@ private function generateCodeForArray( */ private function generateCodeForArrayCollection( PropertyMetadata $propertyMetadata, - PropertyTypeArray $type, + PropertyTypeIterable $type, ArrayPath $arrayPath, ModelPath $modelPath, array $stack, diff --git a/src/Recursion.php b/src/Recursion.php index 69edc38..e6fab3e 100644 --- a/src/Recursion.php +++ b/src/Recursion.php @@ -5,8 +5,8 @@ namespace Liip\Serializer; use Liip\MetadataParser\Metadata\PropertyMetadata; -use Liip\MetadataParser\Metadata\PropertyTypeArray; use Liip\MetadataParser\Metadata\PropertyTypeClass; +use Liip\MetadataParser\Metadata\PropertyTypeIterable; abstract class Recursion { @@ -44,7 +44,7 @@ public static function hasMaxDepthReached(PropertyMetadata $propertyMetadata, ar private static function getClassNameFromProperty(PropertyMetadata $propertyMetadata): ?string { $type = $propertyMetadata->getType(); - if ($type instanceof PropertyTypeArray) { + if ($type instanceof PropertyTypeIterable) { $type = $type->getLeafType(); } diff --git a/src/SerializerGenerator.php b/src/SerializerGenerator.php index 2915a70..8adcfba 100644 --- a/src/SerializerGenerator.php +++ b/src/SerializerGenerator.php @@ -8,10 +8,11 @@ use Liip\MetadataParser\Metadata\ClassMetadata; use Liip\MetadataParser\Metadata\PropertyMetadata; use Liip\MetadataParser\Metadata\PropertyType; -use Liip\MetadataParser\Metadata\PropertyTypeArray; use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypeDateTime; +use Liip\MetadataParser\Metadata\PropertyTypeIterable; use Liip\MetadataParser\Metadata\PropertyTypePrimitive; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\Reducer\GroupReducer; use Liip\MetadataParser\Reducer\PreferredReducer; @@ -120,6 +121,11 @@ private function generateCodeForClass( string $modelPath, array $stack = [], ): string { + $discriminatorMetadata = $classMetadata->getDiscriminatorMetadata(); + if (null !== $discriminatorMetadata && $discriminatorMetadata->baseClass == $classMetadata->getClassName()) { + return $this->generateCodeForDiscriminatorClass($classMetadata, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack); + } + $stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1; $code = ''; @@ -127,9 +133,39 @@ private function generateCodeForClass( $code .= $this->generateCodeForField($propertyMetadata, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack); } + if (null !== $discriminatorMetadata) { + $discriminatorFieldPath = $arrayPath.'["'.$discriminatorMetadata->propertyName.'"]'; + $code .= $this->templating->renderAssign($discriminatorFieldPath, \sprintf("'%s'", $discriminatorMetadata->value)); + } + return $this->templating->renderClass($arrayPath, $code); } + /** + * @param list $serializerGroups + * @param array $stack + */ + private function generateCodeForDiscriminatorClass( + ClassMetadata $classMetadata, + ?string $apiVersion, + array $serializerGroups, + string $arrayPath, + string $modelPath, + array $stack = [], + ): string { + $code = ''; + $discriminatorMetadata = $classMetadata->getDiscriminatorMetadata(); + foreach ($discriminatorMetadata->classMap as $class) { + $code .= $this->templating->renderInstanceOfConditional( + $modelPath, + $class, + $this->generateCodeForClass($discriminatorMetadata->getMetadataForClass($class), $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack) + ); + } + + return $code; + } + /** * @param list $serializerGroups * @param array $stack @@ -196,9 +232,12 @@ private function generateCodeForFieldType( case $type instanceof PropertyTypeClass: return $this->generateCodeForClass($type->getClassMetadata(), $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack); - case $type instanceof PropertyTypeArray: + case $type instanceof PropertyTypeIterable: return $this->generateCodeForArray($type, $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack); + case $type instanceof PropertyTypeUnion: + return $this->generateCodeForUnion($type, $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack); + default: throw new \Exception('Unexpected type '.$type::class.' at '.$modelPropertyPath); } @@ -209,7 +248,7 @@ private function generateCodeForFieldType( * @param array $stack */ private function generateCodeForArray( - PropertyTypeArray $type, + PropertyTypeIterable $type, ?string $apiVersion, array $serializerGroups, string $arrayPath, @@ -221,11 +260,11 @@ private function generateCodeForArray( switch ($subType) { case $subType instanceof PropertyTypePrimitive: - case $subType instanceof PropertyTypeArray && self::isArrayForPrimitive($subType): + case $subType instanceof PropertyTypeIterable && self::isArrayForPrimitive($subType): case $subType instanceof PropertyTypeUnknown && $this->configuration->shouldAllowGenericArrays(): return $this->templating->renderArrayAssign($arrayPath, $modelPath); - case $subType instanceof PropertyTypeArray: + case $subType instanceof PropertyTypeIterable: $innerCode = $this->generateCodeForArray($subType, $apiVersion, $serializerGroups, $arrayPath.'['.$index.']', $modelPath.'['.$index.']', $stack); break; @@ -252,14 +291,62 @@ private function generateCodeForArray( return $this->templating->renderLoopArray($arrayPath, $modelPath, $index, $innerCode); } - private static function isArrayForPrimitive(PropertyTypeArray $type): bool + /** + * @param list $serializerGroups + * @param array $stack + */ + private function generateCodeForUnion( + PropertyTypeUnion $subType, + ?string $apiVersion, + array $serializerGroups, + string $arrayPath, + string $modelPath, + array $stack, + ): string { + $code = ''; + + $types = $subType->getTypes(); + $typesWithoutPrimitives = array_filter($types, static function (PropertyType $subType): bool { + return !($subType instanceof PropertyTypePrimitive || $subType instanceof PropertyTypeUnknown); + }); + + $hasPrimitives = \count($types) !== \count($typesWithoutPrimitives); + if ($hasPrimitives) { + $code .= $this->templating->renderPrimitiveConditional( + $modelPath, + $this->templating->renderAssign($arrayPath, $modelPath) + ); + } + + foreach ($typesWithoutPrimitives as $subType) { + switch ($subType::class) { + case PropertyTypeClass::class: + $code .= $this->templating->renderInstanceOfConditional( + $modelPath, + $subType->getClassName(), + $this->generateCodeForFieldType($subType, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack) + ); + break; + case PropertyTypeIterable::class: + $code .= $this->templating->renderArrayConditional( + $modelPath, + $this->generateCodeForArray($subType, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack) + ); + break; + } + } + + return $code; + } + + private static function isArrayForPrimitive(PropertyTypeIterable $type): bool { do { $type = $type->getSubType(); if ($type instanceof PropertyTypePrimitive) { return true; } - } while ($type instanceof PropertyTypeArray); + } while ($type instanceof PropertyTypeIterable); return false; } diff --git a/src/Template/Deserialization.php b/src/Template/Deserialization.php index 4767e94..2a4a7a5 100644 --- a/src/Template/Deserialization.php +++ b/src/Template/Deserialization.php @@ -9,6 +9,25 @@ final class Deserialization { + private const PRIMITIVE_CHECKS = [ + 'null' => 'is_null({{value}})', + 'array' => 'is_array({{value}})', + 'int' => '(string) (int) {{value}} === (string) {{value}}', + 'float' => '(string) (float) {{value}} === (string) {{value}}', + 'bool' => '!is_array({{value}}) && (string) (bool) {{value}} === (string) {{value}}', + 'true' => 'true === {{value}}', + 'false' => 'false === {{value}}', + 'string' => '!is_array({{value}}) && !is_object({{value}})', + ]; + + private const PRIMITIVE_CASTS = [ + 'array' => '{{value}}', + 'int' => '(int) {{value}}', + 'float' => '(float) {{value}}', + 'bool' => '(bool) {{value}}', + 'string' => '(string) {{value}}', + ]; + private const TMPL_FUNCTION = <<<'EOT' render(self::TMPL_DISCRIMINATOR_CONDITIONAL, [ + 'jsonPath' => $jsonPath, + 'typeValue' => $typeValue, + 'code' => $code, + ]); + } + + public function renderPrimitiveConditional(string $phpType, string $jsonPath, string $code, bool $withElseBlock = false): string + { + $typeCheck = self::PRIMITIVE_CHECKS[$phpType] ?? null; + if (null === $typeCheck) { + throw new \InvalidArgumentException(\sprintf('Provided type "%s" but only the following types are supported: %s', $phpType, implode(', ', array_keys(self::PRIMITIVE_CHECKS)))); + } + + $typeConditional = $this->render($typeCheck, [ + 'value' => $jsonPath, + ]); + + return $this->render(self::TMPL_PRIMITIVE_CONDITIONAL, [ + 'typeConditional' => $typeConditional, + 'code' => $code, + 'withElseBlock' => $withElseBlock, + ]); + } + public function renderAssignJsonDataToField(string $modelPath, string $jsonPath): string { return $this->render(self::TMPL_ASSIGN_JSON_DATA_TO_FIELD, [ @@ -193,6 +253,21 @@ public function renderAssignJsonDataToField(string $modelPath, string $jsonPath) ]); } + public function renderAssignJsonDataToFieldWithCast(string $phpType, string $modelPath, string $jsonPath): string + { + $typeCast = self::PRIMITIVE_CASTS[$phpType] ?? null; + if (null !== $typeCast) { + $jsonPath = $this->render($typeCast, [ + 'value' => $jsonPath, + ]); + } + + return $this->render(self::TMPL_ASSIGN_JSON_DATA_TO_FIELD, [ + 'modelPath' => $modelPath, + 'jsonPath' => $jsonPath, + ]); + } + public function renderAssignJsonDataToFieldWithCasting(string $modelPath, string $jsonPath, string $type): string { return $this->render(self::TMPL_ASSIGN_JSON_DATA_TO_FIELD_CASTING, [ @@ -213,15 +288,10 @@ public function renderAssignDateTimeToField(bool $immutable, string $modelPath, } /** - * @param list|string $formats + * @param list $formats */ - public function renderAssignDateTimeFromFormat(bool $immutable, string $modelPath, string $jsonPath, array|string $formats, ?string $timezone = null): string + public function renderAssignDateTimeFromFormat(bool $immutable, string $modelPath, string $jsonPath, array $formats, ?string $timezone = null): string { - if (\is_string($formats)) { - @trigger_error('Passing a string for argument $formats is deprecated, please pass an array of strings instead', \E_USER_DEPRECATED); - $formats = [$formats]; - } - $template = $immutable ? self::TMPL_ASSIGN_DATETIME_IMMUTABLE_FROM_FORMAT : self::TMPL_ASSIGN_DATETIME_FROM_FORMAT; $formats = array_map( static fn (string $f): string => var_export($f, true), diff --git a/src/Template/Serialization.php b/src/Template/Serialization.php index 1e1bcd4..3c94d83 100644 --- a/src/Template/Serialization.php +++ b/src/Template/Serialization.php @@ -16,6 +16,14 @@ function {{functionName}}({{className}} $model, bool $useStdClass = true) { $emptyHashmap = $useStdClass ? new \stdClass() : []; $emptyObject = $useStdClass ? new \stdClass() : []; + $isPrimitive = function (mixed $data) { + if (is_array($data)) { + return false; + } + + return null === $data || is_scalar($data); + }; + {{code}} @@ -38,6 +46,25 @@ function {{functionName}}({{className}} $model, bool $useStdClass = true) {{code}} } +EOT; + + private const TMPL_INSTANCE_OF_CONDITIONAL = <<<'EOT' +if ({{propertyAccessor}} instanceof {{class}}) { + {{code}} +} + +EOT; + + private const TMPL_PRIMITIVE_CONDITIONAL = <<<'EOT' +if ($isPrimitive({{propertyAccessor}})) { + {{code}} +} +EOT; + + private const TMPL_ARRAY_CONDITIONAL = <<<'EOT' +if (is_array({{propertyAccessor}})) { + {{code}} +} EOT; private const TMPL_ASSIGN = <<<'EOT' @@ -124,6 +151,31 @@ public function renderConditional(string $condition, string $code): string ]); } + public function renderInstanceOfConditional(string $propertyAccessor, string $class, string $code): string + { + return $this->render(self::TMPL_INSTANCE_OF_CONDITIONAL, [ + 'propertyAccessor' => $propertyAccessor, + 'class' => $class, + 'code' => $code, + ]); + } + + public function renderPrimitiveConditional(string $propertyAccessor, string $code): string + { + return $this->render(self::TMPL_PRIMITIVE_CONDITIONAL, [ + 'propertyAccessor' => $propertyAccessor, + 'code' => $code, + ]); + } + + public function renderArrayConditional(string $propertyAccessor, string $code): string + { + return $this->render(self::TMPL_ARRAY_CONDITIONAL, [ + 'propertyAccessor' => $propertyAccessor, + 'code' => $code, + ]); + } + public function renderAssign(string $jsonPath, string $propertyAccessor): string { return $this->render(self::TMPL_ASSIGN, [ diff --git a/tests/Fixtures/ComplexUnionTyping.php b/tests/Fixtures/ComplexUnionTyping.php new file mode 100644 index 0000000..0587fd1 --- /dev/null +++ b/tests/Fixtures/ComplexUnionTyping.php @@ -0,0 +1,13 @@ + DiscriminatorComment::class, 'author' => DiscriminatorAuthor::class])] + public DiscriminatorComment|DiscriminatorAuthor $property; +} diff --git a/tests/Fixtures/Discriminator.php b/tests/Fixtures/Discriminator.php new file mode 100644 index 0000000..ef16aed --- /dev/null +++ b/tests/Fixtures/Discriminator.php @@ -0,0 +1,17 @@ +objectType; + } +} diff --git a/tests/Fixtures/DiscriminatorDependency.php b/tests/Fixtures/DiscriminatorDependency.php new file mode 100644 index 0000000..fd3d327 --- /dev/null +++ b/tests/Fixtures/DiscriminatorDependency.php @@ -0,0 +1,10 @@ +apiString); } + public function testDiscriminator(): void + { + $functionName = 'deserialize_Tests_Liip_Serializer_Fixtures_DiscriminatorDependency'; + self::generateDeserializer(self::$metadataBuilder, DiscriminatorDependency::class, $functionName); + + $input = [ + 'discriminator' => [ + 'first_property' => 'first-value', + 'type' => 'first', + ], + ]; + $model = $functionName($input); + + self::assertInstanceOf(DiscriminatorDependency::class, $model); + self::assertInstanceOf(DiscriminatorFirstChild::class, $model->discriminator); + self::assertSame('first-value', $model->discriminator->firstProperty); + + $input = [ + 'discriminator' => [ + 'second_property' => 'second-value', + 'type' => 'second', + ], + ]; + $model = $functionName($input); + + self::assertInstanceOf(DiscriminatorDependency::class, $model); + self::assertInstanceOf(DiscriminatorSecondChild::class, $model->discriminator); + self::assertSame('second-value', $model->discriminator->secondProperty); + } + + public function testComplexUnionDiscriminator(): void + { + if (\PHP_VERSION_ID < 80100) { + self::markTestSkipped('Intersection property types are only supported in PHP 8.1 or newer'); + } + + $functionName = 'deserialize_Tests_Liip_Serializer_Fixtures_ComplexUnionTyping'; + self::generateDeserializer(self::$metadataBuilder, ComplexUnionTyping::class, $functionName); + + $input = [ + 'property' => [ + 'text' => 'my-text', + 'objectType' => 'comment', + ], + ]; + $model = $functionName($input); + + self::assertInstanceOf(ComplexUnionTyping::class, $model); + self::assertInstanceOf(DiscriminatorComment::class, $model->property); + self::assertSame('my-text', $model->property->text); + + $input = [ + 'property' => [ + 'name' => 'my-author', + 'objectType' => 'author', + ], + ]; + $model = $functionName($input); + + self::assertInstanceOf(ComplexUnionTyping::class, $model); + self::assertInstanceOf(DiscriminatorAuthor::class, $model->property); + self::assertSame('my-author', $model->property->name); + } + + /** + * @dataProvider providePrimitiveUnionDiscriminatorCases + */ + public function testPrimitiveUnionDiscriminator(mixed $propertyValue): void + { + $functionName = 'deserialize_Tests_Liip_Serializer_Fixtures_PrimitiveUnionTyping'; + self::generateDeserializer(self::$metadataBuilder, PrimitiveUnionTyping::class, $functionName, ['allow_generic_arrays' => true]); + + $input = ['property' => $propertyValue]; + $model = $functionName($input); + + self::assertInstanceOf(PrimitiveUnionTyping::class, $model); + self::assertSame($propertyValue, $model->property); + } + + public static function providePrimitiveUnionDiscriminatorCases(): iterable + { + return [ + [42], + ['string-value'], + [false], + [0.5], + [['key' => 'value', 'another_key' => 'another_value']], + ]; + } + public function testPostDeserialize(): void { $functionName = 'deserialize_Tests_Liip_Serializer_Fixtures_PostDeserialize'; diff --git a/tests/Unit/SerializerGeneratorTest.php b/tests/Unit/SerializerGeneratorTest.php index 6f926a8..68ac976 100644 --- a/tests/Unit/SerializerGeneratorTest.php +++ b/tests/Unit/SerializerGeneratorTest.php @@ -12,7 +12,12 @@ use Liip\MetadataParser\ModelParser\ReflectionParser; use Tests\Liip\Serializer\Fixtures\AccessorOrder; use Tests\Liip\Serializer\Fixtures\AccessorOrderInherit; +use Tests\Liip\Serializer\Fixtures\ComplexUnionTyping; use Tests\Liip\Serializer\Fixtures\ContainsPrivateProperty; +use Tests\Liip\Serializer\Fixtures\DiscriminatorAuthor; +use Tests\Liip\Serializer\Fixtures\DiscriminatorComment; +use Tests\Liip\Serializer\Fixtures\DiscriminatorDependency; +use Tests\Liip\Serializer\Fixtures\DiscriminatorFirstChild; use Tests\Liip\Serializer\Fixtures\InaccessiblePrivateProperty; use Tests\Liip\Serializer\Fixtures\Inheritance; use Tests\Liip\Serializer\Fixtures\ListModel; @@ -20,6 +25,7 @@ use Tests\Liip\Serializer\Fixtures\MultidimensionalArrayForPrimitive; use Tests\Liip\Serializer\Fixtures\Nested; use Tests\Liip\Serializer\Fixtures\PostDeserialize; +use Tests\Liip\Serializer\Fixtures\PrimitiveUnionTyping; use Tests\Liip\Serializer\Fixtures\PrivateProperty; use Tests\Liip\Serializer\Fixtures\RecursionModel; use Tests\Liip\Serializer\Fixtures\UnknownArraySubType; @@ -343,6 +349,92 @@ public function testVirtualProperties(): void self::assertSame($expected, $data); } + public function testDiscriminator(): void + { + $functionName = 'serialize_Tests_Liip_Serializer_Fixtures_DiscriminatorDependency_2'; + self::generateSerializers(self::$metadataBuilder, DiscriminatorDependency::class, [$functionName]); + + $model = new DiscriminatorDependency(); + $model->discriminator = new DiscriminatorFirstChild(); + $model->discriminator->firstProperty = 'my-value'; + + $expected = [ + 'discriminator' => [ + 'first_property' => 'my-value', + 'type' => 'first', + ], + ]; + $data = $functionName($model); + + self::assertSame($expected, $data); + } + + public function testComplexUnionDiscriminator(): void + { + if (\PHP_VERSION_ID < 80100) { + self::markTestSkipped('Intersection property types are only supported in PHP 8.1 or newer'); + } + + $functionName = 'serialize_Tests_Liip_Serializer_Fixtures_ComplexUnionTyping_2'; + self::generateSerializers(self::$metadataBuilder, ComplexUnionTyping::class, [$functionName]); + + $model = new ComplexUnionTyping(); + $model->property = new DiscriminatorAuthor(); + $model->property->name = 'author-name'; + + $expected = [ + 'property' => [ + 'name' => 'author-name', + 'objectType' => 'author', + ], + ]; + $data = $functionName($model); + + self::assertSame($expected, $data); + + $model = new ComplexUnionTyping(); + $model->property = new DiscriminatorComment(); + $model->property->text = 'comment-text'; + + $expected = [ + 'property' => [ + 'text' => 'comment-text', + 'objectType' => 'comment', + ], + ]; + $data = $functionName($model); + + self::assertSame($expected, $data); + } + + /** + * @dataProvider providePrimitiveUnionDiscriminatorCases + */ + public function testPrimitiveUnionDiscriminator(mixed $propertyValue): void + { + $functionName = 'serialize_Tests_Liip_Serializer_Fixtures_PrimitiveUnionTyping'; + self::generateSerializers(self::$metadataBuilder, PrimitiveUnionTyping::class, [$functionName], [''], [], ['allow_generic_arrays' => true]); + + $model = new PrimitiveUnionTyping(); + $model->property = $propertyValue; + + $expected = ['property' => $propertyValue]; + $data = $functionName($model); + + self::assertSame($expected, $data); + } + + public static function providePrimitiveUnionDiscriminatorCases(): iterable + { + return [ + [42], + ['string-value'], + [false], + [0.5], + [['key' => 'value', 'another_key' => 'another_value']], + ]; + } + /** * Assert that post deserialize is never executed during serialize. */