Skip to content

Commit ab7348b

Browse files
committed
[Serializer] Add support for collecting type error during denormalization
1 parent 488a46f commit ab7348b

13 files changed

+388
-20
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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,46 @@
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+
$currentType = get_debug_type($data);
34+
$self->currentType = $currentType;
35+
$self->expectedTypes = $expectedTypes;
36+
$self->path = $path;
37+
$self->useMessageForUser = $useMessageForUser;
38+
39+
return $self;
40+
}
41+
42+
public function getCurrentType(): ?string
43+
{
44+
return $this->currentType;
45+
}
46+
47+
public function getExpectedTypes(): ?array
48+
{
49+
return $this->expectedTypes;
50+
}
51+
52+
public function getPath(): ?string
53+
{
54+
return $this->path;
55+
}
56+
57+
public function canUseMessageForUser(): ?bool
58+
{
59+
return $this->useMessageForUser;
60+
}
1961
}

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

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
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\TypeException;
2122
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
2223
use Symfony\Component\Serializer\Exception\LogicException;
2324
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -239,6 +240,8 @@ private function getAttributeDenormalizationContext(string $class, string $attri
239240
return $context;
240241
}
241242

243+
$context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute : $attribute;
244+
242245
return array_merge($context, $metadata->getDenormalizationContextForGroups($this->getGroups($context)));
243246
}
244247

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

377380
if (null !== $types) {
378-
$value = $this->validateAndDenormalize($types, $resolvedClass, $attribute, $value, $format, $attributeContext);
381+
try {
382+
$value = $this->validateAndDenormalize($types, $resolvedClass, $attribute, $value, $format, $attributeContext);
383+
} catch (NotNormalizableValueException $exception) {
384+
if (null === ($context['not_normalizable_value_exceptions'] ?? null)) {
385+
throw $exception;
386+
}
387+
$context['not_normalizable_value_exceptions'][] = $exception;
388+
continue;
389+
}
379390
}
380391
try {
381392
$this->setAttributeValue($object, $attribute, $value, $format, $attributeContext);
382393
} catch (InvalidArgumentException $e) {
383-
throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
394+
$exception = NotNormalizableValueException::create(
395+
sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type),
396+
$data,
397+
['unknown'],
398+
$context['deserialization_path'] ?? null,
399+
false,
400+
$e->getCode(),
401+
$e
402+
);
403+
if (null === ($context['not_normalizable_value_exceptions'] ?? null)) {
404+
throw $exception;
405+
}
406+
$context['not_normalizable_value_exceptions'][] = $exception;
407+
continue;
384408
}
385409
}
386410

@@ -439,14 +463,24 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
439463
} elseif ('true' === $data || '1' === $data) {
440464
$data = true;
441465
} else {
442-
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data));
466+
throw NotNormalizableValueException::create(
467+
sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data),
468+
$data,
469+
[Type::BUILTIN_TYPE_BOOL],
470+
$context['deserialization_path'] ?? null
471+
);
443472
}
444473
break;
445474
case Type::BUILTIN_TYPE_INT:
446475
if (ctype_digit($data) || '-' === $data[0] && ctype_digit(substr($data, 1))) {
447476
$data = (int) $data;
448477
} else {
449-
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data));
478+
throw NotNormalizableValueException::create(
479+
sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data),
480+
$data,
481+
[Type::BUILTIN_TYPE_INT],
482+
$context['deserialization_path'] ?? null
483+
);
450484
}
451485
break;
452486
case Type::BUILTIN_TYPE_FLOAT:
@@ -462,7 +496,12 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
462496
case '-INF':
463497
return -\INF;
464498
default:
465-
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data));
499+
throw NotNormalizableValueException::create(
500+
sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data),
501+
$data,
502+
[Type::BUILTIN_TYPE_FLOAT],
503+
$context['deserialization_path'] ?? null
504+
);
466505
}
467506

468507
break;
@@ -533,7 +572,12 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
533572
return $data;
534573
}
535574

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)));
575+
throw NotNormalizableValueException::create(
576+
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)),
577+
$data,
578+
array_keys($expectedTypes),
579+
$context['deserialization_path'] ?? null
580+
);
537581
}
538582

539583
/**

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
@@ -30,7 +30,9 @@ interface DenormalizerInterface
3030
* @param mixed $data Data to restore
3131
* @param string $type The expected class to instantiate
3232
* @param string $format Format the given data was extracted from
33-
* @param array $context Options available to the denormalizer
33+
* @param array $context Options available to the denormalizer.
34+
* You can pass a reference to an array with the key "not_normalizable_value_exceptions"
35+
* to collect all NotNormalizableValueException that could occurs.
3436
*
3537
* @return mixed
3638
*

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

Lines changed: 8 additions & 1 deletion
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\LogicException;
1516
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1617
use Symfony\Component\Uid\AbstractUid;
@@ -72,7 +73,13 @@ public function denormalize($data, string $type, string $format = null, array $c
7273
try {
7374
return Ulid::class === $type ? Ulid::fromString($data) : Uuid::fromString($data);
7475
} catch (\InvalidArgumentException $exception) {
75-
throw new NotNormalizableValueException(sprintf('The data is not a valid "%s" string representation.', $type));
76+
throw NotNormalizableValueException::create(
77+
sprintf('The data is not a valid "%s" string representation.', $type),
78+
$data,
79+
[Type::BUILTIN_TYPE_STRING],
80+
$context['deserialization_path'] ?? null,
81+
true
82+
);
7683
}
7784
}
7885

0 commit comments

Comments
 (0)