Skip to content

Commit ffbb577

Browse files
committed
[Serializer] Add support for collecting type error during denormalization
1 parent dabd127 commit ffbb577

14 files changed

+497
-23
lines changed

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add support of PHP backed enumerations
88
* Add support for serializing empty array as object
9+
* Add support for collecting type error during denormalization
910

1011
5.3
1112
---

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,45 @@
1616
*/
1717
class NotNormalizableValueException extends UnexpectedValueException
1818
{
19+
private $currentType = null;
20+
private $expectedTypes = null;
21+
private $path = null;
22+
private $useMessageForUser = null;
23+
24+
/**
25+
* @param bool $useMessageForUser If the message passed to this exception is something that can be shown
26+
* safely to your user. In other words, avoid catching other exceptions and
27+
* passing their message directly to this class.
28+
*/
29+
public static function create(string $message, $data, array $expectedTypes, string $path = null, bool $useMessageForUser = false, int $code = 0, \Throwable $previous = null): self
30+
{
31+
$self = new self($message, $code, $previous);
32+
33+
$self->currentType = get_debug_type($data);
34+
$self->expectedTypes = $expectedTypes;
35+
$self->path = $path;
36+
$self->useMessageForUser = $useMessageForUser;
37+
38+
return $self;
39+
}
40+
41+
public function getCurrentType(): ?string
42+
{
43+
return $this->currentType;
44+
}
45+
46+
public function getExpectedTypes(): ?array
47+
{
48+
return $this->expectedTypes;
49+
}
50+
51+
public function getPath(): ?string
52+
{
53+
return $this->path;
54+
}
55+
56+
public function canUseMessageForUser(): ?bool
57+
{
58+
return $this->useMessageForUser;
59+
}
1960
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
/**
15+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
16+
*/
17+
class PartialDenormalizationException extends UnexpectedValueException
18+
{
19+
private $data;
20+
private $errors;
21+
22+
public function __construct($data, array $errors)
23+
{
24+
$this->data = $data;
25+
$this->errors = $errors;
26+
}
27+
28+
public function getData()
29+
{
30+
return $this->data;
31+
}
32+
33+
public function getErrors(): array
34+
{
35+
return $this->errors;
36+
}
37+
}

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ private function getAttributeDenormalizationContext(string $class, string $attri
239239
return $context;
240240
}
241241

242+
$context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute : $attribute;
243+
242244
return array_merge($context, $metadata->getDenormalizationContextForGroups($this->getGroups($context)));
243245
}
244246

@@ -375,12 +377,33 @@ public function denormalize($data, string $type, string $format = null, array $c
375377
$types = $this->getTypes($resolvedClass, $attribute);
376378

377379
if (null !== $types) {
378-
$value = $this->validateAndDenormalize($types, $resolvedClass, $attribute, $value, $format, $attributeContext);
380+
try {
381+
$value = $this->validateAndDenormalize($types, $resolvedClass, $attribute, $value, $format, $attributeContext);
382+
} catch (NotNormalizableValueException $exception) {
383+
if (isset($context['not_normalizable_value_exceptions'])) {
384+
$context['not_normalizable_value_exceptions'][] = $exception;
385+
continue;
386+
}
387+
throw $exception;
388+
}
379389
}
380390
try {
381391
$this->setAttributeValue($object, $attribute, $value, $format, $attributeContext);
382392
} catch (InvalidArgumentException $e) {
383-
throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
393+
$exception = NotNormalizableValueException::create(
394+
sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type),
395+
$data,
396+
['unknown'],
397+
$context['deserialization_path'] ?? null,
398+
false,
399+
$e->getCode(),
400+
$e
401+
);
402+
if (isset($context['not_normalizable_value_exceptions'])) {
403+
$context['not_normalizable_value_exceptions'][] = $exception;
404+
continue;
405+
}
406+
throw $exception;
384407
}
385408
}
386409

@@ -439,14 +462,14 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
439462
} elseif ('true' === $data || '1' === $data) {
440463
$data = true;
441464
} else {
442-
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data));
465+
throw NotNormalizableValueException::create(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null);
443466
}
444467
break;
445468
case Type::BUILTIN_TYPE_INT:
446469
if (ctype_digit($data) || '-' === $data[0] && ctype_digit(substr($data, 1))) {
447470
$data = (int) $data;
448471
} else {
449-
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data));
472+
throw NotNormalizableValueException::create(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null);
450473
}
451474
break;
452475
case Type::BUILTIN_TYPE_FLOAT:
@@ -462,7 +485,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
462485
case '-INF':
463486
return -\INF;
464487
default:
465-
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data));
488+
throw NotNormalizableValueException::create(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null);
466489
}
467490

468491
break;
@@ -533,7 +556,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
533556
return $data;
534557
}
535558

536-
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)));
559+
throw NotNormalizableValueException::create(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? null);
537560
}
538561

