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(), 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