Skip to content

Commit 56744dc

Browse files
fix(serializer): fix union types on collection denormalization (#6192)
1 parent 42215cf commit 56744dc

File tree

3 files changed

+40
-7
lines changed

3 files changed

+40
-7
lines changed

src/Serializer/AbstractItemNormalizer.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -514,16 +514,27 @@ protected function denormalizeCollection(string $attribute, ApiProperty $propert
514514
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null);
515515
}
516516

517-
$collectionKeyType = $type->getCollectionKeyTypes()[0] ?? null;
518-
$collectionKeyBuiltinType = $collectionKeyType?->getBuiltinType();
519-
$childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
520517
$values = [];
518+
$childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
519+
$collectionKeyTypes = $type->getCollectionKeyTypes();
521520
foreach ($value as $index => $obj) {
522-
if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
523-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyBuiltinType, \gettype($index)), $index, [$collectionKeyBuiltinType], ($context['deserialization_path'] ?? false) ? sprintf('key(%s)', $context['deserialization_path']) : null, true);
521+
// no typehint provided on collection key
522+
if (!$collectionKeyTypes) {
523+
$values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext);
524+
continue;
524525
}
525526

526-
$values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext);
527+
// validate collection key typehint
528+
foreach ($collectionKeyTypes as $collectionKeyType) {
529+
$collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
530+
if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
531+
continue;
532+
}
533+
534+
$values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext);
535+
continue 2;
536+
}
537+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyTypes[0]->getBuiltinType(), \gettype($index)), $index, [$collectionKeyTypes[0]->getBuiltinType()], ($context['deserialization_path'] ?? false) ? sprintf('key(%s)', $context['deserialization_path']) : null, true);
527538
}
528539

529540
return $values;

src/Serializer/Tests/AbstractItemNormalizerTest.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,21 +878,27 @@ public function testDenormalizeWritableLinks(): void
878878
'name' => 'foo',
879879
'relatedDummy' => ['foo' => 'bar'],
880880
'relatedDummies' => [['bar' => 'baz']],
881+
'relatedDummiesWithUnionTypes' => [0 => ['bar' => 'qux'], 1. => ['bar' => 'quux']],
881882
];
882883

883884
$relatedDummy1 = new RelatedDummy();
884885
$relatedDummy2 = new RelatedDummy();
886+
$relatedDummy3 = new RelatedDummy();
887+
$relatedDummy4 = new RelatedDummy();
885888

886889
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
887-
$propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'relatedDummy', 'relatedDummies']));
890+
$propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'relatedDummy', 'relatedDummies', 'relatedDummiesWithUnionTypes']));
888891

889892
$relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class);
890893
$relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType);
894+
$relatedDummiesWithUnionTypesIntType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType);
895+
$relatedDummiesWithUnionTypesFloatType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_FLOAT), $relatedDummyType);
891896

892897
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
893898
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true));
894899
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true));
895900
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true));
901+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummiesWithUnionTypes', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesWithUnionTypesIntType, $relatedDummiesWithUnionTypesFloatType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true));
896902

897903
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
898904

@@ -908,6 +914,8 @@ public function testDenormalizeWritableLinks(): void
908914
$serializerProphecy->willImplement(DenormalizerInterface::class);
909915
$serializerProphecy->denormalize(['foo' => 'bar'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy1);
910916
$serializerProphecy->denormalize(['bar' => 'baz'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy2);
917+
$serializerProphecy->denormalize(['bar' => 'qux'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy3);
918+
$serializerProphecy->denormalize(['bar' => 'quux'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy4);
911919

912920
$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
913921
$propertyNameCollectionFactoryProphecy->reveal(),
@@ -930,6 +938,7 @@ public function testDenormalizeWritableLinks(): void
930938
$propertyAccessorProphecy->setValue($actual, 'name', 'foo')->shouldHaveBeenCalled();
931939
$propertyAccessorProphecy->setValue($actual, 'relatedDummy', $relatedDummy1)->shouldHaveBeenCalled();
932940
$propertyAccessorProphecy->setValue($actual, 'relatedDummies', [$relatedDummy2])->shouldHaveBeenCalled();
941+
$propertyAccessorProphecy->setValue($actual, 'relatedDummiesWithUnionTypes', [0 => $relatedDummy3, 1. => $relatedDummy4])->shouldHaveBeenCalled();
933942
}
934943

935944
public function testBadRelationType(): void
@@ -1220,6 +1229,11 @@ public function testDenormalizeBadKeyType(): void
12201229
'bar' => 'baz',
12211230
],
12221231
],
1232+
'relatedDummiesWithUnionTypes' => [
1233+
'a' => [
1234+
'bar' => 'baz',
1235+
],
1236+
],
12231237
];
12241238

12251239
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);

src/Serializer/Tests/Fixtures/ApiResource/Dummy.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ class Dummy
8585

8686
public Collection|iterable $relatedDummies;
8787

88+
/**
89+
* @phpstan-ignore-next-line
90+
*
91+
* @var Collection<int|float, RelatedDummy>
92+
*/
93+
public Collection $relatedDummiesWithUnionTypes;
94+
8895
/**
8996
* @var array|null serialize data
9097
*/
@@ -107,6 +114,7 @@ public static function staticMethod(): void
107114
public function __construct()
108115
{
109116
$this->relatedDummies = new ArrayCollection();
117+
$this->relatedDummiesWithUnionTypes = new ArrayCollection();
110118
}
111119

112120
public function getId()

0 commit comments

Comments
 (0)