539562
/**

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,20 @@ public function denormalize($data, string $type, string $format = null, array $c
5050

5151
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
5252
foreach ($data as $key => $value) {
53+
$subContext = $context;
54+
$subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]";
55+
5356
if (null !== $builtinType && !('is_'.$builtinType)($key)) {
54-
throw new NotNormalizableValueException(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key)));
57+
throw NotNormalizableValueException::create(
58+
sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key)),
59+
$key,
60+
[$builtinType],
61+
$subContext['deserialization_path'] ?? null,
62+
true
63+
);
5564
}
5665

57-
$data[$key] = $this->denormalizer->denormalize($value, $type, $format, $context);
66+
$data[$key] = $this->denormalizer->denormalize($value, $type, $format, $subContext);
5867
}
5968

6069
return $data;

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\PropertyInfo\Type;
1415
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1516
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1617

@@ -57,13 +58,27 @@ public function denormalize($data, $type, $format = null, array $context = [])
5758
}
5859

5960
if (!\is_int($data) && !\is_string($data)) {
60-
throw new NotNormalizableValueException('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.');
61+
throw NotNormalizableValueException::create(
62+
'The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.',
63+
$data,
64+
[Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING],
65+
$context['deserialization_path'] ?? null,
66+
true
67+
);
6168
}
6269

6370
try {
6471
return $type::from($data);
6572
} catch (\ValueError $e) {
66-
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
73+
throw NotNormalizableValueException::create(
74+
$e->getMessage(),
75+
$data,
76+
[Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING],
77+
$context['deserialization_path'] ?? null,
78+
true,
79+
$e->getCode(),
80+
$e
81+
);
6782
}
6883
}
6984

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,13 @@ public function supportsNormalization($data, string $format = null)
9696
public function denormalize($data, string $type, string $format = null, array $context = [])
9797
{
9898
if (!preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) {
99-
throw new NotNormalizableValueException('The provided "data:" URI is not valid.');
99+
throw NotNormalizableValueException::create(
100+
'The provided "data:" URI is not valid.',
101+
$data,
102+
['string'],
103+
$context['deserialization_path'] ?? null,
104+
true
105+
);
100106
}
101107

102108
try {
@@ -113,7 +119,15 @@ public function denormalize($data, string $type, string $format = null, array $c
113119
return new \SplFileObject($data);
114120
}
115121
} catch (\RuntimeException $exception) {
116-
throw new NotNormalizableValueException($exception->getMessage(), $exception->getCode(), $exception);
122+
throw NotNormalizableValueException::create(
123+
$exception->getMessage(),
124+
$data,
125+
['string'],
126+
$context['deserialization_path'] ?? null,
127+
false,
128+
$exception->getCode(),
129+
$exception
130+
);
117131
}
118132

119133
throw new InvalidArgumentException(sprintf('The class parameter "%s" is not supported. It must be one of "SplFileInfo", "SplFileObject" or "Symfony\Component\HttpFoundation\File\File".', $type));

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\PropertyInfo\Type;
1415
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1516
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1617

@@ -86,7 +87,13 @@ public function denormalize($data, string $type, string $format = null, array $c
8687
$timezone = $this->getTimezone($context);
8788

8889
if (null === $data || (\is_string($data) && '' === trim($data))) {
89-
throw new NotNormalizableValueException('The data is either an empty string or null, you should pass a string that can be parsed with the passed format or a valid DateTime string.');
90+
throw NotNormalizableValueException::create(
91+
'The data is either an empty string or null, you should pass a string that can be parsed with the passed format or a valid DateTime string.',
92+
$data,
93+
[Type::BUILTIN_TYPE_STRING],
94+
$context['deserialization_path'] ?? null,
95+
true
96+
);
9097
}
9198

9299
if (null !== $dateTimeFormat) {
@@ -98,13 +105,27 @@ public function denormalize($data, string $type, string $format = null, array $c
98105

99106
$dateTimeErrors = \DateTime::class === $type ? \DateTime::getLastErrors() : \DateTimeImmutable::getLastErrors();
100107

101-
throw new NotNormalizableValueException(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])));
108+
throw NotNormalizableValueException::create(
109+
sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])),
110+
$data,
111+
[Type::BUILTIN_TYPE_STRING],
112+
$context['deserialization_path'] ?? null,
113+
true
114+
);
102115
}
103116

104117
try {
105118
return \DateTime::class === $type ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone);
106119
} catch (\Exception $e) {
107-
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
120+
throw NotNormalizableValueException::create(
121+
$e->getMessage(),
122+
$data,
123+
[Type::BUILTIN_TYPE_STRING],
124+
$context['deserialization_path'] ?? null,
125+
true,
126+
$e->getCode(),
127+
$e
128+
);
108129
}
109130
}
110131

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\PropertyInfo\Type;
1415
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1516
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1617

@@ -55,13 +56,27 @@ public function supportsNormalization($data, string $format = null)
5556
public function denormalize($data, string $type, string $format = null, array $context = [])
5657
{
5758
if ('' === $data || null === $data) {
58-
throw new NotNormalizableValueException('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.');
59+
throw NotNormalizableValueException::create(
60+
'The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.',
61+
$data,
62+
[Type::BUILTIN_TYPE_STRING],
63+
$context['deserialization_path'] ?? null,
64+
true
65+
);
5966
}
6067

6168
try {
6269
return new \DateTimeZone($data);
6370
} catch (\Exception $e) {
64-
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
71+
throw NotNormalizableValueException::create(
72+
$e->getMessage(),
73+
$data,
74+
[Type::BUILTIN_TYPE_STRING],
75+
$context['deserialization_path'] ?? null,
76+
true,
77+
$e->getCode(),
78+
$e
79+
);
6580
}
6681
}
6782

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
*/
2525
interface DenormalizerInterface
2626
{
27+
public const COLLECT_DENORMALIZATION_ERRORS = 'collect_denormalization_errors';
28+
2729
/**
2830
* Denormalizes data back into an object of the given class.
2931
*
3032
* @param mixed $data Data to restore
3133
* @param string $type The expected class to instantiate
3234
* @param string $format Format the given data was extracted from
33-
* @param array $context Options available to the denormalizer
35+
* @param array $context options available to the denormalizer
3436
*
3537
* @return mixed
3638
*

0 commit comments

Comments
 (0)