diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 2e11136ba..06c316eb0 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -367,32 +367,32 @@ protected function instantiateObject(array &$data, string $class, array &$contex } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { $parameterData = $data[$key]; if (null === $parameterData && $constructorParameter->allowsNull()) { - $params[] = null; + $params[$paramName] = null; $unsetKeys[] = $key; continue; } try { - $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format); + $params[$paramName] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format); } catch (NotNormalizableValueException $exception) { if (!isset($context['not_normalizable_value_exceptions'])) { throw $exception; } $context['not_normalizable_value_exceptions'][] = $exception; - $params[] = $parameterData; + $params[$paramName] = $parameterData; } $unsetKeys[] = $key; } elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) { - $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; + $params[$paramName] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; } elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) { - $params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; + $params[$paramName] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; } elseif ($constructorParameter->isDefaultValueAvailable()) { - $params[] = $constructorParameter->getDefaultValue(); + $params[$paramName] = $constructorParameter->getDefaultValue(); } elseif (!($context[self::REQUIRE_ALL_PROPERTIES] ?? $this->defaultContext[self::REQUIRE_ALL_PROPERTIES] ?? false) && $constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) { - $params[] = null; + $params[$paramName] = null; } else { if (!isset($context['not_normalizable_value_exceptions'])) { $missingConstructorArguments[] = $constructorParameter->name; @@ -445,6 +445,15 @@ protected function instantiateObject(array &$data, string $class, array &$contex unset($context['has_constructor']); + if (!$reflectionClass->isInstantiable()) { + throw NotNormalizableValueException::createForUnexpectedDataType( + sprintf('Failed to create object because the class "%s" is not instantiable.', $class), + $data, + ['unknown'], + $context['deserialization_path'] ?? null + ); + } + return new $class(); } diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index a6dbf3dc9..9c6098b5f 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -190,12 +190,7 @@ public function normalize(mixed $object, string $format = null, array $context = $attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($object)?->getTypeProperty() ? $this->classDiscriminatorResolver?->getTypeForMappedObject($object) : $this->getAttributeValue($object, $attribute, $format, $attributeContext); - } catch (UninitializedPropertyException $e) { - if ($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) { - continue; - } - throw $e; - } catch (\Error $e) { + } catch (UninitializedPropertyException|\Error $e) { if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e)) { continue; } @@ -373,6 +368,10 @@ public function denormalize(mixed $data, string $type, string $format = null, ar ? $discriminatorMapping : $this->getAttributeValue($object, $attribute, $format, $attributeContext); } catch (NoSuchPropertyException) { + } catch (UninitializedPropertyException|\Error $e) { + if (!(($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e))) { + throw $e; + } } } @@ -491,7 +490,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri } break; case Type::BUILTIN_TYPE_INT: - if (ctype_digit('-' === $data[0] ? substr($data, 1) : $data)) { + if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) { $data = (int) $data; } else { throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); @@ -769,9 +768,10 @@ private function getCacheKey(?string $format, array $context): bool|string * This error may occur when specific object normalizer implementation gets attribute value * by accessing a public uninitialized property or by calling a method accessing such property. */ - private function isUninitializedValueError(\Error $e): bool + private function isUninitializedValueError(\Error|UninitializedPropertyException $e): bool { - return str_starts_with($e->getMessage(), 'Typed property') + return $e instanceof UninitializedPropertyException + || str_starts_with($e->getMessage(), 'Typed property') && str_ends_with($e->getMessage(), 'must not be accessed before initialization'); } diff --git a/Normalizer/BackedEnumNormalizer.php b/Normalizer/BackedEnumNormalizer.php index 393479447..fc7a70188 100644 --- a/Normalizer/BackedEnumNormalizer.php +++ b/Normalizer/BackedEnumNormalizer.php @@ -77,7 +77,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar return $type::from($data); } catch (\ValueError $e) { if (isset($context['has_constructor'])) { - throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type); + throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type, 0, $e); } throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e); diff --git a/Serializer.php b/Serializer.php index ff28cbaaf..f65281492 100644 --- a/Serializer.php +++ b/Serializer.php @@ -193,6 +193,7 @@ public function normalize(mixed $data, string $format = null, array $context = [ /** * @throws NotNormalizableValueException + * @throws PartialDenormalizationException Occurs when one or more properties of $type fails to denormalize */ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed { diff --git a/Tests/Fixtures/DummyNullableInt.php b/Tests/Fixtures/DummyNullableInt.php new file mode 100644 index 000000000..2671f66a9 --- /dev/null +++ b/Tests/Fixtures/DummyNullableInt.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +/** + * @author Nicolas PHILIPPE + */ +class DummyNullableInt +{ + public int|null $value = null; +} diff --git a/Tests/Php80Dummy.php b/Tests/Fixtures/Php80Dummy.php similarity index 84% rename from Tests/Php80Dummy.php rename to Tests/Fixtures/Php80Dummy.php index baa75b124..85c354314 100644 --- a/Tests/Php80Dummy.php +++ b/Tests/Fixtures/Php80Dummy.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Serializer\Tests; +namespace Symfony\Component\Serializer\Tests\Fixtures; final class Php80Dummy { diff --git a/Tests/Fixtures/Php80WithOptionalConstructorParameter.php b/Tests/Fixtures/Php80WithOptionalConstructorParameter.php new file mode 100644 index 000000000..6593635df --- /dev/null +++ b/Tests/Fixtures/Php80WithOptionalConstructorParameter.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +final class Php80WithOptionalConstructorParameter +{ + public function __construct( + public string $one, + public string $two, + public ?string $three = null, + ) { + } +} diff --git a/Tests/Normalizer/AbstractNormalizerTest.php b/Tests/Normalizer/AbstractNormalizerTest.php index 924f07e34..a2f39206f 100644 --- a/Tests/Normalizer/AbstractNormalizerTest.php +++ b/Tests/Normalizer/AbstractNormalizerTest.php @@ -16,6 +16,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -33,6 +34,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\NullableOptionalConstructorArgumentDummy; use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorDummy; use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorNormalizer; +use Symfony\Component\Serializer\Tests\Fixtures\UnitEnumDummy; use Symfony\Component\Serializer\Tests\Fixtures\VariadicConstructorTypedArgsDummy; /** @@ -280,4 +282,16 @@ public function testIgnore() $this->assertSame([], $normalizer->normalize($dummy)); } + + /** + * @requires PHP 8.1.2 + */ + public function testDenormalizeWhenObjectNotInstantiable() + { + $this->expectException(NotNormalizableValueException::class); + + $normalizer = new ObjectNormalizer(); + + $normalizer->denormalize('{}', UnitEnumDummy::class); + } } diff --git a/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php b/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php index fb055abd1..dfcb904ab 100644 --- a/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php +++ b/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php @@ -12,14 +12,14 @@ namespace Symfony\Component\Serializer\Tests\Normalizer\Features; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; /** * Test AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES. */ trait SkipUninitializedValuesTestTrait { - abstract protected function getNormalizerForSkipUninitializedValues(): NormalizerInterface; + abstract protected function getNormalizerForSkipUninitializedValues(): AbstractObjectNormalizer; /** * @dataProvider skipUninitializedValuesFlagProvider @@ -31,6 +31,15 @@ public function testSkipUninitializedValues(array $context) $normalizer = $this->getNormalizerForSkipUninitializedValues(); $result = $normalizer->normalize($object, null, $context); $this->assertSame(['initialized' => 'value'], $result); + + $normalizer->denormalize( + ['unInitialized' => 'value'], + TypedPropertiesObjectWithGetters::class, + null, + ['object_to_populate' => $objectToPopulate = new TypedPropertiesObjectWithGetters(), 'deep_object_to_populate' => true] + $context + ); + + $this->assertSame('value', $objectToPopulate->getUninitialized()); } public function skipUninitializedValuesFlagProvider(): iterable diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 1d471981e..7877a3c5e 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -487,7 +487,7 @@ protected function getNormalizerForCacheableObjectAttributesTest(): GetSetMethod return new GetSetMethodNormalizer(); } - protected function getNormalizerForSkipUninitializedValues(): NormalizerInterface + protected function getNormalizerForSkipUninitializedValues(): GetSetMethodNormalizer { return new GetSetMethodNormalizer(new ClassMetadataFactory(new AttributeLoader())); } diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 30bbecbc8..281c8a055 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -41,6 +41,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\OtherSerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74DummyPrivate; +use Symfony\Component\Serializer\Tests\Fixtures\Php80Dummy; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait; @@ -58,7 +59,6 @@ use Symfony\Component\Serializer\Tests\Normalizer\Features\TypedPropertiesObject; use Symfony\Component\Serializer\Tests\Normalizer\Features\TypedPropertiesObjectWithGetters; use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait; -use Symfony\Component\Serializer\Tests\Php80Dummy; /** * @author Kévin Dunglas diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 631111d2a..0601a2d60 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -25,7 +25,6 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; @@ -492,7 +491,7 @@ protected function getNormalizerForCacheableObjectAttributesTest(): AbstractObje return new PropertyNormalizer(); } - protected function getNormalizerForSkipUninitializedValues(): NormalizerInterface + protected function getNormalizerForSkipUninitializedValues(): PropertyNormalizer { return new PropertyNormalizer(new ClassMetadataFactory(new AttributeLoader())); } diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index e61678b03..6bceb93c2 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; @@ -56,6 +57,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberThree; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo; +use Symfony\Component\Serializer\Tests\Fixtures\DummyNullableInt; use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor; use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; @@ -65,6 +67,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy; use Symfony\Component\Serializer\Tests\Fixtures\ObjectCollectionPropertyDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Full; +use Symfony\Component\Serializer\Tests\Fixtures\Php80WithOptionalConstructorParameter; use Symfony\Component\Serializer\Tests\Fixtures\Php80WithPromotedTypedConstructor; use Symfony\Component\Serializer\Tests\Fixtures\TraversableDummy; use Symfony\Component\Serializer\Tests\Fixtures\TrueBuiltInDummy; @@ -751,6 +754,16 @@ public function testDeserializeWrappedScalar() $this->assertSame(42, $serializer->deserialize('{"wrapper": 42}', 'int', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[wrapper]'])); } + public function testDeserializeNullableIntInXml() + { + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['xml' => new XmlEncoder()]); + + $obj = $serializer->deserialize('', DummyNullableInt::class, 'xml'); + $this->assertInstanceOf(DummyNullableInt::class, $obj); + $this->assertNull($obj->value); + } + public function testUnionTypeDeserializable() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); @@ -1508,6 +1521,58 @@ public function testSerializerUsesSupportedTypesMethod() $serializer->denormalize('foo', Model::class, 'json'); $serializer->denormalize('foo', Model::class, 'json'); } + + public function testPartialDenormalizationWithMissingConstructorTypes() + { + $json = '{"one": "one string", "three": "three string"}'; + + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + + $serializer = new Serializer( + [new ObjectNormalizer(null, null, null, $extractor)], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize($json, Php80WithOptionalConstructorParameter::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + + $this->fail(); + } catch (\Throwable $th) { + $this->assertInstanceOf(PartialDenormalizationException::class, $th); + } + + $this->assertInstanceOf(Php80WithOptionalConstructorParameter::class, $object = $th->getData()); + + $this->assertSame('one string', $object->one); + $this->assertFalse(isset($object->two)); + $this->assertSame('three string', $object->three); + + $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + return [ + 'currentType' => $e->getCurrentType(), + 'expectedTypes' => $e->getExpectedTypes(), + 'path' => $e->getPath(), + 'useMessageForUser' => $e->canUseMessageForUser(), + 'message' => $e->getMessage(), + ]; + }, $th->getErrors()); + + $expected = [ + [ + 'currentType' => 'array', + 'expectedTypes' => [ + 'unknown', + ], + 'path' => null, + 'useMessageForUser' => true, + 'message' => 'Failed to create object because the class misses the "two" property.', + ], + ]; + + $this->assertSame($expected, $exceptionsAsArray); + } } class Model