diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index b74a97a78a..3b4f395315 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -514,16 +514,27 @@ protected function denormalizeCollection(string $attribute, ApiProperty $propert 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); } - $collectionKeyType = $type->getCollectionKeyTypes()[0] ?? null; - $collectionKeyBuiltinType = $collectionKeyType?->getBuiltinType(); - $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); $values = []; + $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); + $collectionKeyTypes = $type->getCollectionKeyTypes(); foreach ($value as $index => $obj) { - if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) { - 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); + // no typehint provided on collection key + if (!$collectionKeyTypes) { + $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext); + continue; } - $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext); + // validate collection key typehint + foreach ($collectionKeyTypes as $collectionKeyType) { + $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType(); + if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) { + continue; + } + + $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext); + continue 2; + } + 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); } return $values; diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 7efd665e02..58a3a7a88a 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -878,21 +878,27 @@ public function testDenormalizeWritableLinks(): void 'name' => 'foo', 'relatedDummy' => ['foo' => 'bar'], 'relatedDummies' => [['bar' => 'baz']], + 'relatedDummiesWithUnionTypes' => [0 => ['bar' => 'qux'], 1. => ['bar' => 'quux']], ]; $relatedDummy1 = new RelatedDummy(); $relatedDummy2 = new RelatedDummy(); + $relatedDummy3 = new RelatedDummy(); + $relatedDummy4 = new RelatedDummy(); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'relatedDummy', 'relatedDummies'])); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'relatedDummy', 'relatedDummies', 'relatedDummiesWithUnionTypes'])); $relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); $relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); + $relatedDummiesWithUnionTypesIntType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); + $relatedDummiesWithUnionTypesFloatType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_FLOAT), $relatedDummyType); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummiesWithUnionTypes', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesWithUnionTypesIntType, $relatedDummiesWithUnionTypesFloatType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -908,6 +914,8 @@ public function testDenormalizeWritableLinks(): void $serializerProphecy->willImplement(DenormalizerInterface::class); $serializerProphecy->denormalize(['foo' => 'bar'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy1); $serializerProphecy->denormalize(['bar' => 'baz'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy2); + $serializerProphecy->denormalize(['bar' => 'qux'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy3); + $serializerProphecy->denormalize(['bar' => 'quux'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy4); $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ $propertyNameCollectionFactoryProphecy->reveal(), @@ -930,6 +938,7 @@ public function testDenormalizeWritableLinks(): void $propertyAccessorProphecy->setValue($actual, 'name', 'foo')->shouldHaveBeenCalled(); $propertyAccessorProphecy->setValue($actual, 'relatedDummy', $relatedDummy1)->shouldHaveBeenCalled(); $propertyAccessorProphecy->setValue($actual, 'relatedDummies', [$relatedDummy2])->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'relatedDummiesWithUnionTypes', [0 => $relatedDummy3, 1. => $relatedDummy4])->shouldHaveBeenCalled(); } public function testBadRelationType(): void @@ -1220,6 +1229,11 @@ public function testDenormalizeBadKeyType(): void 'bar' => 'baz', ], ], + 'relatedDummiesWithUnionTypes' => [ + 'a' => [ + 'bar' => 'baz', + ], + ], ]; $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); diff --git a/src/Serializer/Tests/Fixtures/ApiResource/Dummy.php b/src/Serializer/Tests/Fixtures/ApiResource/Dummy.php index ea62a525ba..a85f785d10 100644 --- a/src/Serializer/Tests/Fixtures/ApiResource/Dummy.php +++ b/src/Serializer/Tests/Fixtures/ApiResource/Dummy.php @@ -85,6 +85,13 @@ class Dummy public Collection|iterable $relatedDummies; + /** + * @phpstan-ignore-next-line + * + * @var Collection + */ + public Collection $relatedDummiesWithUnionTypes; + /** * @var array|null serialize data */ @@ -107,6 +114,7 @@ public static function staticMethod(): void public function __construct() { $this->relatedDummies = new ArrayCollection(); + $this->relatedDummiesWithUnionTypes = new ArrayCollection(); } public function getId()