diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index ea4b4b6635..aa5816e075 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -15,7 +15,9 @@ use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; +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\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -379,6 +381,61 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer $data = [$data]; } + // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine, + // 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. + // + // This is special to xml and csv format + if ( + \is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format) + ) { + if ( + '' === $data && $type->isNullable() && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true) + ) { + return null; + } + + switch ($type->getBuiltinType()) { + case Type::BUILTIN_TYPE_BOOL: + // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" + if ('false' === $data || '0' === $data) { + $data = false; + } elseif ('true' === $data || '1' === $data) { + $data = true; + } else { + throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data)); + } + break; + case Type::BUILTIN_TYPE_INT: + if ( + ctype_digit($data) || + '-' === $data[0] && ctype_digit(substr($data, 1)) + ) { + $data = (int) $data; + } else { + throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data)); + } + break; + case Type::BUILTIN_TYPE_FLOAT: + if (is_numeric($data)) { + return (float) $data; + } + + switch ($data) { + case 'NaN': + return NAN; + case 'INF': + return INF; + case '-INF': + return -INF; + default: + throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data)); + } + + break; + } + } + if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { $builtinType = Type::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 155ee13924..b08e74660c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -273,6 +273,79 @@ class AbstractObjectNormalizerTest extends TestCase $this->assertInstanceOf(AbstractDummySecondChild::class, $denormalizedData); } + public function testDenormalizeBasicTypePropertiesFromXml() + { + $denormalizer = $this->getDenormalizerForObjectWithBasicProperties(); + + // bool + $objectWithBooleanProperties = $denormalizer->denormalize( + [ + 'boolTrue1' => 'true', + 'boolFalse1' => 'false', + 'boolTrue2' => '1', + 'boolFalse2' => '0', + 'int1' => '4711', + 'int2' => '-4711', + 'float1' => '123.456', + 'float2' => '-1.2344e56', + 'float3' => '45E-6', + 'floatNaN' => 'NaN', + 'floatInf' => 'INF', + 'floatNegInf' => '-INF', + ], + ObjectWithBasicProperties::class, + 'xml' + ); + + $this->assertInstanceOf(ObjectWithBasicProperties::class, $objectWithBooleanProperties); + + // Bool Properties + $this->assertTrue($objectWithBooleanProperties->boolTrue1); + $this->assertFalse($objectWithBooleanProperties->boolFalse1); + $this->assertTrue($objectWithBooleanProperties->boolTrue2); + $this->assertFalse($objectWithBooleanProperties->boolFalse2); + + // Integer Properties + $this->assertEquals(4711, $objectWithBooleanProperties->int1); + $this->assertEquals(-4711, $objectWithBooleanProperties->int2); + + // Float Properties + $this->assertEqualsWithDelta(123.456, $objectWithBooleanProperties->float1, 0.01); + $this->assertEqualsWithDelta(-1.2344e56, $objectWithBooleanProperties->float2, 1); + $this->assertEqualsWithDelta(45E-6, $objectWithBooleanProperties->float3, 1); + $this->assertNan($objectWithBooleanProperties->floatNaN); + $this->assertInfinite($objectWithBooleanProperties->floatInf); + $this->assertEquals(-INF, $objectWithBooleanProperties->floatNegInf); + } + + private function getDenormalizerForObjectWithBasicProperties() + { + $extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock(); + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('bool')], + [new Type('int')], + [new Type('int')], + [new Type('float')], + [new Type('float')], + [new Type('float')], + [new Type('float')], + [new Type('float')], + [new Type('float')] + )); + + $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. */ @@ -359,6 +432,45 @@ class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer } } +class ObjectWithBasicProperties +{ + /** @var bool */ + public $boolTrue1; + + /** @var bool */ + public $boolFalse1; + + /** @var bool */ + public $boolTrue2; + + /** @var bool */ + public $boolFalse2; + + /** @var int */ + public $int1; + + /** @var int */ + public $int2; + + /** @var float */ + public $float1; + + /** @var float */ + public $float2; + + /** @var float */ + public $float3; + + /** @var float */ + public $floatNaN; + + /** @var float */ + public $floatInf; + + /** @var float */ + public $floatNegInf; +} + class StringCollection { /** @var string[] */