feature #33850 [Serializer] fix denormalization of basic property-types in XML and CSV (mkrauser)

This PR was submitted for the 3.4 branch but it was squashed and merged into the 5.2-dev branch instead.

Discussion
----------

[Serializer] fix denormalization of basic property-types in XML and CSV

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #33849
| License       | MIT
| Doc PR        |

Like I explained in the Issue, the serializer cannot de-serialize non-string basic properties (int, float, bool). This PR add's some logic to cast to the expected types.

Similar logic is already present in the [XmlUtils](https://github.com/symfony/symfony/blob/4.4/src/Symfony/Component/Config/Util/XmlUtils.php#L215)-Class of the Config-Component

Commits
-------

3824dafffb [Serializer] fix denormalization of basic property-types in XML and CSV
This commit is contained in:
Fabien Potencier 2020-09-02 07:44:29 +02:00
commit 4753e4d712
2 changed files with 169 additions and 0 deletions

View File

@ -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().'[]';

View File

@ -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[] */