Skip to content

Commit c23a29b

Browse files
committed
[Serializer] Add ability to collect denormalization errors
1 parent 49eafee commit c23a29b

13 files changed

+282
-10
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\Exception;
13+
14+
interface AggregableExceptionInterface extends ExceptionInterface
15+
{
16+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\Exception;
13+
14+
final class AggregatedException extends RuntimeException implements AggregableExceptionInterface
15+
{
16+
/**
17+
* @var bool
18+
*/
19+
public $has = false;
20+
21+
/**
22+
* @var array
23+
*/
24+
private $collection;
25+
26+
public function __construct(\Throwable $previous = null)
27+
{
28+
parent::__construct('Errors occurred during the denormalization process', 0, $previous);
29+
}
30+
31+
public function add(string $param, AggregableExceptionInterface $exception): self
32+
{
33+
$this->has = true;
34+
$this->collection[] = [$param, $exception];
35+
36+
return $this;
37+
}
38+
39+
/**
40+
* @return \Generator<string, AggregableExceptionInterface>
41+
*/
42+
public function getExceptions(): \Generator
43+
{
44+
foreach ($this->collection as [$param, $exception]) {
45+
if ($exception instanceof self) {
46+
foreach ($exception->getExceptions() as $nestedParams => $nestedException) {
47+
yield "$param.$nestedParams" => $nestedException;
48+
}
49+
} else {
50+
yield $param => $exception;
51+
}
52+
}
53+
}
54+
}

src/Symfony/Component/Serializer/Exception/MissingConstructorArgumentsException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
/**
1515
* @author Maxime VEBER <maxime.veber@nekland.fr>
1616
*/
17-
class MissingConstructorArgumentsException extends RuntimeException
17+
class MissingConstructorArgumentsException extends RuntimeException implements AggregableExceptionInterface
1818
{
1919
}

src/Symfony/Component/Serializer/Exception/NotNormalizableValueException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
/**
1515
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
1616
*/
17-
class NotNormalizableValueException extends UnexpectedValueException
17+
class NotNormalizableValueException extends UnexpectedValueException implements AggregableExceptionInterface
1818
{
1919
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\Exception;
13+
14+
final class VariadicConstructorArgumentsException extends RuntimeException implements AggregableExceptionInterface
15+
{
16+
}

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

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\Serializer\Exception\AggregableExceptionInterface;
15+
use Symfony\Component\Serializer\Exception\AggregatedException;
1416
use Symfony\Component\Serializer\Exception\CircularReferenceException;
1517
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1618
use Symfony\Component\Serializer\Exception\LogicException;
1719
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
1820
use Symfony\Component\Serializer\Exception\RuntimeException;
21+
use Symfony\Component\Serializer\Exception\VariadicConstructorArgumentsException;
1922
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
2023
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
2124
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -351,6 +354,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
351354

352355
$constructorParameters = $constructor->getParameters();
353356

357+
$aggregatedException = new AggregatedException();
354358
$params = [];
355359
foreach ($constructorParameters as $constructorParameter) {
356360
$paramName = $constructorParameter->name;
@@ -361,12 +365,28 @@ protected function instantiateObject(array &$data, string $class, array &$contex
361365
if ($constructorParameter->isVariadic()) {
362366
if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
363367
if (!\is_array($data[$paramName])) {
364-
throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name));
368+
$e = new VariadicConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name));
369+
if (!($context[self::COLLECT_EXCEPTIONS] ?? false)) {
370+
throw $e;
371+
}
372+
373+
$aggregatedException->add($paramName, $e);
374+
unset($data[$key]);
375+
continue;
365376
}
366377

367378
$variadicParameters = [];
368379
foreach ($data[$paramName] as $parameterData) {
369-
$variadicParameters[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
380+
try {
381+
$variadicParameters[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
382+
} catch (AggregableExceptionInterface $e) {
383+
if (!($context[self::COLLECT_EXCEPTIONS] ?? false)) {
384+
throw $e;
385+
}
386+
387+
$aggregatedException->add($paramName, $e);
388+
continue;
389+
}
370390
}
371391

372392
$params = array_merge($params, $variadicParameters);
@@ -382,7 +402,15 @@ protected function instantiateObject(array &$data, string $class, array &$contex
382402
}
383403

384404
// Don't run set for a parameter passed to the constructor
385-
$params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
405+
try {
406+
$params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
407+
} catch (AggregableExceptionInterface $e) {
408+
if (!($context[self::COLLECT_EXCEPTIONS] ?? false)) {
409+
throw $e;
410+
}
411+
412+
$aggregatedException->add($paramName, $e);
413+
}
386414
unset($data[$key]);
387415
} elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
388416
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
@@ -391,10 +419,20 @@ protected function instantiateObject(array &$data, string $class, array &$contex
391419
} elseif ($constructorParameter->isDefaultValueAvailable()) {
392420
$params[] = $constructorParameter->getDefaultValue();
393421
} else {
394-
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));
422+
$e = new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
423+
424+
if (!($context[self::COLLECT_EXCEPTIONS] ?? false)) {
425+
throw $e;
426+
}
427+
428+
$aggregatedException->add($paramName, $e);
395429
}
396430
}
397431

