Skip to content

Commit 3824daf

Browse files
mkrauserfabpot
authored andcommitted
[Serializer] fix denormalization of basic property-types in XML and CSV
1 parent 78eca96 commit 3824daf

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

+57
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1616
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
1717
use Symfony\Component\PropertyInfo\Type;
18+
use Symfony\Component\Serializer\Encoder\CsvEncoder;
1819
use Symfony\Component\Serializer\Encoder\JsonEncoder;
20+
use Symfony\Component\Serializer\Encoder\XmlEncoder;
1921
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
2022
use Symfony\Component\Serializer\Exception\LogicException;
2123
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -379,6 +381,61 @@ private function validateAndDenormalize(string $currentClass, string $attribute,
379381
$data = [$data];
380382
}
381383

384+
// In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
385+
// if a value is meant to be a string, float, int or a boolean value from the serialized representation.
386+
// That's why we have to transform the values, if one of these non-string basic datatypes is expected.
387+
//
388+
// This is special to xml and csv format
389+
if (
390+
\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)
391+
) {
392+
if (
393+
'' === $data && $type->isNullable() && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)
394+
) {
395+
return null;
396+
}
397+
398+
switch ($type->getBuiltinType()) {
399+
case Type::BUILTIN_TYPE_BOOL:
400+
// according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
401+
if ('false' === $data || '0' === $data) {
402+
$data = false;
403+
} elseif ('true' === $data || '1' === $data) {
404+
$data = true;
405+
} else {
406+
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data));
407+
}
408+
break;
409+
case Type::BUILTIN_TYPE_INT:
410+
if (
411+
ctype_digit($data) ||
412+
'-' === $data[0] && ctype_digit(substr($data, 1))
413+
) {
414+
$data = (int) $data;
415+
} else {
416+
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data));
417+
}
418+
break;
419+
case Type::BUILTIN_TYPE_FLOAT:
420+
if (is_numeric($data)) {
421+
return (float) $data;
422+
}
423+
424+
switch ($data) {
425+
case 'NaN':
426+
return NAN;
427+
case 'INF':
428+
return INF;
429+
case '-INF':
430+
return -INF;
431+
default:
432+
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data));
433+
}
434+
435+
break;
436+
}
437+
}
438+
382439
if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
383440
$builtinType = Type::BUILTIN_TYPE_OBJECT;
384441
$class = $collectionValueType->getClassName().'[]';

src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php

+112
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,79 @@ public function getTypeForMappedObject($object): ?string
273273
$this->assertInstanceOf(AbstractDummySecondChild::class, $denormalizedData);
274274
}
275275

276+
public function testDenormalizeBasicTypePropertiesFromXml()
277+
{
278+
$denormalizer = $this->getDenormalizerForObjectWithBasicProperties();
279+
280+
// bool
281+
$objectWithBooleanProperties = $denormalizer->denormalize(
282+
[
283+
'boolTrue1' => 'true',
284+
'boolFalse1' => 'false',
285+
'boolTrue2' => '1',
286+
'boolFalse2' => '0',
287+
'int1' => '4711',
288+
'int2' => '-4711',
289+
'float1' => '123.456',
290+
'float2' => '-1.2344e56',
291+
'float3' => '45E-6',
292+
'floatNaN' => 'NaN',
293+
'floatInf' => 'INF',
294+
'floatNegInf' => '-INF',
295+
],
296+
ObjectWithBasicProperties::class,
297+
'xml'
298+
);
299+
300+
$this->assertInstanceOf(ObjectWithBasicProperties::class, $objectWithBooleanProperties);
301+
302+
// Bool Properties
303+
$this->assertTrue($objectWithBooleanProperties->boolTrue1);
304+
$this->assertFalse($objectWithBooleanProperties->boolFalse1);
305+
$this->assertTrue($objectWithBooleanProperties->boolTrue2);
306+
$this->assertFalse($objectWithBooleanProperties->boolFalse2);
307+
308+
// Integer Properties
309+
$this->assertEquals(4711, $objectWithBooleanProperties->int1);
310+
$this->assertEquals(-4711, $objectWithBooleanProperties->int2);
311+
312+
// Float Properties
313+
$this->assertEqualsWithDelta(123.456, $objectWithBooleanProperties->float1, 0.01);
314+
$this->assertEqualsWithDelta(-1.2344e56, $objectWithBooleanProperties->float2, 1);
315+
$this->assertEqualsWithDelta(45E-6, $objectWithBooleanProperties->float3, 1);
316+
$this->assertNan($objectWithBooleanProperties->floatNaN);
317+
$this->assertInfinite($objectWithBooleanProperties->floatInf);
318+
$this->assertEquals(-INF, $objectWithBooleanProperties->floatNegInf);
319+
}
320+
321+
private function getDenormalizerForObjectWithBasicProperties()
322+
{
323+
$extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock();
324+
$extractor->method('getTypes')
325+
->will($this->onConsecutiveCalls(
326+
[new Type('bool')],
327+
[new Type('bool')],
328+
[new Type('bool')],
329+
[new Type('bool')],
330+
[new Type('int')],
331+
[new Type('int')],
332+
[new Type('float')],
333+
[new Type('float')],
334+
[new Type('float')],
335+
[new Type('float')],
336+
[new Type('float')],
337+
[new Type('float')]
338+
));
339+
340+
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
341+
$arrayDenormalizer = new ArrayDenormalizerDummy();
342+
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
343+
$arrayDenormalizer->setSerializer($serializer);
344+
$denormalizer->setSerializer($serializer);
345+
346+
return $denormalizer;
347+
}
348+
276349
/**
277350
* Test that additional attributes throw an exception if no metadata factory is specified.
278351
*/
@@ -359,6 +432,45 @@ protected function setAttributeValue(object $object, string $attribute, $value,
359432
}
360433
}
361434

435+
class ObjectWithBasicProperties
436+
{
437+
/** @var bool */
438+
public $boolTrue1;
439+
440+
/** @var bool */
441+
public $boolFalse1;
442+
443+
/** @var bool */
444+
public $boolTrue2;
445+
446+
/** @var bool */
447+
public $boolFalse2;
448+
449+
/** @var int */
450+
public $int1;
451+
452+
/** @var int */
453+
public $int2;
454+
455+
/** @var float */
456+
public $float1;
457+
458+
/** @var float */
459+
public $float2;
460+
461+
/** @var float */
462+
public $float3;
463+
464+
/** @var float */
465+
public $floatNaN;
466+
467+
/** @var float */
468+
public $floatInf;
469+
470+
/** @var float */
471+
public $floatNegInf;
472+
}
473+
362474
class StringCollection
363475
{
364476
/** @var string[] */

0 commit comments

Comments
 (0)