8000 [Serializer] Fix collecting only first missing constructor argument · symfony/symfony@0f398ce · GitHub
[go: up one dir, main page]

Skip to content

Commit 0f398ce

Browse files
HypeMCnicolas-grekas
authored andcommitted
[Serializer] Fix collecting only first missing constructor argument
1 parent 7aaa92a commit 0f398ce

File tree

5 files changed

+161
-19
lines changed

5 files changed

+161
-19
lines changed

src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
348348
}
349349

350350
$constructorParameters = $constructor->getParameters();
351-
351+
$missingConstructorArguments = [];
352352
$params = [];
353353
foreach ($constructorParameters as $constructorParameter) {
354354
$paramName = $constructorParameter->name;
@@ -401,7 +401,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex
401401
$params[] = null;
402402
} else {
403403
if (!isset($context['not_normalizable_value_exceptions'])) {
404-
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name), 0, null, [$constructorParameter->name]);
404+
$missingConstructorArguments[] = 8000 $constructorParameter->name;
405+
continue;
405406
}
406407

407408
$exception = NotNormalizableValueException::createForUnexpectedDataType(
@@ -412,24 +413,26 @@ protected function instantiateObject(array &$data, string $class, array &$contex
412413
true
413414
);
414415
$context['not_normalizable_value_exceptions'][] = $exception;
415-
416-
return $reflectionClass->newInstanceWithoutConstructor();
417416
}
418417
}
419418

420-
if ($constructor->isConstructor()) {
421-
try {
422-
return $reflectionClass->newInstanceArgs($params);
423-
} catch (\TypeError $th) {
424-
if (!isset($context['not_normalizable_value_exceptions'])) {
425- 8000
throw $th;
426-
}
419+
if ($missingConstructorArguments) {
420+
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$%s".', $class, implode('", "$', $missingConstructorArguments)), 0, null, $missingConstructorArguments);
421+
}
427422

428-
return $reflectionClass->newInstanceWithoutConstructor();
429-
}
430-
} else {
423+
if (!$constructor->isConstructor()) {
431424
return $constructor->invokeArgs(null, $params);
432425
}
426+
427+
try {
428+
return $reflectionClass->newInstanceArgs($params);
429+
} catch (\TypeError $e) {
430+
if (!isset($context['not_normalizable_value_exceptions'])) {
431+
throw $e;
432+
}
433+
434+
return $reflectionClass->newInstanceWithoutConstructor();
435+
}
433436
}
434437

435438
unset($context['has_constructor']);

src/Symfony/Component/Serializer/Tests/Fixtures/Php80WithPromotedTypedConstructor.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
final class Php80WithPromotedTypedConstructor
1515
{
16-
public function __construct(public bool $bool)
17-
{
16+
public function __construct(
17+
public bool $bool,
18+
public string $string,
19+
public int $int,
20+
) {
1821
}
1922
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
final class WithTypedConstructor
15+
{
16+
/**
17+
* @var string
18+
*/
19+
public $string;
20+
/**
21+
* @var bool
22+
*/
23+
public $bool;
24+
/**
25+
* @var int
26+
*/
27+
public $int;
28+
29+
public function __construct(string $string, bool $bool, int $int)
30+
{
31+
$this->string = $string;
32+
$this->bool = $bool;
33+
$this->int = $int;
34+
}
35+
}

src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,30 @@ public function testConstructorWithMissingData()
6262
];
6363

6464
$normalizer = $this->getDenormalizerForConstructArguments();
65+
try {
66+
$normalizer->denormalize($data, ConstructorArgumentsObject::class);
67+
self::fail(sprintf('Failed asserting that exception of type "%s" is thrown.', MissingConstructorArgumentsException::class));
68+
} catch (MissingConstructorArgumentsException $e) {
69+
self::assertSame(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$bar", "$baz".', ConstructorArgumentsObject::class), $e->getMessage());
70+
self::assertSame(['bar', 'baz'], $e->getMissingConstructorArguments());
71+
}
72+
}
73+
74+
public function testExceptionsAreCollectedForConstructorWithMissingData()
75+
{
76+
$data = [
77+
'foo' => 10,
78+
];
79+
80+
$exceptions = [];
81+
82+
$normalizer = $this->getDenormalizerForConstructArguments();
83+
$normalizer->denormalize($data, ConstructorArgumentsObject::class, null, [
84+
'not_normalizable_value_exceptions' => &$exceptions,
85+
]);
6586

66-
$this->expectException(MissingConstructorArgumentsException::class);
67-
$this->expectExceptionMessage('Cannot create an instance of "'.ConstructorArgumentsObject::class.'" from serialized data because its constructor requires parameter "bar" to be present.');
68-
$normalizer->denormalize($data, ConstructorArgumentsObject::class);
87+
self::assertCount(2, $exceptions);
88+
self::assertSame('Failed to create object because the class misses the "bar" property.', $exceptions[0]->getMessage());
89+
self::assertSame('Failed to create object because the class misses the "baz" property.', $exceptions[1]->getMessage());
6990
}
7091
}

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
use Symfony\Component\Serializer\Tests\Fixtures\Php74Full;
6868
use Symfony\Component\Serializer\Tests\Fixtures\Php80WithPromotedTypedConstructor;
6969
use Symfony\Component\Serializer\Tests\Fixtures\TraversableDummy;
70+
use Symfony\Component\Serializer\Tests\Fixtures\WithTypedConstructor;
7071
use Symfony\Component\Serializer\Tests\Normalizer\TestDenormalizer;
7172
use Symfony\Component\Serializer\Tests\Normalizer\TestNormalizer;
7273

