diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 0fc8fd2c6898c..68bc4d3b71857 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * added `UidNormalizer` * added `FormErrorNormalizer` * added `MimeMessageNormalizer` + * added `DenormalizerInterface::COLLECT_INVARIANT_VIOLATIONS` context option to collect denormalization errors instead of throwing immediately 5.1.0 ----- diff --git a/src/Symfony/Component/Serializer/Exception/InvariantViolationException.php b/src/Symfony/Component/Serializer/Exception/InvariantViolationException.php new file mode 100644 index 0000000000000..c08d4a2043475 --- /dev/null +++ b/src/Symfony/Component/Serializer/Exception/InvariantViolationException.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Exception; + +use Symfony\Component\Serializer\InvariantViolation; + +final class InvariantViolationException extends \RuntimeException implements ExceptionInterface +{ + private $violations; + + /** + * @param array> $violations + */ + public function __construct(array $violations) + { + parent::__construct('Denormalization failed because some values were invalid.'); + + $this->violations = $violations; + } + + /** + * @return array> + */ + public function getViolations(): array + { + return $this->violations; + } + + /** + * @return array> + */ + public function getViolationsNestedIn(string $parentPath): array + { + if ('' === $parentPath) { + throw new \InvalidArgumentException('Parent path cannot be empty.'); + } + + $nestedViolations = []; + + foreach ($this->violations as $path => $violations) { + $path = '' !== $path ? "{$parentPath}.{$path}" : $parentPath; + + $nestedViolations[$path] = $violations; + } + + return $nestedViolations; + } + + /** + * @return array> + */ + public function getViolationMessages(): array + { + $messages = []; + + foreach ($this->violations as $path => $violations) { + foreach ($violations as $violation) { + $messages[$path][] = $violation->getMessage(); + } + } + + return $messages; + } +} diff --git a/src/Symfony/Component/Serializer/InvariantViolation.php b/src/Symfony/Component/Serializer/InvariantViolation.php new file mode 100644 index 0000000000000..a7e8ccdd6de49 --- /dev/null +++ b/src/Symfony/Component/Serializer/InvariantViolation.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer; + +class InvariantViolation +{ + private $normalizedValue; + private $message; + private $exception; + + public function __construct($normalizedValue, string $message, \Throwable $exception) + { + $this->normalizedValue = $normalizedValue; + $this->message = $message; + $this->exception = $exception; + } + + public function getNormalizedValue() + { + return $this->normalizedValue; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getException(): \Throwable + { + return $this->exception; + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index aa1be48cfbaf5..518c69e4ff953 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\ExtraAttributesException; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -311,6 +312,8 @@ public function denormalize($data, string $type, string $format = null, array $c $object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format); $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object); + $invariantViolations = []; + foreach ($normalizedData as $attribute => $value) { if ($this->nameConverter) { $attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context); @@ -331,7 +334,12 @@ public function denormalize($data, string $type, string $format = null, array $c } } - $value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context); + try { + $value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context); + } catch (InvariantViolationException $exception) { + $invariantViolations += $exception->getViolationsNestedIn($attribute); + } + try { $this->setAttributeValue($object, $attribute, $value, $format, $context); } catch (InvalidArgumentException $e) { @@ -339,6 +347,10 @@ public function denormalize($data, string $type, string $format = null, array $c } } + if ([] !== $invariantViolations) { + throw new InvariantViolationException($invariantViolations); + } + if (!empty($extraAttributes)) { throw new ExtraAttributesException($extraAttributes); } diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php index 33892277389e5..0ef844f13b648 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\Serializer\Exception\BadMethodCallException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -51,13 +52,23 @@ public function denormalize($data, string $type, string $format = null, array $c $serializer = $this->serializer; $type = substr($type, 0, -2); + $invariantViolations = []; + $builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null; foreach ($data as $key => $value) { - if (null !== $builtinType && !('is_'.$builtinType)($key)) { - throw new NotNormalizableValueException(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key))); + try { + if (null !== $builtinType && !('is_'.$builtinType)($key)) { + throw new NotNormalizableValueException($key, $value, sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key))); + } + + $data[$key] = $serializer->denormalize($value, $type, $format, $context); + } catch (InvariantViolationException $exception) { + $invariantViolations += $exception->getViolationsNestedIn($key); } + } - $data[$key] = $serializer->denormalize($value, $type, $format, $context); + if ([] !== $invariantViolations) { + throw new InvariantViolationException($invariantViolations); } return $data; diff --git a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php index 979288cf6ace3..aa3ade182d4ac 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php @@ -15,7 +15,9 @@ use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\InvariantViolation; /** * Normalizes an {@see \SplFileInfo} object to a data URI. @@ -90,6 +92,21 @@ public function supportsNormalization($data, string $format = null) * @throws NotNormalizableValueException */ public function denormalize($data, string $type, string $format = null, array $context = []) + { + try { + return $this->doDenormalize($data, $type); + } catch (NotNormalizableValueException $exception) { + if ($context[self::COLLECT_INVARIANT_VIOLATIONS] ?? false) { + $violation = new InvariantViolation($data, 'This value is not a valid data URI.', $exception); + + throw new InvariantViolationException(['' => [$violation]]); + } + + throw $exception; + } + } + + private function doDenormalize($data, string $type) { 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)) { throw new NotNormalizableValueException('The provided "data:" URI is not valid.'); diff --git a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php index de028b34341f9..e38596fcf52b0 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\InvariantViolation; /** * Normalizes an instance of {@see \DateInterval} to an interval string. @@ -70,6 +72,21 @@ public function hasCacheableSupportsMethod(): bool * @throws UnexpectedValueException */ public function denormalize($data, string $type, string $format = null, array $context = []) + { + try { + return $this->doDenormalize($data, $context); + } catch (\Throwable $exception) { + if ($context[self::COLLECT_INVARIANT_VIOLATIONS] ?? false) { + $violation = new InvariantViolation($data, 'This value is not a valid date interval.', $exception); + + throw new InvariantViolationException(['' => [$violation]]); + } + + throw $exception; + } + } + + private function doDenormalize($data, array $context = []) { if (!\is_string($data)) { throw new InvalidArgumentException(sprintf('Data expected to be a string, "%s" given.', get_debug_type($data))); diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index c12e23470a445..342658c5fb971 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\InvariantViolation; /** * Normalizes an object implementing the {@see \DateTimeInterface} to a date string. @@ -77,6 +79,21 @@ public function supportsNormalization($data, string $format = null) * @throws NotNormalizableValueException */ public function denormalize($data, string $type, string $format = null, array $context = []) + { + try { + return $this->doDenormalize($data, $type, $context); + } catch (NotNormalizableValueException $exception) { + if ($context[self::COLLECT_INVARIANT_VIOLATIONS] ?? false) { + $violation = new InvariantViolation($data, 'This value is not a valid date.', $exception); + + throw new InvariantViolationException(['' => [$violation]]); + } + + throw $exception; + } + } + + private function doDenormalize($data, string $type, array $context = []) { $dateTimeFormat = $context[self::FORMAT_KEY] ?? null; $timezone = $this->getTimezone($context); diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php index 73ca7ce3865d4..d71e9a6213c29 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php @@ -26,6 +26,8 @@ */ interface DenormalizerInterface { + const COLLECT_INVARIANT_VIOLATIONS = 'collect_invariant_violations'; + /** * Denormalizes data back into an object of the given class. * diff --git a/src/Symfony/Component/Serializer/Normalizer/MimeMessageNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/MimeMessageNormalizer.php index a1c4f169bbf5e..97a6652af424c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/MimeMessageNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/MimeMessageNormalizer.php @@ -17,6 +17,8 @@ use Symfony\Component\Mime\Header\UnstructuredHeader; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Serializer\Exception\InvariantViolationException; +use Symfony\Component\Serializer\InvariantViolation; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -55,9 +57,19 @@ public function setSerializer(SerializerInterface $serializer) public function normalize($object, ?string $format = null, array $context = []) { if ($object instanceof Headers) { + $invariantViolations = []; + $ret = []; foreach ($this->headersProperty->getValue($object) as $name => $header) { - $ret[$name] = $this->serializer->normalize($header, $format, $context); + try { + $ret[$name] = $this->serializer->normalize($header, $format, $context); + } catch (InvariantViolationException $exception) { + $invariantViolations[''][] = new InvariantViolation($header, "Header {$name} is invalid.", $exception); + } + } + + if ([] !== $invariantViolations) { + throw new InvariantViolationException($invariantViolations); } return $ret; diff --git a/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php index 22b563adc5804..80c5b005744cc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\InvariantViolation; use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Uuid; @@ -42,6 +44,12 @@ public function denormalize($data, string $type, string $format = null, array $c try { return Ulid::class === $type ? Ulid::fromString($data) : Uuid::fromString($data); } catch (\InvalidArgumentException $exception) { + if ($context[self::COLLECT_INVARIANT_VIOLATIONS] ?? false) { + $violation = new InvariantViolation($data, sprintf('This value is not a valid %s URI.', substr(strrchr($type, '\\'), 1)), $exception); + + throw new InvariantViolationException(['' => [$violation]]); + } + throw new NotNormalizableValueException(sprintf('The data is not a valid "%s" string representation.', $type)); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php index a56546c6775b7..02cc954f42d32 100644 --- a/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerAwareTrait; @@ -48,7 +49,13 @@ public function denormalize($data, $class, string $format = null, array $context $data = $this->propertyAccessor->getValue($data, $propertyPath); } - return $this->serializer->denormalize($data, $class, $format, $context); + try { + return $this->serializer->denormalize($data, $class, $format, $context); + } catch (InvariantViolationException $exception) { + $propertyPath = str_replace('][', '.', substr($propertyPath, 1, -1)); + + throw new InvariantViolationException($exception->getViolationsNestedIn($propertyPath)); + } } /** diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 80c430a7d4323..538e604f4b64f 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\InvariantViolationException; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; @@ -30,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; @@ -633,6 +635,92 @@ public function testDeserializeAndUnwrap() $serializer->deserialize($jsonData, __NAMESPACE__.'\Model', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]']) ); } + + public function testCollectDenormalizationErrors() + { + $serializer = new Serializer( + [ + new DateTimeNormalizer(), + new ObjectNormalizer(null, null, null, new PhpDocExtractor()), + new ArrayDenormalizer(), + ], + [ + 'json' => new JsonEncoder(), + ] + ); + + $json = json_encode([ + 'foo' => 'foo', + 'bar' => 'bar', + 'baz' => [ + 'foo' => 'foo', + 'bar' => 'bar', + ], + ]); + + $exception = null; + + try { + $serializer->deserialize($json, Dto::class, 'json', [ + Serializer::COLLECT_INVARIANT_VIOLATIONS => true, + ]); + } catch (InvariantViolationException $exception) { + } + + self::assertNotNull($exception); + self::assertSame([ + 'foo' => ['This value is not a valid date.'], + 'bar' => ['This value is not a valid date.'], + 'baz.foo' => ['This value is not a valid date.'], + 'baz.bar' => ['This value is not a valid date.'], + ], $exception->getViolationMessages()); + } + + public function testCollectDenormalizationErrorsWithUnwrapping() + { + $serializer = new Serializer( + [ + new UnwrappingDenormalizer(new PropertyAccessor()), + new DateTimeNormalizer(), + new ObjectNormalizer(null, null, null, new PhpDocExtractor()), + new ArrayDenormalizer(), + ], + [ + 'json' => new JsonEncoder(), + ] + ); + + $json = json_encode([ + 'wrapped' => [ + 'data' => [ + 'foo' => 'foo', + 'bar' => 'bar', + 'baz' => [ + 'foo' => 'foo', + 'bar' => 'bar', + ], + ], + ], + ]); + + $exception = null; + + try { + $serializer->deserialize($json, Dto::class, 'json', [ + Serializer::COLLECT_INVARIANT_VIOLATIONS => true, + UnwrappingDenormalizer::UNWRAP_PATH => '[wrapped][data]', + ]); + } catch (InvariantViolationException $exception) { + } + + self::assertNotNull($exception); + self::assertSame([ + 'wrapped.data.foo' => ['This value is not a valid date.'], + 'wrapped.data.bar' => ['This value is not a valid date.'], + 'wrapped.data.baz.foo' => ['This value is not a valid date.'], + 'wrapped.data.baz.bar' => ['This value is not a valid date.'], + ], $exception->getViolationMessages()); + } } class Model @@ -706,3 +794,21 @@ interface NormalizerAwareNormalizer extends NormalizerInterface, NormalizerAware interface DenormalizerAwareDenormalizer extends DenormalizerInterface, DenormalizerAwareInterface { } + +class Dto +{ + /** + * @var \DateTimeImmutable + */ + public $foo; + + /** + * @var \DateTimeImmutable + */ + public $bar; + + /** + * @var Dto + */ + public $baz; +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy