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:
commit
4753e4d712
@ -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().'[]';
|
||||
|
@ -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[] */
|
||||
|
Reference in New Issue
Block a user