Skip to content

Commit 798b4cc

Browse files
authored
fix(serializer): denormalize nullable collections of enums/objects (#8381)
Fixes #8379
1 parent 48e8f97 commit 798b4cc

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

src/Serializer/AbstractItemNormalizer.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,11 +1299,19 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value
12991299
}
13001300
}
13011301

1302+
// A nullable collection value type (e.g. an array of a nullable BackedEnum/object) is represented
1303+
// as a NullableType wrapping the real ObjectType, so it must be unwrapped before the instanceof
1304+
// check below; nested CollectionType wrapping is left untouched to keep union/collection detection intact.
1305+
$unwrappedCollectionValueType = $collectionValueType;
1306+
while ($unwrappedCollectionValueType instanceof WrappingTypeInterface && !$unwrappedCollectionValueType instanceof CollectionType) {
1307+
$unwrappedCollectionValueType = $unwrappedCollectionValueType->getWrappedType();
1308+
}
1309+
13021310
if (
1303-
($t instanceof CollectionType && $collectionValueType instanceof ObjectType)
1311+
($t instanceof CollectionType && $unwrappedCollectionValueType instanceof ObjectType)
13041312
|| ($t instanceof LegacyType && $t->isCollection() && null !== $collectionValueType && null !== $collectionValueType->getClassName())
13051313
) {
1306-
$className = $collectionValueType->getClassName();
1314+
$className = $unwrappedCollectionValueType instanceof ObjectType ? $unwrappedCollectionValueType->getClassName() : $collectionValueType->getClassName();
13071315
if (!$this->serializer instanceof DenormalizerInterface) {
13081316
throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
13091317
}

src/Serializer/Tests/AbstractItemNormalizerTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyWithMultipleRequiredConstructorArgs;
4141
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyWithNullableConstructorArg;
4242
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\NonCloneableDummy;
43+
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\NotificationType;
4344
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\PropertyCollectionIriOnly;
4445
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\PropertyCollectionIriOnlyRelation;
4546
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\RelatedDummy;
@@ -1335,6 +1336,57 @@ public function testUnionTypeCollectionDenormalizationAcceptsAnyMember(): void
13351336
$this->assertInstanceOf(Dummy::class, $actual);
13361337
}
13371338

1339+
public function testDenormalizeNullableCollectionOfBackedEnums(): void
1340+
{
1341+
// Nullable collection value types (NullableType wrapping ObjectType/BackedEnumType) only exist
1342+
// in the native TypeInfo system.
1343+
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
1344+
$this->markTestSkipped('Requires symfony/property-info >= 7.1 (native types).');
1345+
}
1346+
1347+
$data = ['notificationType' => ['email']];
1348+
1349+
$propertyNameCollectionFactory = $this->createStub(PropertyNameCollectionFactoryInterface::class);
1350+
$propertyNameCollectionFactory->method('create')->willReturn(new PropertyNameCollection(['notificationType']));
1351+
1352+
// Mirrors what Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor produces for a nullable
1353+
// Doctrine SIMPLE_ARRAY column mapped with "enumType": both the collection and its value type
1354+
// end up wrapped in a NullableType.
1355+
$propertyMetadataFactory = $this->createStub(PropertyMetadataFactoryInterface::class);
1356+
$propertyMetadataFactory->method('create')->willReturn(
1357+
(new ApiProperty())
1358+
->withNativeType(Type::nullable(Type::list(Type::nullable(Type::enum(NotificationType::class)))))
1359+
->withWritable(true)
1360+
);
1361+
1362+
$resourceClassResolver = $this->createStub(ResourceClassResolverInterface::class);
1363+
$resourceClassResolver->method('isResourceClass')->willReturnMap([
1364+
[Dummy::class, true],
1365+
[NotificationType::class, false],
1366+
]);
1367+
$resourceClassResolver->method('getResourceClass')->willReturnMap([
1368+
[null, Dummy::class, Dummy::class],
1369+
]);
1370+
1371+
$propertyAccessor = $this->createMock(PropertyAccessorInterface::class);
1372+
$propertyAccessor->expects($this->once())
1373+
->method('setValue')
1374+
->with($this->isInstanceOf(Dummy::class), 'notificationType', [NotificationType::Email]);
1375+
1376+
$iriConverter = $this->createStub(IriConverterInterface::class);
1377+
1378+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
1379+
$serializerProphecy->willImplement(DenormalizerInterface::class);
1380+
$serializerProphecy->denormalize(['email'], NotificationType::class.'[]', null, Argument::type('array'))->willReturn([NotificationType::Email]);
1381+
1382+
$normalizer = new class($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, null, null, [], null, null) extends AbstractItemNormalizer {};
1383+
$normalizer->setSerializer($serializerProphecy->reveal());
1384+
1385+
$actual = $normalizer->denormalize($data, Dummy::class);
1386+
1387+
$this->assertInstanceOf(Dummy::class, $actual);
1388+
}
1389+
13381390
public function testTypeConfusionGuardPreservesPathAndExpectedType(): void
13391391
{
13401392
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Serializer\Tests\Fixtures\ApiResource;
15+
16+
enum NotificationType: string
17+
{
18+
case Email = 'email';
19+
case Sms = 'sms';
20+
}

0 commit comments

Comments
 (0)