From 2e6a44fbfcaa38a825d6045f53765e922ede6faa Mon Sep 17 00:00:00 2001 From: NorthBlue333 Date: Sun, 12 Jun 2022 22:39:08 +0200 Subject: [PATCH] Add COLLECT_EXTRA_ATTRIBUTES_ERRORS and full deserialization path --- UPGRADE-6.2.md | 2 + .../Component/PropertyAccess/CHANGELOG.md | 1 + .../Component/PropertyAccess/PropertyPath.php | 29 +++ .../PropertyAccess/Tests/PropertyPathTest.php | 20 ++ src/Symfony/Component/Serializer/CHANGELOG.md | 2 + .../Context/SerializerContextBuilder.php | 6 + .../PartialDenormalizationException.php | 39 +++- .../Normalizer/AbstractNormalizer.php | 3 +- .../Normalizer/AbstractObjectNormalizer.php | 13 +- .../Normalizer/ArrayDenormalizer.php | 3 +- .../Normalizer/DenormalizerInterface.php | 8 + .../Component/Serializer/Serializer.php | 26 ++- .../Context/SerializerContextBuilderTest.php | 4 + .../Serializer/Tests/Fixtures/Php74Full.php | 1 + .../Serializer/Tests/SerializerTest.php | 215 +++++++++++++++++- 15 files changed, 345 insertions(+), 27 deletions(-) diff --git a/UPGRADE-6.2.md b/UPGRADE-6.2.md index 9bb4ed45ba9d5..c3f0b1d68f3cf 100644 --- a/UPGRADE-6.2.md +++ b/UPGRADE-6.2.md @@ -100,6 +100,8 @@ Serializer * Deprecate calling `AttributeMetadata::setSerializedName()`, `ClassMetadata::setClassDiscriminatorMapping()` without arguments * Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)` * Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)` + * Change the signature of `PartialDenormalizationException::__construct($data, array $errors)` to `__construct(mixed $data, array $errors, array $extraAttributesErrors = [])` + * Deprecate `PartialDenormalizationException::getErrors()`, call `getNotNormalizableValueErrors()` instead Translation ----------- diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index 64df40fed5170..f6fea94a99313 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Added method `isNullSafe()` to `PropertyPathInterface`, implementing the interface without implementing this method is deprecated * Add support for the null-coalesce operator in property paths + * Add `PropertyPath::append()` 6.0 --- diff --git a/src/Symfony/Component/PropertyAccess/PropertyPath.php b/src/Symfony/Component/PropertyAccess/PropertyPath.php index 341891316dedc..a81237c61ceb2 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyPath.php +++ b/src/Symfony/Component/PropertyAccess/PropertyPath.php @@ -203,4 +203,33 @@ public function isNullSafe(int $index): bool return $this->isNullSafe[$index]; } + + /** + * Utility method for dealing with property paths. + * For more extensive functionality, use instances of this class. + * + * Appends a path to a given property path. + * + * If the base path is empty, the appended path will be returned unchanged. + * If the base path is not empty, and the appended path starts with a + * squared opening bracket ("["), the concatenation of the two paths is + * returned. Otherwise, the concatenation of the two paths is returned, + * separated by a dot ("."). + */ + public static function append(string $basePath, string $subPath): string + { + if ('' === $subPath) { + return $basePath; + } + + if ('[' === $subPath[0]) { + return $basePath.$subPath; + } + + if ('' === $basePath) { + return $subPath; + } + + return $basePath.'.'.$subPath; + } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php index c7724318a0b7e..a87d5062d050c 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php @@ -170,4 +170,24 @@ public function testIsIndexDoesNotAcceptNegativeIndices() $propertyPath->isIndex(-1); } + + /** + * @dataProvider provideAppendPaths + */ + public function testAppend(string $basePath, string $subPath, string $expectedPath, string $message) + { + $this->assertSame($expectedPath, PropertyPath::append($basePath, $subPath), $message); + } + + public function provideAppendPaths() + { + return [ + ['foo', '', 'foo', 'It returns the basePath if subPath is empty'], + ['', 'bar', 'bar', 'It returns the subPath if basePath is empty'], + ['foo', 'bar', 'foo.bar', 'It append the subPath to the basePath'], + ['foo', '[bar]', 'foo[bar]', 'It does not include the dot separator if subPath uses the array notation'], + ['0', 'bar', '0.bar', 'Leading zeros are kept.'], + ['0', 1, '0.1', 'Numeric subpaths do not cause PHP 7.4 errors.'], + ]; + } } diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 3342ada2fea86..044df09f37fb8 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -12,6 +12,8 @@ CHANGELOG * Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)` * Add option YamlEncoder::YAML_INDENTATION to YamlEncoder constructor options to configure additional indentation for each level of nesting. This allows configuring indentation in the service configuration. * Add `SerializedPath` annotation to flatten nested attributes + * Add `COLLECT_EXTRA_ATTRIBUTES_ERRORS` option to `Serializer` to collect errors from nested denormalizations + * Deprecate `PartialDenormalizationException::getErrors()`, call `getNotNormalizableValueErrors()` instead 6.1 --- diff --git a/src/Symfony/Component/Serializer/Context/SerializerContextBuilder.php b/src/Symfony/Component/Serializer/Context/SerializerContextBuilder.php index a6359be98f6db..5d9ec20c2a7f2 100644 --- a/src/Symfony/Component/Serializer/Context/SerializerContextBuilder.php +++ b/src/Symfony/Component/Serializer/Context/SerializerContextBuilder.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Context; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -36,4 +37,9 @@ public function withCollectDenormalizationErrors(?bool $collectDenormalizationEr { return $this->with(DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS, $collectDenormalizationErrors); } + + public function withCollectExtraAttributesErrors(?bool $collectExtraAttributesErrors): static + { + return $this->with(DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS, $collectExtraAttributesErrors); + } } diff --git a/src/Symfony/Component/Serializer/Exception/PartialDenormalizationException.php b/src/Symfony/Component/Serializer/Exception/PartialDenormalizationException.php index fdb838be79cae..dee06829d86e8 100644 --- a/src/Symfony/Component/Serializer/Exception/PartialDenormalizationException.php +++ b/src/Symfony/Component/Serializer/Exception/PartialDenormalizationException.php @@ -16,13 +16,26 @@ */ class PartialDenormalizationException extends UnexpectedValueException { - private $data; - private $errors; + private ?ExtraAttributesException $extraAttributesError = null; - public function __construct($data, array $errors) + public function __construct( + private mixed $data, + /** + * @var NotNormalizableValueException[] + */ + private array $notNormalizableErrors, + array $extraAttributesErrors = [] + ) { $this->data = $data; - $this->errors = $errors; + $this->notNormalizableErrors = $notNormalizableErrors; + $extraAttributes = []; + foreach ($extraAttributesErrors as $error) { + $extraAttributes = \array_merge($extraAttributes, $error->getExtraAttributes()); + } + if ($extraAttributes) { + $this->extraAttributesError = new ExtraAttributesException($extraAttributes); + } } public function getData() @@ -30,8 +43,24 @@ public function getData() return $this->data; } + /** + * @deprecated since Symfony 6.2, use getNotNormalizableValueErrors() instead. + */ public function getErrors(): array { - return $this->errors; + return $this->getNotNormalizableValueErrors(); + } + + /** + * @return NotNormalizableValueException[] + */ + public function getNotNormalizableValueErrors(): array + { + return $this->notNormalizableErrors; + } + + public function getExtraAttributesError(): ?ExtraAttributesException + { + return $this->extraAttributesError; } } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 12c778cb803af..6a1396a7973fc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; @@ -505,7 +506,7 @@ protected function getAttributeNormalizationContext(object $object, string $attr */ protected function getAttributeDenormalizationContext(string $class, string $attribute, array $context): array { - $context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute : $attribute; + $context['deserialization_path'] = PropertyPath::append($context['deserialization_path'] ?? '', $attribute); if (null === $metadata = $this->getAttributeMetadata($class, $attribute)) { return $context; diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 7c4c5fb41bd49..28fd77a9f6d2e 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -15,6 +15,7 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Encoder\CsvEncoder; @@ -228,12 +229,12 @@ protected function instantiateObject(array &$data, string $class, array &$contex { if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { if (!isset($data[$mapping->getTypeProperty()])) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], PropertyPath::append($context['deserialization_path'] ?? '', $mapping->getTypeProperty()), false); } $type = $data[$mapping->getTypeProperty()]; if (null === ($mappedClass = $mapping->getClassForType($type))) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], PropertyPath::append($context['deserialization_path'] ?? '', $mapping->getTypeProperty()), true); } if ($mappedClass !== $class) { @@ -398,8 +399,12 @@ public function denormalize(mixed $data, string $type, string $format = null, ar } } - if ($extraAttributes) { - throw new ExtraAttributesException($extraAttributes); + if (!$extraAttributes) { + $extraAttributeException = new ExtraAttributesException(array_map(fn (string $extraAttribute) => PropertyPath::append($context['deserialization_path'] ?? '', $extraAttribute), $extraAttributes)); + if (!isset($context['extra_attributes_exceptions'])) { + throw $extraAttributeException; + } + $context['extra_attributes_exceptions'][] = $extraAttributeException; } return $object; diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php index a88beba7ab6c6..47876a8a4102e 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\BadMethodCallException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; @@ -47,7 +48,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null; foreach ($data as $key => $value) { $subContext = $context; - $subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]"; + $subContext['deserialization_path'] = PropertyPath::append($context['deserialization_path'] ?? '', "[$key]"); if (null !== $builtinType && !('is_'.$builtinType)($key)) { 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); diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php index ae3adbfe330fa..9d74ae98c8f1a 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php @@ -24,8 +24,16 @@ */ interface DenormalizerInterface { + /** + * Whether to collect all denormalization errors or to stop at first error. + */ public const COLLECT_DENORMALIZATION_ERRORS = 'collect_denormalization_errors'; + /** + * Whether to collect all extra attributes errors or to stop at first nested error. + */ + public const COLLECT_EXTRA_ATTRIBUTES_ERRORS = 'collect_extra_attributes_errors'; + /** * Denormalizes data back into an object of the given class. * diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index 85a3ac5b558d4..8f05faa958b69 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -212,19 +212,27 @@ public function denormalize(mixed $data, string $type, string $format = null, ar throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type)); } - if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) { - unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]); + $notNormalizableExceptions = []; + if ($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS] ?? false) { $context['not_normalizable_value_exceptions'] = []; - $errors = &$context['not_normalizable_value_exceptions']; - $denormalized = $normalizer->denormalize($data, $type, $format, $context); - if ($errors) { - throw new PartialDenormalizationException($denormalized, $errors); - } + $notNormalizableExceptions = &$context['not_normalizable_value_exceptions']; + } + unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]); + + $extraAttributesExceptions = []; + if ($context[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS] ?? false) { + $context['extra_attributes_exceptions'] = []; + $extraAttributesExceptions = &$context['extra_attributes_exceptions']; + } + unset($context[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS]); + + $denormalized = $normalizer->denormalize($data, $type, $format, $context); - return $denormalized; + if ($notNormalizableExceptions || $extraAttributesExceptions) { + throw new PartialDenormalizationException($denormalized, $notNormalizableExceptions, $extraAttributesExceptions); } - return $normalizer->denormalize($data, $type, $format, $context); + return $denormalized; } public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool diff --git a/src/Symfony/Component/Serializer/Tests/Context/SerializerContextBuilderTest.php b/src/Symfony/Component/Serializer/Tests/Context/SerializerContextBuilderTest.php index ca13c6530555c..c21606d865760 100644 --- a/src/Symfony/Component/Serializer/Tests/Context/SerializerContextBuilderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Context/SerializerContextBuilderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Context\SerializerContextBuilder; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -38,6 +39,7 @@ public function testWithers(array $values) $context = $this->contextBuilder ->withEmptyArrayAsObject($values[Serializer::EMPTY_ARRAY_AS_OBJECT]) ->withCollectDenormalizationErrors($values[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) + ->withCollectExtraAttributesErrors($values[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS]) ->toArray(); $this->assertSame($values, $context); @@ -51,11 +53,13 @@ public function withersDataProvider(): iterable yield 'With values' => [[ Serializer::EMPTY_ARRAY_AS_OBJECT => true, DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => false, + DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => false, ]]; yield 'With null values' => [[ Serializer::EMPTY_ARRAY_AS_OBJECT => null, DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => null, + DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => null, ]]; } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php index 5aea0fa4af76f..e25235812cf8a 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php @@ -33,6 +33,7 @@ final class Php74Full public TestFoo $nestedObject; /** @var Php74Full[] */ public $anotherCollection; + public TestFoo $nestedObject2; } diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 255abb3864b1d..5a7b667af8825 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -896,9 +896,10 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet $this->assertInstanceOf(PartialDenormalizationException::class, $th); } + /** @var PartialDenormalizationException $th */ $this->assertInstanceOf(Php74Full::class, $th->getData()); - $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + $exceptionsAsArray = array_map(static function (NotNormalizableValueException $e): array { return [ 'currentType' => $e->getCurrentType(), 'expectedTypes' => $e->getExpectedTypes(), @@ -906,7 +907,7 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet 'useMessageForUser' => $e->canUseMessageForUser(), 'message' => $e->getMessage(), ]; - }, $th->getErrors()); + }, $th->getNotNormalizableValueErrors()); $expected = [ [ @@ -1092,11 +1093,12 @@ public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMe $this->assertInstanceOf(PartialDenormalizationException::class, $th); } + /** @var PartialDenormalizationException $th */ $this->assertCount(2, $th->getData()); $this->assertInstanceOf(Php74Full::class, $th->getData()[0]); $this->assertInstanceOf(Php74Full::class, $th->getData()[1]); - $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + $exceptionsAsArray = array_map(static function (NotNormalizableValueException $e): array { return [ 'currentType' => $e->getCurrentType(), 'expectedTypes' => $e->getExpectedTypes(), @@ -1104,7 +1106,7 @@ public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMe 'useMessageForUser' => $e->canUseMessageForUser(), 'message' => $e->getMessage(), ]; - }, $th->getErrors()); + }, $th->getNotNormalizableValueErrors()); $expected = [ [ @@ -1130,6 +1132,204 @@ public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMe $this->assertSame($expected, $exceptionsAsArray); } + public function testNoCollectExtraAttributesErrors() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $json = ' + { + "extra1": true, + "collection": [ + { + "extra2": true + }, + { + "extra3": true + } + ], + "nestedObject2": { + "extra4": true + } + }'; + + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + + $serializer = new Serializer( + [ + new ArrayDenormalizer(), + new DateTimeNormalizer(), + new DateTimeZoneNormalizer(), + new DataUriNormalizer(), + new UidNormalizer(), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), + ], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize($json, Php74Full::class, 'json', [ + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, + DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => false, + ]); + + $this->fail(); + } catch (\Throwable $th) { + $this->assertInstanceOf(ExtraAttributesException::class, $th); + } + + /** + * @var ExtraAttributesException $th + */ + $exceptionAsArray = [ + 'extraAttributes' => $th->getExtraAttributes(), + ]; + + $expected = [ + 'extraAttributes' => [ + 'collection[0].extra2', + ], + ]; + + $this->assertSame($expected, $exceptionAsArray); + } + + public function testCollectExtraAttributesErrors() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $json = ' + { + "extra1": true, + "collection": [ + { + "extra2": true + }, + { + "extra3": true + } + ], + "nestedObject2": { + "extra4": true + } + }'; + + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + + $serializer = new Serializer( + [ + new ArrayDenormalizer(), + new DateTimeNormalizer(), + new DateTimeZoneNormalizer(), + new DataUriNormalizer(), + new UidNormalizer(), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), + ], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize($json, Php74Full::class, 'json', [ + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, + DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => true, + ]); + + $this->fail(); + } catch (\Throwable $th) { + $this->assertInstanceOf(PartialDenormalizationException::class, $th); + /** @var PartialDenormalizationException $th */ + $this->assertInstanceOf(ExtraAttributesException::class, $extraAttributeError = $th->getExtraAttributesError()); + } + + $this->assertInstanceOf(Php74Full::class, $th->getData()); + + $exceptionAsArray = [ + 'extraAttributes' => $extraAttributeError->getExtraAttributes(), + ]; + + $expected = [ + 'extraAttributes' => [ + 'collection[0].extra2', + 'collection[1].extra3', + 'nestedObject2.extra4', + 'extra1', + ], + ]; + + $this->assertSame($expected, $exceptionAsArray); + } + + public function testCollectDenormalizationAndExtraAttributesErrors() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $json = ' + { + "string": null, + "extra1": true + }'; + + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + + $serializer = new Serializer( + [ + new ArrayDenormalizer(), + new DateTimeNormalizer(), + new DateTimeZoneNormalizer(), + new DataUriNormalizer(), + new UidNormalizer(), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), + ], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize($json, Php74Full::class, 'json', [ + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => true, + ]); + + $this->fail(); + } catch (\Throwable $th) { + $this->assertInstanceOf(PartialDenormalizationException::class, $th); + /** @var PartialDenormalizationException $th */ + $this->assertInstanceOf(ExtraAttributesException::class, $extraAttributeError = $th->getExtraAttributesError()); + } + + $this->assertInstanceOf(Php74Full::class, $th->getData()); + + $extraAttributesExceptionAsArray = [ + 'extraAttributes' => $extraAttributeError->getExtraAttributes(), + ]; + + $expectedExtraAttributesException = [ + 'extraAttributes' => [ + 'extra1', + ], + ]; + + $this->assertSame($expectedExtraAttributesException, $extraAttributesExceptionAsArray); + + $exceptionsAsArray = array_map(static function (NotNormalizableValueException $e): array { + return [ + 'currentType' => $e->getCurrentType(), + 'expectedTypes' => $e->getExpectedTypes(), + 'path' => $e->getPath(), + 'useMessageForUser' => $e->canUseMessageForUser(), + 'message' => $e->getMessage(), + ]; + }, $th->getNotNormalizableValueErrors()); + + $expectedExceptions = [[ + 'currentType' => 'null', + 'expectedTypes' => [ + 'string', + ], + 'path' => 'string', + 'useMessageForUser' => false, + 'message' => 'The type of the "string" attribute for class "Symfony\\Component\\Serializer\\Tests\\Fixtures\\Php74Full" must be one of "string" ("null" given).', + ]]; + + $this->assertSame($expectedExceptions, $exceptionsAsArray); + } + /** * @dataProvider provideCollectDenormalizationErrors */ @@ -1156,9 +1356,10 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa $this->assertInstanceOf(PartialDenormalizationException::class, $th); } + /** @var PartialDenormalizationException $th */ $this->assertInstanceOf(Php80WithPromotedTypedConstructor::class, $th->getData()); - $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + $exceptionsAsArray = array_map(static function (NotNormalizableValueException $e): array { return [ 'currentType' => $e->getCurrentType(), 'expectedTypes' => $e->getExpectedTypes(), @@ -1166,7 +1367,7 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa 'useMessageForUser' => $e->canUseMessageForUser(), 'message' => $e->getMessage(), ]; - }, $th->getErrors()); + }, $th->getNotNormalizableValueErrors()); $expected = [ [ @@ -1201,7 +1402,7 @@ public function testCollectDenormalizationErrorsWithEnumConstructor() $this->assertInstanceOf(PartialDenormalizationException::class, $th); } - $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + $exceptionsAsArray = array_map(static function (NotNormalizableValueException $e): array { return [ 'currentType' => $e->getCurrentType(), 'useMessageForUser' => $e->canUseMessageForUser(),