@@ -1196,6 +1197,85 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa
11961197
'useMessageForUser' => false,
11971198
'message' => 'The type of the "bool" attribute for class "Symfony\\Component\\Serializer\\Tests\\Fixtures\\Php80WithPromotedTypedConstructor" must be one of "bool" ("string" given).',
11981199
],
1200+
[
1201+
'currentType' => 'array',
1202+
'expectedTypes' => [
1203+
'unknown',
1204+
],
1205+
'path' => null,
1206+
'useMessageForUser' => true,
1207+
'message' => 'Failed to create object because the class misses the "string" property.',
1208+
],
1209+
[
1210+
'currentType' => 'array',
1211+
'expectedTypes' => [
1212+
'unknown',
1213+
],
1214+
'path' => null,
1215+
'useMessageForUser' => true,
1216+
'message' => 'Failed to create object because the class misses the "int" property.',
1217+
],
1218+
];
1219+
1220+
$this->assertSame($expected, $exceptionsAsArray);
1221+
}
1222+
1223+
public function testCollectDenormalizationErrorsWithInvalidConstructorTypes()
1224+
{
1225+
$json = '{"string": "some string", "bool": "bool", "int": true}';
1226+
1227+
$extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]);
1228+
1229+
$serializer = new Serializer(
1230+
[new ObjectNormalizer(null, null, null, $extractor)],
1231+
['json' => new JsonEncoder()]
1232+
);
1233+
1234+
try {
1235+
$serializer->deserialize($json, WithTypedConstructor::class, 'json', [
1236+
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
1237+
]);
1238+
1239+
$this->fail();
1240+
} catch (\Throwable $th) {
1241+
$this->assertInstanceOf(PartialDenormalizationException::class, $th);
1242+
}
1243+
1244+
$this->assertInstanceOf(WithTypedConstructor::class, $object = $th->getData());
1245+
1246+
$this->assertSame('some string', $object->string);
1247+
$this->assertTrue($object->bool);
1248+
$this->assertSame(1, $object->int);
1249+
1250+
$exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array {
1251+
return [
1252+
'currentType' => $e->getCurrentType(),
1253+
'expectedTypes' => $e->getExpectedTypes(),
1254+
'path' => $e->getPath(),
1255+
'useMessageForUser' => $e->canUseMessageForUser(),
1256+
'message' => $e->getMessage(),
1257+
];
1258+
}, $th->getErrors());
1259+
1260+
$expected = [
1261+
[
1262+
'currentType' => 'string',
1263+
'expectedTypes' => [
1264+
0 => 'bool',
1265+
],
1266+
'path' => 'bool',
1267+
'useMessageForUser' => false,
1268+
'message' => 'The type of the "bool" attribute for class "Symfony\Component\Serializer\Tests\Fixtures\WithTypedConstructor" must be one of "bool" ("string" given).',
1269+
],
1270+
[
1271+
'currentType' => 'bool',
1272+
'expectedTypes' => [
1273+
0 => 'int',
1274+
],
1275+
'path' => 'int',
1276+
'useMessageForUser' => false,
1277+
'message' => 'The type of the "int" attribute for class "Symfony\Component\Serializer\Tests\Fixtures\WithTypedConstructor" must be one of "int" ("bool" given).',
1278+
],
11991279
];
12001280

12011281
$this->assertSame($expected, $exceptionsAsArray);

0 commit comments

Comments
 (0)
0