diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 245c16ca6abdb..dc9c748810a27 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -52,6 +52,15 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer */ public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement'; + /** + * While denormalizing object type boolean fields try to parse string values + * into boolean values. + * + * ["true", "yes", "on", "1"] => true + * ["false", "no", "off", "0", ""] => false + */ + public const ENABLE_STRING_BOOL_VALUE = 'enable_string_bool_values'; + /** * Flag to control whether fields with the value `null` should be output * when normalizing or omitted. @@ -573,6 +582,14 @@ private function validateAndDenormalize(array $types, string $currentClass, stri return (float) $data; } + if (($context[self::ENABLE_STRING_BOOL_VALUE] ?? $this->defaultContext[self::ENABLE_STRING_BOOL_VALUE] ?? false) && Type::BUILTIN_TYPE_BOOL === $builtinType && \is_string($data)) { + if (null !== $booleanValue = filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE)) { + return $booleanValue; + } + + throw new NotNormalizableValueException(sprintf('The value of the "%s" attribute for class "%s" must be one of "true", "yes", "on", "1", "false", "no", "off", "0", "" ("%s" given).', $attribute, $currentClass, $data)); + } + if (('is_'.$builtinType)($data)) { return $data; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index aa142ed3b4b05..532d87c061ec1 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -362,6 +362,83 @@ private function getDenormalizerForObjectWithBasicProperties() return $denormalizer; } + public function testDenormalizeBasicTypePropertiesWithStringBoolValues() + { + $denormalizer = $this->getDenormalizerForObjectWithStringBoolValues(); + + // bool + $objectWithBooleanProperties = $denormalizer->denormalize( + [ + 'true' => 'true', + 'yes' => 'yEs', + 'on' => 'ON', + 'one' => '1', + 'false' => 'falsE', + 'no' => 'No', + 'off' => 'OfF', + 'zero' => '0', + 'empty' => '', + ], + ObjectWithStringBoolean::class, + null, + [AbstractObjectNormalizer::ENABLE_STRING_BOOL_VALUE => true] + ); + + $this->assertInstanceOf(ObjectWithStringBoolean::class, $objectWithBooleanProperties); + + $this->assertTrue($objectWithBooleanProperties->true); + $this->assertTrue($objectWithBooleanProperties->yes); + $this->assertTrue($objectWithBooleanProperties->on); + $this->assertTrue($objectWithBooleanProperties->one); + $this->assertFalse($objectWithBooleanProperties->false); + $this->assertFalse($objectWithBooleanProperties->no); + $this->assertFalse($objectWithBooleanProperties->off); + $this->assertFalse($objectWithBooleanProperties->zero); + $this->assertFalse($objectWithBooleanProperties->empty); + } + + public function testDenormalizeBasicTypePropertiesWithBadStringBoolValues() + { + $denormalizer = $this->getDenormalizerForObjectWithStringBoolValues(); + + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('The value of the "yes" attribute for class "Symfony\Component\Serializer\Tests\Normalizer\ObjectWithStringBoolean" must be one of "true", "yes", "on", "1", "false", "no", "off", "0", "" ("foo" given).'); + + $objectWithBooleanProperties = $denormalizer->denormalize( + [ + 'yes' => 'foo', + ], + ObjectWithStringBoolean::class, + null, + [AbstractObjectNormalizer::ENABLE_STRING_BOOL_VALUE => true] + ); + } + + private function getDenormalizerForObjectWithStringBoolValues() + { + $extractor = $this->createMock(PhpDocExtractor::class); + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('bool')] + )); + + $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); + $arrayDenormalizer = new ArrayDenormalizerDummy(); + $serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]); + $arrayDenormalizer->setSerializer($serializer); + $denormalizer->setSerializer($serializer); + + return $denormalizer; + } + /** * Test that additional attributes throw an exception if no metadata factory is specified. */ @@ -416,6 +493,36 @@ public function instantiateObject(array &$data, string $class, array &$context, } } +class ObjectWithStringBoolean +{ + /** @var bool */ + public $true; + + /** @var bool */ + public $yes; + + /** @var bool */ + public $on; + + /** @var bool */ + public $one; + + /** @var bool */ + public $false; + + /** @var bool */ + public $no; + + /** @var bool */ + public $off; + + /** @var bool */ + public $zero; + + /** @var bool */ + public $empty; +} + class Dummy { public $foo;