432+
if (($context[self::COLLECT_EXCEPTIONS] ?? false) && $aggregatedException->has) {
433+
throw $aggregatedException;
434+
}
435+
398436
if ($constructor->isConstructor()) {
399437
return $reflectionClass->newInstanceArgs($params);
400438
} else {

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use Symfony\Component\Serializer\Encoder\CsvEncoder;
1919
use Symfony\Component\Serializer\Encoder\JsonEncoder;
2020
use Symfony\Component\Serializer\Encoder\XmlEncoder;
21+
use Symfony\Component\Serializer\Exception\AggregableExceptionInterface;
22+
use Symfony\Component\Serializer\Exception\AggregatedException;
2123
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
2224
use Symfony\Component\Serializer\Exception\LogicException;
2325
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -311,6 +313,8 @@ public function denormalize($data, string $type, string $format = null, array $c
311313
$object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format);
312314
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
313315

316+
$aggregatedException = new AggregatedException();
317+
314318
foreach ($normalizedData as $attribute => $value) {
315319
if ($this->nameConverter) {
316320
$attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context);
@@ -331,18 +335,31 @@ public function denormalize($data, string $type, string $format = null, array $c
331335
}
332336
}
333337

334-
$value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context);
335338
try {
336-
$this->setAttributeValue($object, $attribute, $value, $format, $context);
337-
} catch (InvalidArgumentException $e) {
338-
throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
339+
$value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context);
340+
try {
341+
$this->setAttributeValue($object, $attribute, $value, $format, $context);
342+
} catch (InvalidArgumentException $e) {
343+
throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
344+
}
345+
} catch (AggregableExceptionInterface $e) {
346+
if (!($context[self::COLLECT_EXCEPTIONS] ?? false)) {
347+
throw $e;
348+
}
349+
350+
$aggregatedException->add($attribute, $e);
351+
continue;
339352
}
340353
}
341354

342355
if (!empty($extraAttributes)) {
343356
throw new ExtraAttributesException($extraAttributes);
344357
}
345358

359+
if (($context[self::COLLECT_EXCEPTIONS] ?? false) && $aggregatedException->has) {
360+
throw $aggregatedException;
361+
}
362+
346363
return $object;
347364
}
348365

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
*/
2525
interface DenormalizerInterface
2626
{
27+
/**
28+
* Collects denormalization exceptions instead of throwing out the first one.
29+
*/
30+
public const COLLECT_EXCEPTIONS = 'collect_exceptions';
31+
2732
/**
2833
* Denormalizes data back into an object of the given class.
2934
*
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\Serializer\Tests\Fixtures;
4+
5+
class CollectErrorsArrayDummy
6+
{
7+
/**
8+
* @var \DateTimeImmutable
9+
*/
10+
public $date;
11+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Symfony\Component\Serializer\Tests\Fixtures;
4+
5+
class CollectErrorsDummy
6+
{
7+
/**
8+
* @var int
9+
*/
10+
public $int;
11+
12+
/**
13+
* @var array
14+
*/
15+
public $array;
16+
17+
/**
18+
* @var CollectErrorsArrayDummy[]
19+
*/
20+
public $arrayOfObjects;
21+
22+
/**
23+
* @var CollectErrorsObjectDummy
24+
*/
25+
public $object;
26+
27+
/**
28+
* @var CollectErrorsVariadicObjectDummy
29+
*/
30+
public $variadicObject;
31+
}

0 commit comments

Comments
 (0)