diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 80ea6903dad25..791a744d9dadd 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -378,32 +378,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, $context, $format); + $params[$paramName] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $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 ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) { - $params[] = null; + $params[$paramName] = null; } else { if (!isset($context['not_normalizable_value_exceptions'])) { $missingConstructorArguments[] = $constructorParameter->name; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Php80WithOptionalConstructorParameter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Php80WithOptionalConstructorParameter.php new file mode 100644 index 0000000000000..6593635df4125 --- /dev/null +++ b/src/Symfony/Component/Serializer/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/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 265c3f2c3d246..447e0f882a8c5 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -69,6 +69,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy; 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\WithTypedConstructor; @@ -1433,6 +1434,61 @@ public static function provideCollectDenormalizationErrors() [new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))], ]; } + + /** + * @requires PHP 8 + */ + 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