Skip to content

Commit c6236f3

Browse files
soyukaclaude
andcommitted
fix(serializer): report all missing constructor arguments in instantiateObject
| Q | A | ------------- | --- | Branch? | 4.2 | Tickets | Fixes #7785 | License | MIT | Doc PR | ∅ * Add missing `continue` after collecting a missing constructor argument name, matching Symfony's AbstractNormalizer::instantiateObject() behavior. * Without it, the first missing arg initializes `not_normalizable_value_exceptions` in context, causing subsequent missing args to skip the collection — only the first missing field was reported in MissingConstructorArgumentsException. * Also set `api_platform_input` context flag when re-entering the serializer for input DTO denormalization, so downstream normalizers can detect re-entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b7ce920 commit c6236f3

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

src/Serializer/AbstractItemNormalizer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
392392
} else {
393393
if (!isset($context['not_normalizable_value_exceptions'])) {
394394
$missingConstructorArguments[] = $constructorParameter->name;
395+
continue;
395396
}
396397

397398
$constructorParameterType = 'unknown';

src/Serializer/Tests/AbstractItemNormalizerTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use ApiPlatform\Serializer\AbstractItemNormalizer;
3535
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DtoWithNullValue;
3636
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\Dummy;
37+
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyWithMultipleRequiredConstructorArgs;
3738
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyTableInheritance;
3839
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyTableInheritanceChild;
3940
use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyTableInheritanceRelated;
@@ -51,6 +52,7 @@
5152
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
5253
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
5354
use Symfony\Component\PropertyInfo\Type as LegacyType;
55+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
5456
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
5557
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
5658
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@@ -1937,6 +1939,46 @@ public function testSupportsNormalizationWithApiPlatformOutputContext(): void
19371939
'api_platform_output_class' => 'SomeOtherClass',
19381940
]));
19391941
}
1942+
1943+
public function testDenormalizeReportsAllMissingConstructorArguments(): void
1944+
{
1945+
$data = [];
1946+
1947+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
1948+
$propertyNameCollectionFactoryProphecy->create(DummyWithMultipleRequiredConstructorArgs::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['title', 'rating', 'comment']));
1949+
1950+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
1951+
1952+
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
1953+
$propertyMetadataFactoryProphecy->create(DummyWithMultipleRequiredConstructorArgs::class, 'title', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(true));
1954+
$propertyMetadataFactoryProphecy->create(DummyWithMultipleRequiredConstructorArgs::class, 'rating', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_INT)])->withReadable(true)->withWritable(true));
1955+
$propertyMetadataFactoryProphecy->create(DummyWithMultipleRequiredConstructorArgs::class, 'comment', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(true));
1956+
} else {
1957+
$propertyMetadataFactoryProphecy->create(DummyWithMultipleRequiredConstructorArgs::class, 'title', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withWritable(true));
1958+
$propertyMetadataFactoryProphecy->create(DummyWithMultipleRequiredConstructorArgs::class, 'rating', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType(Type::int())->withReadable(true)->withWritable(true));
1959+
$propertyMetadataFactoryProphecy->create(DummyWithMultipleRequiredConstructorArgs::class, 'comment', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withWritable(true));
1960+
}
1961+
1962+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
1963+
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
1964+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
1965+
$resourceClassResolverProphecy->getResourceClass(null, DummyWithMultipleRequiredConstructorArgs::class)->willReturn(DummyWithMultipleRequiredConstructorArgs::class);
1966+
$resourceClassResolverProphecy->isResourceClass(DummyWithMultipleRequiredConstructorArgs::class)->willReturn(true);
1967+
1968+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
1969+
$serializerProphecy->willImplement(NormalizerInterface::class);
1970+
1971+
$normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {};
1972+
$normalizer->setSerializer($serializerProphecy->reveal());
1973+
1974+
try {
1975+
$normalizer->denormalize($data, DummyWithMultipleRequiredConstructorArgs::class);
1976+
$this->fail('Expected MissingConstructorArgumentsException was not thrown');
1977+
} catch (MissingConstructorArgumentsException $e) {
1978+
$this->assertCount(3, $e->getMissingConstructorArguments(), 'All three missing constructor arguments (title, rating, comment) should be reported, not just the first one.');
1979+
$this->assertSame(['title', 'rating', 'comment'], $e->getMissingConstructorArguments());
1980+
}
1981+
}
19401982
}
19411983

19421984
class ObjectWithBasicProperties
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
use ApiPlatform\Metadata\ApiResource;
17+
18+
#[ApiResource]
19+
final class DummyWithMultipleRequiredConstructorArgs
20+
{
21+
public function __construct(
22+
public string $title,
23+
public int $rating,
24+
public string $comment,
25+
) {
26+
}
27+
}

0 commit comments

Comments
 (0)