diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php b/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php index 344eea7cc743f..78d293d8d3350 100644 --- a/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php +++ b/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php @@ -17,6 +17,8 @@ use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * This normalizer is only used in Debug/Dev/Messenger contexts. @@ -50,6 +52,10 @@ public function normalize($object, $format = null, array $context = []) $normalized['status'] = $status; } + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($normalized); + } + return $normalized; } diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 8ccc9f4c7db1a..709b5dd8c09da 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -15,6 +15,7 @@ CHANGELOG * added `FormErrorNormalizer` * added `MimeMessageNormalizer` * serializer mapping can be configured using php attributes + * added `SerializerInterface::RETURN_RESULT` context option to collect (de)normalization errors instead of throwing immediately 5.1.0 ----- diff --git a/src/Symfony/Component/Serializer/InvariantViolation.php b/src/Symfony/Component/Serializer/InvariantViolation.php new file mode 100644 index 0000000000000..50f000649f850 --- /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; + +final class InvariantViolation +{ + private $normalizedValue; + private $message; + private $throwable; + + public function __construct($normalizedValue, string $message, ?\Throwable $throwable = null) + { + $this->normalizedValue = $normalizedValue; + $this->message = $message; + $this->throwable = $throwable; + } + + public function getNormalizedValue() + { + return $this->normalizedValue; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getThrowable(): ?\Throwable + { + return $this->throwable; + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 4a03ab851a3a2..a9fd856d116aa 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -19,8 +19,10 @@ use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Result\NormalizationResult; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerAwareTrait; +use Symfony\Component\Serializer\SerializerInterface; /** * Normalizer implementation. @@ -210,10 +212,22 @@ protected function handleCircularReference(object $object, string $format = null { $circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]; if ($circularReferenceHandler) { - return $circularReferenceHandler($object, $format, $context); + $result = $circularReferenceHandler($object, $format, $context); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; + } + + $exception = new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', get_debug_type($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT])); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::failure(['' => $exception]); } - throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', get_debug_type($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT])); + throw $exception; } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 36099aa385ab7..864da0b06bf31 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -22,11 +22,15 @@ use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; +use Symfony\Component\Serializer\InvariantViolation; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * Base class for a normalizer dealing with objects. @@ -207,6 +211,10 @@ public function normalize($object, string $format = null, array $context = []) return new \ArrayObject(); } + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($data); + } + return $data; } @@ -311,6 +319,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); @@ -332,6 +342,17 @@ public function denormalize($data, string $type, string $format = null, array $c } $value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context); + + if ($value instanceof DenormalizationResult) { + if (!$value->isSucessful()) { + $invariantViolations += $value->getInvariantViolationsNestedIn($attribute); + + continue; + } + + $value = $value->getDenormalizedValue(); + } + try { $this->setAttributeValue($object, $attribute, $value, $format, $context); } catch (InvalidArgumentException $e) { @@ -339,6 +360,20 @@ public function denormalize($data, string $type, string $format = null, array $c } } + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + if (!empty($extraAttributes)) { + $message = (new ExtraAttributesException($extraAttributes))->getMessage(); + + $invariantViolations[''][] = new InvariantViolation($data, $message); + } + + if ([] !== $invariantViolations) { + return DenormalizationResult::failure($invariantViolations, $object); + } + + return DenormalizationResult::success($object); + } + if (!empty($extraAttributes)) { throw new ExtraAttributesException($extraAttributes); } @@ -364,13 +399,13 @@ abstract protected function setAttributeValue(object $object, string $attribute, private function validateAndDenormalize(string $currentClass, string $attribute, $data, ?string $format, array $context) { if (null === $types = $this->getTypes($currentClass, $attribute)) { - return $data; + return $this->denormalizationSuccess($data, $context); } $expectedTypes = []; foreach ($types as $type) { if (null === $data && $type->isNullable()) { - return null; + return $this->denormalizationSuccess(null, $context); } $collectionValueType = $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null : null; @@ -386,7 +421,7 @@ private function validateAndDenormalize(string $currentClass, string $attribute, // That's why we have to transform the values, if one of these non-string basic datatypes is expected. if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { if ('' === $data && $type->isNullable() && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) { - return null; + return $this->denormalizationSuccess(null, $context); } switch ($type->getBuiltinType()) { @@ -397,30 +432,36 @@ private function validateAndDenormalize(string $currentClass, string $attribute, } elseif ('true' === $data || '1' === $data) { $data = true; } else { - throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data)); + $message = sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data); + + return $this->denormalizationFailure($data, $message, $context); } break; case Type::BUILTIN_TYPE_INT: if (ctype_digit($data) || '-' === $data[0] && ctype_digit(substr($data, 1))) { $data = (int) $data; } else { - throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data)); + $message = sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data); + + return $this->denormalizationFailure($data, $message, $context); } break; case Type::BUILTIN_TYPE_FLOAT: if (is_numeric($data)) { - return (float) $data; + return $this->denormalizationSuccess((float) $data, $context); } switch ($data) { case 'NaN': - return \NAN; + return $this->denormalizationSuccess(\NAN, $context); case 'INF': - return \INF; + return $this->denormalizationSuccess(\INF, $context); case '-INF': - return -\INF; + return $this->denormalizationSuccess(-\INF, $context); default: - throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data)); + $message = sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data); + + return $this->denormalizationFailure($data, $message, $context); } break; @@ -479,19 +520,41 @@ private function validateAndDenormalize(string $currentClass, string $attribute, // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when // a float is expected. if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) { - return (float) $data; + return $this->denormalizationSuccess((float) $data, $context); } if (('is_'.$builtinType)($data)) { - return $data; + return $this->denormalizationSuccess($data, $context); } } if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { - return $data; + return $this->denormalizationSuccess($data, $context); + } + + $message = 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)); + + return $this->denormalizationFailure($data, $message, $context); + } + + private function denormalizationSuccess($denormalizedValue, array $context) + { + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return DenormalizationResult::success($denormalizedValue); + } + + return $denormalizedValue; + } + + private function denormalizationFailure($normalizedValue, string $message, array $context) + { + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + $violation = new InvariantViolation($normalizedValue, $message); + + return DenormalizationResult::failure(['' => [$violation]]); } - 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))); + throw new NotNormalizableValueException($message); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php index 77c746c752282..dbd5fb1107b40 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php @@ -11,9 +11,11 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\Serializer\DenormalizationResult; use Symfony\Component\Serializer\Exception\BadMethodCallException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\InvariantViolation; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -34,7 +36,7 @@ class ArrayDenormalizer implements ContextAwareDenormalizerInterface, Denormaliz * * @throws NotNormalizableValueException */ - public function denormalize($data, string $type, string $format = null, array $context = []): array + public function denormalize($data, string $type, string $format = null, array $context = []) { if (null === $this->denormalizer) { throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!'); @@ -48,13 +50,44 @@ public function denormalize($data, string $type, string $format = null, array $c $type = substr($type, 0, -2); + $invariantViolations = []; + $collectInvariantViolations = $context[SerializerInterface::RETURN_RESULT] ?? false; + $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))); + $message = sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key)); + + if ($collectInvariantViolations) { + $invariantViolations[$key][] = new InvariantViolation($value, $message); + + continue; + } + + throw new NotNormalizableValueException($message); } - $data[$key] = $this->denormalizer->denormalize($value, $type, $format, $context); + $denormalizedValue = $this->denormalizer->denormalize($value, $type, $format, $context); + + if ($denormalizedValue instanceof DenormalizationResult) { + if (!$denormalizedValue->isSucessful()) { + $invariantViolations += $denormalizedValue->getInvariantViolationsNestedIn($key); + + continue; + } + + $denormalizedValue = $denormalizedValue->getDenormalizedValue(); + } + + $data[$key] = $denormalizedValue; + } + + if ([] !== $invariantViolations) { + return DenormalizationResult::failure($invariantViolations, $data); + } + + if ($collectInvariantViolations) { + return DenormalizationResult::success($data); } return $data; diff --git a/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php index 2546ffd0c69d5..0d6a6435839ae 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; /** @@ -41,8 +43,6 @@ public function __construct($defaultContext = [], NameConverterInterface $nameCo /** * {@inheritdoc} - * - * @return array */ public function normalize($object, string $format = null, array $context = []) { @@ -103,7 +103,13 @@ public function normalize($object, string $format = null, array $context = []) $result['instance'] = $instance; } - return $result + ['violations' => $violations]; + $result += ['violations' => $violations]; + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/CustomNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/CustomNormalizer.php index 6bf6339372d5f..56182705772c2 100644 --- a/src/Symfony/Component/Serializer/Normalizer/CustomNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/CustomNormalizer.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\Serializer\Result\NormalizationResult; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerAwareTrait; +use Symfony\Component\Serializer\SerializerInterface; /** * @author Jordi Boggiano @@ -27,7 +29,13 @@ class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, Se */ public function normalize($object, string $format = null, array $context = []) { - return $object->normalize($this->serializer, $format, $context); + $result = $object->normalize($this->serializer, $format, $context); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php index bb866ec9bcc36..370a7528cfa60 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php @@ -16,6 +16,10 @@ use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\InvariantViolation; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * Normalizes an {@see \SplFileInfo} object to a data URI. @@ -67,10 +71,16 @@ public function normalize($object, string $format = null, array $context = []) } if ('text' === explode('/', $mimeType, 2)[0]) { - return sprintf('data:%s,%s', $mimeType, rawurlencode($data)); + $result = sprintf('data:%s,%s', $mimeType, rawurlencode($data)); + } else { + $result = sprintf('data:%s;base64,%s', $mimeType, base64_encode($data)); } - return sprintf('data:%s;base64,%s', $mimeType, base64_encode($data)); + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; } /** @@ -94,6 +104,27 @@ public function supportsNormalization($data, string $format = null) * @return \SplFileInfo */ public function denormalize($data, string $type, string $format = null, array $context = []) + { + try { + $result = $this->doDenormalize($data, $type); + } catch (NotNormalizableValueException $exception) { + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + $violation = new InvariantViolation($data, $exception->getMessage(), $exception); + + return DenormalizationResult::failure(['' => [$violation]]); + } + + throw $exception; + } + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return DenormalizationResult::success($result); + } + + return $result; + } + + 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 a47fd76b45116..ea8caa58e192f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php @@ -13,6 +13,10 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\InvariantViolation; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * Normalizes an instance of {@see \DateInterval} to an interval string. @@ -46,7 +50,13 @@ public function normalize($object, string $format = null, array $context = []) throw new InvalidArgumentException('The object must be an instance of "\DateInterval".'); } - return $object->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]); + $result = $object->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; } /** @@ -74,6 +84,27 @@ public function hasCacheableSupportsMethod(): bool * @return \DateInterval */ public function denormalize($data, string $type, string $format = null, array $context = []) + { + try { + $result = $this->doDenormalize($data, $context); + } catch (InvalidArgumentException | UnexpectedValueException $exception) { + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + $violation = new InvariantViolation($data, $exception->getMessage(), $exception); + + return DenormalizationResult::failure(['' => [$violation]]); + } + + throw $exception; + } + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return DenormalizationResult::success($result); + } + + return $result; + } + + 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 9de76008a56a7..5fe5c7ef3929a 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -13,6 +13,10 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\InvariantViolation; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * Normalizes an object implementing the {@see \DateTimeInterface} to a date string. @@ -45,8 +49,6 @@ public function __construct(array $defaultContext = []) * {@inheritdoc} * * @throws InvalidArgumentException - * - * @return string */ public function normalize($object, string $format = null, array $context = []) { @@ -62,7 +64,13 @@ public function normalize($object, string $format = null, array $context = []) $object = $object->setTimezone($timezone); } - return $object->format($dateTimeFormat); + $result = $object->format($dateTimeFormat); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; } /** @@ -81,6 +89,27 @@ public function supportsNormalization($data, string $format = null) * @return \DateTimeInterface */ public function denormalize($data, string $type, string $format = null, array $context = []) + { + try { + $result = $this->doDenormalize($data, $type, $context); + } catch (NotNormalizableValueException $exception) { + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + $violation = new InvariantViolation($data, $exception->getMessage(), $exception); + + return DenormalizationResult::failure(['' => [$violation]]); + } + + throw $exception; + } + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return DenormalizationResult::success($result); + } + + return $result; + } + + 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/DateTimeZoneNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php index af262ebaad70e..ea20948845fdc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php @@ -13,6 +13,8 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * Normalizes a {@see \DateTimeZone} object to a timezone string. @@ -34,7 +36,13 @@ public function normalize($object, string $format = null, array $context = []) throw new InvalidArgumentException('The object must be an instance of "\DateTimeZone".'); } - return $object->getName(); + $result = $object->getName(); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php index d903b3912d019..05845d2e69f34 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php @@ -27,6 +27,10 @@ interface DenormalizerInterface /** * Denormalizes data back into an object of the given class. * + * When context option `return_result` is enabled, the denormalizer must + * always return an instance of + * {@see \Symfony\Component\Serializer\Result\DenormalizationResult}. + * * @param mixed $data Data to restore * @param string $type The expected class to instantiate * @param string $format Format the given data was extracted from diff --git a/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php index 48399f4e6c068..3492def115842 100644 --- a/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\Form\FormInterface; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * Normalizes invalid Form instances. @@ -25,7 +27,7 @@ final class FormErrorNormalizer implements NormalizerInterface, CacheableSupport /** * {@inheritdoc} */ - public function normalize($object, $format = null, array $context = []): array + public function normalize($object, $format = null, array $context = []) { $data = [ 'title' => $context[self::TITLE] ?? 'Validation Failed', @@ -38,6 +40,10 @@ public function normalize($object, $format = null, array $context = []): array $data['children'] = $this->convertFormChildrenToArray($object); } + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($data); + } + return $data; } diff --git a/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php index f38956c3e3cab..7c776f2722270 100644 --- a/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php @@ -13,6 +13,8 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * A normalizer that uses an objects own JsonSerializable implementation. @@ -38,7 +40,13 @@ public function normalize($object, string $format = null, array $context = []) throw new LogicException('Cannot normalize object because injected serializer is not a normalizer.'); } - return $this->serializer->normalize($object->jsonSerialize(), $format, $context); + $result = $this->serializer->normalize($object->jsonSerialize(), $format, $context); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/MimeMessageNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/MimeMessageNormalizer.php index a1c4f169bbf5e..b41d1e60f0367 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\Result\DenormalizationResult; +use Symfony\Component\Serializer\Result\NormalizationResult; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -60,6 +62,10 @@ public function normalize($object, ?string $format = null, array $context = []) $ret[$name] = $this->serializer->normalize($header, $format, $context); } + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($ret); + } + return $ret; } @@ -67,10 +73,20 @@ public function normalize($object, ?string $format = null, array $context = []) $ret = $this->normalizer->normalize($object, $format, $context); $ret['class'] = \get_class($object); + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($ret); + } + return $ret; } - return $this->normalizer->normalize($object, $format, $context); + $ret = $this->normalizer->normalize($object, $format, $context); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($ret); + } + + return $ret; } /** @@ -80,13 +96,37 @@ public function denormalize($data, string $type, ?string $format = null, array $ { if (Headers::class === $type) { $ret = []; + $invariantViolations = []; foreach ($data as $headers) { foreach ($headers as $header) { - $ret[] = $this->serializer->denormalize($header, $this->headerClassMap[strtolower($header['name'])] ?? UnstructuredHeader::class, $format, $context); + $name = $header['name']; + $result = $this->serializer->denormalize($header, $this->headerClassMap[strtolower($name)] ?? UnstructuredHeader::class, $format, $context); + + if ($result instanceof DenormalizationResult) { + if (!$result->isSucessful()) { + $invariantViolations += $result->getInvariantViolations(); + + continue; + } + + $result = $result->getDenormalizedValue(); + } + + $ret[] = $result; } } - return new Headers(...$ret); + if ([] !== $invariantViolations) { + return DenormalizationResult::failure($invariantViolations); + } + + $headers = new Headers(...$ret); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return DenormalizationResult::success($headers); + } + + return $headers; } if (AbstractPart::class === $type) { diff --git a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php index 653f949548c41..df0ef7b656f5f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php @@ -15,6 +15,7 @@ use Symfony\Component\Serializer\Exception\ExceptionInterface; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Result\NormalizationResult; /** * @author Jordi Boggiano @@ -24,11 +25,14 @@ interface NormalizerInterface /** * Normalizes an object into a set of arrays/scalars. * + * When context option `return_result` is enabled, the normalizer must + * always return an instance of {@see NormalizationResult}. + * * @param mixed $object Object to normalize * @param string $format Format the normalization result will be encoded as * @param array $context Context options for the normalizer * - * @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is encoded as an object not an array + * @return NormalizationResult|array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is encoded as an object not an array * * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer * @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular diff --git a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php index 6fdd2773a3608..f2bdfd6f0359d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php @@ -13,6 +13,8 @@ use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * Normalizes errors according to the API Problem spec (RFC 7807). @@ -61,6 +63,10 @@ public function normalize($object, string $format = null, array $context = []) $data['trace'] = $object->getTrace(); } + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($data); + } + return $data; } diff --git a/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php index a7152613cbd93..3e31fae064bfc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php @@ -13,6 +13,10 @@ use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\InvariantViolation; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\Result\NormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Uuid; @@ -41,6 +45,17 @@ public function __construct(array $defaultContext = []) * @param AbstractUid $object */ public function normalize($object, string $format = null, array $context = []) + { + $result = $this->doNormalize($object, $context); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; + } + + public function doNormalize(AbstractUid $object, array $context): string { switch ($context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY]) { case self::NORMALIZATION_FORMAT_CANONICAL: @@ -70,10 +85,24 @@ public function supportsNormalization($data, string $format = null) public function denormalize($data, string $type, string $format = null, array $context = []) { try { - return Ulid::class === $type ? Ulid::fromString($data) : Uuid::fromString($data); + $result = Ulid::class === $type ? Ulid::fromString($data) : Uuid::fromString($data); } catch (\InvalidArgumentException $exception) { - throw new NotNormalizableValueException(sprintf('The data is not a valid "%s" string representation.', $type)); + $message = sprintf('The data is not a valid "%s" string representation.', $type); + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + $violation = new InvariantViolation($data, $message, $exception); + + return DenormalizationResult::failure(['' => [$violation]]); + } + + throw new NotNormalizableValueException($message); } + + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return DenormalizationResult::success($result); + } + + return $result; } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php index 6bc8df80bb3cd..781059796f336 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\Result\DenormalizationResult; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerAwareTrait; @@ -48,7 +49,15 @@ public function denormalize($data, $class, string $format = null, array $context $data = $this->propertyAccessor->getValue($data, $propertyPath); } - return $this->serializer->denormalize($data, $class, $format, $context); + $result = $this->serializer->denormalize($data, $class, $format, $context); + + if ($result instanceof DenormalizationResult && !$result->isSucessful()) { + $propertyPath = str_replace('][', '.', substr($propertyPath, 1, -1)); + + return DenormalizationResult::failure($result->getInvariantViolationsNestedIn($propertyPath)); + } + + return $result; } /** diff --git a/src/Symfony/Component/Serializer/Result/DenormalizationResult.php b/src/Symfony/Component/Serializer/Result/DenormalizationResult.php new file mode 100644 index 0000000000000..34c75d9fde4d2 --- /dev/null +++ b/src/Symfony/Component/Serializer/Result/DenormalizationResult.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Result; + +use Symfony\Component\Serializer\InvariantViolation; + +final class DenormalizationResult +{ + private $denormalizedValue; + private $invariantViolations = []; + + private function __construct() + { + } + + public static function success($denormalizedValue): self + { + $result = new self(); + $result->denormalizedValue = $denormalizedValue; + + return $result; + } + + /** + * @param array> $invariantViolations + */ + public static function failure(array $invariantViolations, $partiallyDenormalizedValue = null): self + { + $result = new self(); + $result->invariantViolations = $invariantViolations; + $result->denormalizedValue = $partiallyDenormalizedValue; + + return $result; + } + + public function isSucessful(): bool + { + return [] === $this->invariantViolations; + } + + public function getDenormalizedValue() + { + return $this->denormalizedValue; + } + + public function getInvariantViolations(): array + { + return $this->invariantViolations; + } + + /** + * @return array> + */ + public function getInvariantViolationsNestedIn(string $parentPath): array + { + if ('' === $parentPath) { + throw new \InvalidArgumentException('Parent path cannot be empty.'); + } + + $nestedViolations = []; + + foreach ($this->invariantViolations as $path => $violations) { + $path = '' !== $path ? "{$parentPath}.{$path}" : $parentPath; + + $nestedViolations[$path] = $violations; + } + + return $nestedViolations; + } + + /** + * @return array> + */ + public function getInvariantViolationMessages(): array + { + $messages = []; + + foreach ($this->invariantViolations as $path => $violations) { + foreach ($violations as $violation) { + $messages[$path][] = $violation->getMessage(); + } + } + + return $messages; + } +} diff --git a/src/Symfony/Component/Serializer/Result/NormalizationResult.php b/src/Symfony/Component/Serializer/Result/NormalizationResult.php new file mode 100644 index 0000000000000..b7c44cab38071 --- /dev/null +++ b/src/Symfony/Component/Serializer/Result/NormalizationResult.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Result; + +use Throwable; + +final class NormalizationResult +{ + private $normalizedValue; + private $exceptions = []; + + private function __construct() + { + } + + public static function success($normalizedValue): self + { + $result = new self(); + $result->normalizedValue = $normalizedValue; + + return $result; + } + + /** + * @param array $exceptions + */ + public static function failure(array $exceptions, $partiallyNormalizedValue = null): self + { + $result = new self(); + $result->exceptions = $exceptions; + $result->normalizedValue = $partiallyNormalizedValue; + + return $result; + } + + public function isSucessful(): bool + { + return [] === $this->exceptions; + } + + public function getNormalizedValue() + { + return $this->normalizedValue; + } + + public function getExceptions(): array + { + return $this->exceptions; + } + + /** + * @return array + */ + public function getExceptionsNestedIn(string $parentPath): array + { + if ('' === $parentPath) { + throw new \InvalidArgumentException('Parent path cannot be empty.'); + } + + $nestedExceptions = []; + + foreach ($this->exceptions as $path => $exception) { + $path = '' !== $path ? "{$parentPath}.{$path}" : $parentPath; + + $nestedExceptions[$path] = $exception; + } + + return $nestedExceptions; + } + + /** + * @return array + */ + public function getExceptionMessages(): array + { + $messages = []; + + foreach ($this->exceptions as $path => $exception) { + $messages[$path] = $exception->getMessage(); + } + + return $messages; + } +} diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index 6414caf900472..ff04b02913b9b 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -29,6 +29,8 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\Result\NormalizationResult; /** * Serializer serializes and deserializes data. @@ -151,16 +153,19 @@ public function normalize($data, string $format = null, array $context = []) { // If a normalizer supports the given data, use it if ($normalizer = $this->getNormalizer($data, $format, $context)) { - return $normalizer->normalize($data, $format, $context); + return $this->normalizationSuccess( + $normalizer->normalize($data, $format, $context), + $context + ); } if (null === $data || is_scalar($data)) { - return $data; + return $this->normalizationSuccess($data, $context); } if (\is_array($data) || $data instanceof \Traversable) { if (($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) === true && $data instanceof \Countable && 0 === $data->count()) { - return $data; + return $this->normalizationSuccess($data, $context); } $normalized = []; @@ -168,7 +173,7 @@ public function normalize($data, string $format = null, array $context = []) $normalized[$key] = $this->normalize($val, $format, $context); } - return $normalized; + return $this->normalizationSuccess($normalized, $context); } if (\is_object($data)) { @@ -194,7 +199,19 @@ public function denormalize($data, string $type, string $format = null, array $c // Check for a denormalizer first, e.g. the data is wrapped if (!$normalizer && isset(self::SCALAR_TYPES[$type])) { if (!('is_'.$type)($data)) { - throw new NotNormalizableValueException(sprintf('Data expected to be of type "%s" ("%s" given).', $type, get_debug_type($data))); + $message = sprintf('Data expected to be of type "%s" ("%s" given).', $type, get_debug_type($data)); + + if ($context[self::RETURN_RESULT] ?? false) { + $violation = new InvariantViolation($data, $message); + + return DenormalizationResult::failure(['' => [$violation]]); + } + + throw new NotNormalizableValueException($message); + } + + if ($context[self::RETURN_RESULT] ?? false) { + return DenormalizationResult::success($data); } return $data; @@ -302,6 +319,15 @@ private function getDenormalizer($data, string $class, ?string $format, array $c return null; } + private function normalizationSuccess($result, array $context) + { + if ($context[SerializerInterface::RETURN_RESULT] ?? false) { + return NormalizationResult::success($result); + } + + return $result; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Serializer/SerializerInterface.php b/src/Symfony/Component/Serializer/SerializerInterface.php index 96e144e90e992..39d669938bb2a 100644 --- a/src/Symfony/Component/Serializer/SerializerInterface.php +++ b/src/Symfony/Component/Serializer/SerializerInterface.php @@ -16,9 +16,15 @@ */ interface SerializerInterface { + const RETURN_RESULT = 'return_result'; + /** * Serializes data in the appropriate format. * + * When context option `return_result` is enabled, the serializer must + * always return an instance of + * {@see \Symfony\Component\Serializer\Result\NormalizationResult}. + * * @param mixed $data Any data * @param string $format Format name * @param array $context Options normalizers/encoders have access to @@ -30,6 +36,10 @@ public function serialize($data, string $format, array $context = []); /** * Deserializes data into the given type. * + * When context option `return_result` is enabled, the serializer must + * always return an instance of + * {@see \Symfony\Component\Serializer\Result\DenormalizationResult}. + * * @param mixed $data * * @return mixed diff --git a/src/Symfony/Component/Serializer/Tests/InvariantViolationTest.php b/src/Symfony/Component/Serializer/Tests/InvariantViolationTest.php new file mode 100644 index 0000000000000..7b81f816fe92c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/InvariantViolationTest.php @@ -0,0 +1,23 @@ +getNormalizedValue()); + self::assertSame('"foo" is not an integer.', $violation->getMessage()); + self::assertSame($exception, $violation->getThrowable()); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php index 1e54f5b37bfa0..f4de12059aeb9 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php @@ -14,6 +14,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * @author Kévin Dunglas @@ -169,6 +172,34 @@ public function validUriProvider() ]; } + /** + * @dataProvider validUriProvider + */ + public function testItDenormalizesAndReturnsSuccessResult($uri): void + { + $result = $this->normalizer->denormalize($uri, \SplFileObject::class, null, [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertTrue($result->isSucessful()); + self::assertInstanceOf(\SplFileObject::class, $result->getDenormalizedValue()); + } + + public function testItDenormalizesAndReturnsFailureResult(): void + { + $result = $this->normalizer->denormalize('not-a-uri', \SplFileObject::class, null, [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertFalse($result->isSucessful()); + self::assertSame( + ['' => ['The provided "data:" URI is not valid.']], + $result->getInvariantViolationMessages() + ); + } + private function getContent(\SplFileObject $file) { $buffer = ''; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index a041074b84fee..de40035e49131 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; /** * @author Kévin Dunglas @@ -259,4 +262,29 @@ public function testDenormalizeFormatMismatchThrowsException() $this->expectException(\Symfony\Component\Serializer\Exception\UnexpectedValueException::class); $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeInterface::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d|']); } + + public function testItDenormalizesAndReturnsSuccessResult(): void + { + $result = $this->normalizer->denormalize('2020-01-01', \DateTimeInterface::class, null, [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertTrue($result->isSucessful()); + self::assertEquals(new \DateTime('2020-01-01'), $result->getDenormalizedValue()); + } + + public function testItDenormalizesAndReturnsFailureResult(): void + { + $result = $this->normalizer->denormalize('not-a-date', \DateTimeInterface::class, null, [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertFalse($result->isSucessful()); + self::assertSame( + ['' => ['DateTimeImmutable::__construct(): Failed to parse time string (not-a-date) at position 0 (n): The timezone could not be found in the database']], + $result->getInvariantViolationMessages() + ); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/UidNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/UidNormalizerTest.php index f1b766ea4c87c..c671a22bf2e75 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/UidNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/UidNormalizerTest.php @@ -4,7 +4,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\UidNormalizer; +use Symfony\Component\Serializer\Result\DenormalizationResult; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Uuid; @@ -168,4 +171,51 @@ public function testNormalizeWithNormalizationFormatNotValid() 'uid_normalization_format' => 'ccc', ]); } + + /** + * @dataProvider dataProvider + */ + public function testItDenormalizesAndReturnsSuccessResult(string $uuidString, string $class): void + { + $result = $this->normalizer->denormalize($uuidString, $class, null, [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertTrue($result->isSucessful()); + self::assertEquals( + Ulid::class === $class ? new Ulid($uuidString) : Uuid::fromString($uuidString), + $result->getDenormalizedValue() + ); + } + + /** + * @dataProvider failureDataProvider + */ + public function testItDenormalizesAndReturnsFailureResult(string $class): void + { + $result = $this->normalizer->denormalize('not-an-uuid', $class, null, [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertFalse($result->isSucessful()); + self::assertSame( + ['' => [sprintf('The data is not a valid "%s" string representation.', $class)]], + $result->getInvariantViolationMessages() + ); + } + + public function failureDataProvider(): iterable + { + return [ + [UuidV1::class], + [UuidV3::class], + [UuidV4::class], + [UuidV5::class], + [UuidV6::class], + [AbstractUid::class], + [Ulid::class], + ]; + } } diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 7806be7cbb496..0384d134989f5 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Tests; +use DateTimeImmutable; use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyAccess\PropertyAccessor; @@ -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; @@ -38,7 +40,9 @@ use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; +use Symfony\Component\Serializer\Result\DenormalizationResult; use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummyFirstChild; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummySecondChild; @@ -640,6 +644,154 @@ public function testDeserializeAndUnwrap() $serializer->deserialize($jsonData, __NAMESPACE__.'\Model', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]']) ); } + + /** + * @dataProvider provideDenormalizationSuccessResultCases + */ + public function testDenormalizationSuccessResult(string $type, $normalizedData, $expectedValue) + { + $serializer = new Serializer( + [ + new DateTimeNormalizer(), + new ObjectNormalizer(null, null, null, new PhpDocExtractor()), + new ArrayDenormalizer(), + ], + [ + 'json' => new JsonEncoder(), + ] + ); + + $json = json_encode($normalizedData); + + $result = $serializer->deserialize($json, $type, 'json', [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertTrue($result->isSucessful()); + self::assertEquals($expectedValue, $result->getDenormalizedValue()); + } + + public function provideDenormalizationSuccessResultCases() + { + $dto = new Dto(); + $dto->int = 1; + $dto->date = new DateTimeImmutable('2020-01-01'); + $dto->nested = new NestedDto(); + $dto->nested->string = 'string'; + + yield [ + Dto::class, + [ + 'int' => 1, + 'date' => '2020-01-01', + 'nested' => [ + 'string' => 'string', + ], + ], + $dto, + ]; + + yield [ + 'bool', + true, + true, + ]; + } + + /** + * @dataProvider provideDenormalizationFailureResultCases + */ + public function testDenormalizationFailureResult(string $type, $normalizedData, array $expectedErrors) + { + $serializer = new Serializer( + [ + new DateTimeNormalizer(), + new ObjectNormalizer(null, null, null, new PhpDocExtractor()), + new ArrayDenormalizer(), + ], + [ + 'json' => new JsonEncoder(), + ] + ); + + $json = json_encode($normalizedData); + + $result = $serializer->deserialize($json, $type, 'json', [ + SerializerInterface::RETURN_RESULT => true, + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertFalse($result->isSucessful()); + self::assertSame($expectedErrors, $result->getInvariantViolationMessages()); + } + + public function provideDenormalizationFailureResultCases() + { + yield [ + Dto::class, + [ + 'int' => 'not-an-integer', + 'date' => 'not-a-date', + 'nested' => [ + 'string' => [], + ], + ], + [ + 'int' => ['The type of the "int" attribute for class "Symfony\Component\Serializer\Tests\Dto" must be one of "int" ("string" given).'], + 'date' => ['DateTimeImmutable::__construct(): Failed to parse time string (not-a-date) at position 0 (n): The timezone could not be found in the database'], + 'nested.string' => ['The type of the "string" attribute for class "Symfony\Component\Serializer\Tests\NestedDto" must be one of "string" ("array" given).'], + ], + ]; + + yield [ + 'bool', + 'not-a-boolean', + [ + '' => ['Data expected to be of type "bool" ("string" given).'], + ], + ]; + } + + public function testDenormalizationFailureResultWithUnwrapping() + { + $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' => [ + 'int' => 'not-an-integer', + 'date' => 'not-a-date', + 'nested' => [ + 'string' => [], + ], + ], + ], + ]); + + $result = $serializer->deserialize($json, Dto::class, 'json', [ + SerializerInterface::RETURN_RESULT => true, + UnwrappingDenormalizer::UNWRAP_PATH => '[wrapped][data]', + ]); + + self::assertInstanceOf(DenormalizationResult::class, $result); + self::assertFalse($result->isSucessful()); + self::assertSame([ + 'wrapped.data.int' => ['The type of the "int" attribute for class "Symfony\Component\Serializer\Tests\Dto" must be one of "int" ("string" given).'], + 'wrapped.data.date' => ['DateTimeImmutable::__construct(): Failed to parse time string (not-a-date) at position 0 (n): The timezone could not be found in the database'], + 'wrapped.data.nested.string' => ['The type of the "string" attribute for class "Symfony\Component\Serializer\Tests\NestedDto" must be one of "string" ("array" given).'], + ], $result->getInvariantViolationMessages()); + } } class Model @@ -713,3 +865,29 @@ interface NormalizerAwareNormalizer extends NormalizerInterface, NormalizerAware interface DenormalizerAwareDenormalizer extends DenormalizerInterface, DenormalizerAwareInterface { } + +class Dto +{ + /** + * @var int + */ + public $int; + + /** + * @var \DateTimeImmutable + */ + public $date; + + /** + * @var NestedDto + */ + public $nested; +} + +class NestedDto +{ + /** + * @var string + */ + public $string; +} 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