diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index b329cf1542334..64bb1e8952997 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION` for scalar type transformation + 7.0 --- diff --git a/src/Symfony/Component/Serializer/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php b/src/Symfony/Component/Serializer/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php index a27f00c5ba80c..19a2e6e794beb 100644 --- a/src/Symfony/Component/Serializer/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php +++ b/src/Symfony/Component/Serializer/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php @@ -61,6 +61,14 @@ public function withDisableTypeEnforcement(?bool $disableTypeEnforcement): stati return $this->with(AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT, $disableTypeEnforcement); } + /** + * Configures whether to convert scalar types to the expected type, if their value is compatible. + */ + public function withEnableTypeConversion(?bool $enableTypeConversion): static + { + return $this->with(AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION, $enableTypeConversion); + } + /** * Configures whether fields with the value `null` should be output during normalization. */ diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 4ca328d591beb..eeb56cb2f6f1c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -57,6 +57,13 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer */ public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement'; + /** + * While denormalizing, convert scalar types to the expected type. + * + * If not defined, it will be enabled for XML and CSV format, because all basic datatypes are represented as strings. + */ + public const ENABLE_TYPE_CONVERSION = 'enable_type_conversion'; + /** * Flag to control whether fields with the value `null` should be output * when normalizing or omitted. @@ -294,6 +301,8 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a return null; } + $context[self::ENABLE_TYPE_CONVERSION] = $context[self::ENABLE_TYPE_CONVERSION] ?? (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format); + $allowedAttributes = $this->getAllowedAttributes($type, $context, true); $normalizedData = $this->prepareForDenormalization($data); $extraAttributes = []; @@ -438,7 +447,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri // if a value is meant to be a string, float, int or a boolean value from the serialized representation. // That's why we have to transform the values, if one of these non-string basic datatypes is expected. $builtinType = $type->getBuiltinType(); - if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { + if (\is_string($data) && ($context[self::ENABLE_TYPE_CONVERSION] ?? $this->defaultContext[self::ENABLE_TYPE_CONVERSION] ?? false)) { if ('' === $data) { if (Type::BUILTIN_TYPE_ARRAY === $builtinType) { return []; diff --git a/src/Symfony/Component/Serializer/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php b/src/Symfony/Component/Serializer/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php index 410f2972b0258..1a12c1a8d4653 100644 --- a/src/Symfony/Component/Serializer/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php @@ -39,6 +39,7 @@ public function testWithers(array $values) ->withEnableMaxDepth($values[AbstractObjectNormalizer::ENABLE_MAX_DEPTH]) ->withDepthKeyPattern($values[AbstractObjectNormalizer::DEPTH_KEY_PATTERN]) ->withDisableTypeEnforcement($values[AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT]) + ->withEnableTypeConversion($values[AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION]) ->withSkipNullValues($values[AbstractObjectNormalizer::SKIP_NULL_VALUES]) ->withSkipUninitializedValues($values[AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES]) ->withMaxDepthHandler($values[AbstractObjectNormalizer::MAX_DEPTH_HANDLER]) @@ -59,6 +60,7 @@ public static function withersDataProvider(): iterable AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true, AbstractObjectNormalizer::DEPTH_KEY_PATTERN => '%s_%s', AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => false, + AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION => false, AbstractObjectNormalizer::SKIP_NULL_VALUES => true, AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => false, AbstractObjectNormalizer::MAX_DEPTH_HANDLER => static function (): void {}, @@ -71,6 +73,7 @@ public static function withersDataProvider(): iterable AbstractObjectNormalizer::ENABLE_MAX_DEPTH => null, AbstractObjectNormalizer::DEPTH_KEY_PATTERN => null, AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => null, + AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION => null, AbstractObjectNormalizer::SKIP_NULL_VALUES => null, AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => null, AbstractObjectNormalizer::MAX_DEPTH_HANDLER => null, diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 4ebe11d915767..befa298ec1c62 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -627,7 +627,10 @@ public function getTypeForMappedObject($object): ?string $this->assertInstanceOf(AbstractDummySecondChild::class, $denormalizedData); } - public function testDenormalizeBasicTypePropertiesFromXml() + /** + * @dataProvider denormalizeBasicTypePropertiesConversionDataProvider + */ + public function testDenormalizeBasicTypePropertiesConversion(string $format, array $context = []) { $denormalizer = $this->getDenormalizerForObjectWithBasicProperties(); @@ -648,7 +651,8 @@ public function testDenormalizeBasicTypePropertiesFromXml() 'floatNegInf' => '-INF', ], ObjectWithBasicProperties::class, - 'xml' + $format, + $context ); $this->assertInstanceOf(ObjectWithBasicProperties::class, $objectWithBooleanProperties); @@ -672,6 +676,30 @@ public function testDenormalizeBasicTypePropertiesFromXml() $this->assertEquals(-\INF, $objectWithBooleanProperties->floatNegInf); } + public function testDenormalizeBasicTypePropertiesThrowsWithoutTypeConversion() + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessageMatches('/must be one of "bool" \("string" given\)/'); + $denormalizer = $this->getDenormalizerForObjectWithBasicProperties(); + $denormalizer->denormalize( + [ + 'boolTrue1' => 'true', + ], + ObjectWithBasicProperties::class, + 'other', + [AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION => false] + ); + } + + public function denormalizeBasicTypePropertiesConversionDataProvider(): array + { + return [ + ['xml', []], + ['csv', []], + ['other', [AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION => true]], + ]; + } + private function getDenormalizerForObjectWithBasicProperties() { $extractor = $this->createMock(PhpDocExtractor::class);