Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ebe6551

Browse filesBrowse files
committed
[Serializer] Add support for collecting type error during denormalization
1 parent 1629b59 commit ebe6551
Copy full SHA for ebe6551

15 files changed

+465
-23
lines changed

‎src/Symfony/Component/Serializer/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add support of PHP backed enumerations
88
* Add support for serializing empty array as object
99
* Return empty collections as `ArrayObject` from `Serializer::normalize()` when `PRESERVE_EMPTY_OBJECTS` is set
10+
* Add support for collecting type errors during denormalization
1011

1112
5.3
1213
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Exception/NotNormalizableValueException.php
+44Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,48 @@
1616
*/
1717
class NotNormalizableValueException extends UnexpectedValueException
1818
{
19+
private $currentType;
20+
private $expectedTypes;
21+
private $path;
22+
private $useMessageForUser = false;
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 createForUnexpectedDataType(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+
/**
47+
* @return string[]|null
48+
*/
49+
public function getExpectedTypes(): ?array
50+
{
51+
return $this->expectedTypes;
52+
}
53+
54+
public function getPath(): ?string
55+
{
56+
return $this->path;
57+
}
58+
59+
public function canUseMessageForUser(): ?bool
60+
{
61+
return $this->useMessageForUser;
62+
}
1963
}
+37Lines 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/AbstractNormalizer.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
+15-1Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1616
use Symfony\Component\Serializer\Exception\LogicException;
1717
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
18+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1819
use Symfony\Component\Serializer\Exception\RuntimeException;
1920
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
2021
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
@@ -399,7 +400,20 @@ protected function instantiateObject(array &$data, string $class, array &$contex
399400
} elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
400401
$params[] = null;
401402
} else {
402-
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));
403+
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));
405+
}
406+
407+
$exception = NotNormalizableValueException::createForUnexpectedDataType(
408+
sprintf('Failed to create object because the object miss the "%s" property.', $constructorParameter->name),
409+
$data,
410+
['unknown'],
411+
$context['deserialization_path'] ?? null,
412+
true
413+
);
414+
$context['not_normalizable_value_exceptions'][] = $exception;
415+
416+
return $reflectionClass->newInstanceWithoutConstructor();
403417
}
404418
}
405419

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+29-6Lines 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::createForUnexpectedDataType(
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::createForUnexpectedDataType(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::createForUnexpectedDataType(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::createForUnexpectedDataType(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::createForUnexpectedDataType(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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php
+5-2Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,14 @@ 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::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key)), $key, [$builtinType], $subContext['deserialization_path'] ?? null, true);
5558
}
5659

57-
$data[$key] = $this->denormalizer->denormalize($value, $type, $format, $context);
60+
$data[$key] = $this->denormalizer->denormalize($value, $type, $format, $subContext);
5861
}
5962

6063
return $data;

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
+3-2Lines changed: 3 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,13 @@ 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::createForUnexpectedDataType('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.'.', $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
6162
}
6263

6364
try {
6465
return $type::from($data);
6566
} catch (\ValueError $e) {
66-
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
67+
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
6768
}
6869
}
6970

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ 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::createForUnexpectedDataType('The provided "data:" URI is not valid.', $data, ['string'], $context['deserialization_path'] ?? null, true);
100100
}
101101

102102
try {
@@ -113,7 +113,7 @@ public function denormalize($data, string $type, string $format = null, array $c
113113
return new \SplFileObject($data);
114114
}
115115
} catch (\RuntimeException $exception) {
116-
throw new NotNormalizableValueException($exception->getMessage(), $exception->getCode(), $exception);
116+
throw NotNormalizableValueException::createForUnexpectedDataType($exception->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, false, $exception->getCode(), $exception);
117117
}
118118

119119
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php
+4-3Lines changed: 4 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,7 @@ 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::createForUnexpectedDataType('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.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
9091
}
9192

9293
if (null !== $dateTimeFormat) {
@@ -98,13 +99,13 @@ public function denormalize($data, string $type, string $format = null, array $c
9899

99100
$dateTimeErrors = \DateTime::class === $type ? \DateTime::getLastErrors() : \DateTimeImmutable::getLastErrors();
100101

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'])));
102+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
102103
}
103104

104105
try {
105106
return \DateTime::class === $type ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone);
106107
} catch (\Exception $e) {
107-
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
108+
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, false, $e->getCode(), $e);
108109
}
109110
}
110111

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php
+3-2Lines changed: 3 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,13 @@ 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::createForUnexpectedDataType('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
5960
}
6061

6162
try {
6263
return new \DateTimeZone($data);
6364
} catch (\Exception $e) {
64-
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
65+
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
6566
}
6667
}
6768

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
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
*

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php
+4-1Lines changed: 4 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,9 @@ 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::createForUnexpectedDataType('The data is not a valid UUID string representation.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
77+
} catch (\TypeError $exception) {
78+
throw NotNormalizableValueException::createForUnexpectedDataType('The data is not a valid UUID string representation.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
7679
}
7780
}
7881

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.