Skip to content

Commit

Permalink
fix(serializer): fix union types on collection denormalization
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Mar 1, 2024
1 parent 42215cf commit bbc750f
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 7 deletions.
23 changes: 17 additions & 6 deletions src/Serializer/AbstractItemNormalizer.php
Expand Up @@ -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;

Check warning on line 524 in src/Serializer/AbstractItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/AbstractItemNormalizer.php#L523-L524

Added lines #L523 - L524 were not covered by tests
}

$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;
Expand Down
16 changes: 15 additions & 1 deletion src/Serializer/Tests/AbstractItemNormalizerTest.php
Expand Up @@ -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);

Expand All @@ -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(),
Expand All @@ -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
Expand Down Expand Up @@ -1220,6 +1229,11 @@ public function testDenormalizeBadKeyType(): void
'bar' => 'baz',
],
],
'relatedDummiesWithUnionTypes' => [
'a' => [
'bar' => 'baz',
],
],
];

$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
Expand Down
7 changes: 7 additions & 0 deletions src/Serializer/Tests/Fixtures/ApiResource/Dummy.php
Expand Up @@ -85,6 +85,12 @@ class Dummy

public Collection|iterable $relatedDummies;

/**
* @phpstan-ignore-next-line
* @var Collection<int|float, RelatedDummy>
*/
public Collection $relatedDummiesWithUnionTypes;

/**
* @var array|null serialize data
*/
Expand All @@ -107,6 +113,7 @@ public static function staticMethod(): void
public function __construct()
{
$this->relatedDummies = new ArrayCollection();
$this->relatedDummiesWithUnionTypes = new ArrayCollection();
}

public function getId()
Expand Down

0 comments on commit bbc750f

Please sign in to comment.