Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 64eaf96

Browse files
committed
Fix denormalizing empty string into object|null parameter
1 parent 48be4b3 commit 64eaf96

9 files changed

+245
-7
lines changed

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -457,8 +457,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
457457
{
458458
$expectedTypes = [];
459459
$isUnionType = \count($types) > 1;
460+
$e = null;
460461
$extraAttributesException = null;
461462
$missingConstructorArgumentException = null;
463+
$isNullable = false;
462464
foreach ($types as $type) {
463465
if (null === $data && $type->isNullable()) {
464466
return null;
@@ -481,18 +483,22 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
481483
// In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
482484
// if a value is meant to be a string, float, int or a boolean value from the serialized representation.
483485
// That's why we have to transform the values, if one of these non-string basic datatypes is expected.
486+
$builtinType = $type->getBuiltinType();
484487
if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
485488
if ('' === $data) {
486-
if (Type::BUILTIN_TYPE_ARRAY === $builtinType = $type->getBuiltinType()) {
489+
if (Type::BUILTIN_TYPE_ARRAY === $builtinType) {
487490
return [];
488491
}
489492

490-
if ($type->isNullable() && \in_array($builtinType, [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) {
491-
return null;
493+
if (Type::BUILTIN_TYPE_STRING === $builtinType) {
494+
return '';
492495
}
496+
497+
// Don't return null yet because Object-types that come first may accept empty-string too
498+
$isNullable = $isNullable ?: $type->isNullable();
493499
}
494500

495-
switch ($builtinType ?? $type->getBuiltinType()) {
501+
switch ($builtinType) {
496502
case Type::BUILTIN_TYPE_BOOL:
497503
// according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
498504
if ('false' === $data || '0' === $data) {
@@ -593,19 +599,19 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
593599
return $data;
594600
}
595601
} catch (NotNormalizableValueException $e) {
596-
if (!$isUnionType) {
602+
if (!$isUnionType && !$isNullable) {
597603
throw $e;
598604
}
599605
} catch (ExtraAttributesException $e) {
600-
if (!$isUnionType) {
606+
if (!$isUnionType && !$isNullable) {
601607
throw $e;
602608
}
603609

604610
if (!$extraAttributesException) {
605611
$extraAttributesException = $e;
606612
}
607613
} catch (MissingConstructorArgumentsException $e) {
608-
if (!$isUnionType) {
614+
if (!$isUnionType && !$isNullable) {
609615
throw $e;
610616
}
611617

@@ -615,6 +621,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
615621
}
616622
}
617623

624+
if ($isNullable) {
625+
return null;
626+
}
627+
618628
if ($extraAttributesException) {
619629
throw $extraAttributesException;
620630
}
@@ -623,6 +633,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
623633
throw $missingConstructorArgumentException;
624634
}
625635

636+
if (!$isUnionType && $e) {
637+
throw $e;
638+
}
639+
626640
if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
627641
return $data;
628642
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
15+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
16+
17+
/**
18+
* @author Jeroen <github.com/Jeroeny>
19+
*/
20+
class DummyString implements DenormalizableInterface
21+
{
22+
/** @var string $value */
23+
public $value;
24+
25+
public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = [])
26+
{
27+
$this->value = $data;
28+
}
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
/**
15+
* @author Jeroen <github.com/Jeroeny>
16+
*/
17+
class DummyWithNotNormalizable
18+
{
19+
public function __construct(public NotNormalizableDummy|null $value)
20+
{
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
/**
15+
* @author Jeroen <github.com/Jeroeny>
16+
*/
17+
class DummyWithObjectOrBool
18+
{
19+
public function __construct(public Php80WithPromotedTypedConstructor|bool $value)
20+
{
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
/**
15+
* @author Jeroen <github.com/Jeroeny>
16+
*/
17+
class DummyWithObjectOrNull
18+
{
19+
public function __construct(public Php80WithPromotedTypedConstructor|null $value)
20+
{
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
/**
15+
* @author Jeroen <github.com/Jeroeny>
16+
*/
17+
class DummyWithStringObject
18+
{
19+
public function __construct(public DummyString|null $value)
20+
{
21+
}
22+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
15+
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
16+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
17+
18+
/**
19+
* @author Jeroen <github.com/Jeroeny>
20+
*/
21+
class NotNormalizableDummy implements DenormalizableInterface
22+
{
23+
public function __construct()
24+
{
25+
}
26+
27+
public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = [])
28+
{
29+
throw new NotNormalizableValueException();
30+
}
31+
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
use Doctrine\Common\Annotations\AnnotationReader;
1515
use PHPUnit\Framework\TestCase;
1616
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
17+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
18+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
1719
use Symfony\Component\PropertyInfo\Type;
1820
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
1921
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
2022
use Symfony\Component\Serializer\Exception\LogicException;
23+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
2124
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
2225
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
2326
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
@@ -29,6 +32,7 @@
2932
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
3033
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
3134
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
35+
use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
3236
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
3337
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
3438
use Symfony\Component\Serializer\Serializer;
@@ -39,6 +43,11 @@
3943
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummySecondChild;
4044
use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux;
4145
use Symfony\Component\Serializer\Tests\Fixtures\DummySecondChildQuux;
46+
use Symfony\Component\Serializer\Tests\Fixtures\DummyString;
47+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable;
48+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrBool;
49+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull;
50+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithStringObject;
4251

4352
class AbstractObjectNormalizerTest extends TestCase
4453
{
@@ -444,6 +453,60 @@ public function testNormalizeEmptyObject()
444453
$normalizedData = $normalizer->normalize(new EmptyDummy(), 'any', ['preserve_empty_objects' => true]);
445454
$this->assertEquals(new \ArrayObject(), $normalizedData);
446455
}
456+
457+
/**
458+
* @requires PHP 8
459+
*/
460+
public function testDenormalizeUntypedFormat()
461+
{
462+
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
463+
$actual = $serializer->denormalize(['value' => ''], DummyWithObjectOrNull::class, 'xml');
464+
465+
$this->assertEquals(new DummyWithObjectOrNull(null), $actual);
466+
}
467+
468+
/**
469+
* @requires PHP 8
470+
*/
471+
public function testDenormalizeUntypedFormatNotNormalizable()
472+
{
473+
$this->expectException(NotNormalizableValueException::class);
474+
$serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
475+
$serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml');
476+
}
477+
478+
/**
479+
* @requires PHP 8
480+
*/
481+
public function testDenormalizeUntypedFormatMissingArg()
482+
{
483+
$this->expectException(MissingConstructorArgumentsException::class);
484+
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
485+
$serializer->denormalize(['value' => 'invalid'], DummyWithObjectOrNull::class, 'xml');
486+
}
487+
488+
/**
489+
* @requires PHP 8
490+
*/
491+
public function testDenormalizeUntypedFormatScalar()
492+
{
493+
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
494+
$actual = $serializer->denormalize(['value' => 'false'], DummyWithObjectOrBool::class, 'xml');
495+
496+
$this->assertEquals(new DummyWithObjectOrBool(false), $actual);
497+
}
498+
499+
/**
500+
* @requires PHP 8
501+
*/
502+
public function testDenormalizeUntypedStringObject()
503+
{
504+
$serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
505+
$actual = $serializer->denormalize(['value' => ''], DummyWithStringObject::class, 'xml');
506+
507+
$this->assertEquals(new DummyWithStringObject(new DummyString()), $actual);
508+
$this->assertEquals('', $actual->value->value);
509+
}
447510
}
448511

449512
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
1818
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
1919
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
20+
use Symfony\Component\Serializer\Encoder\CsvEncoder;
2021
use Symfony\Component\Serializer\Encoder\DecoderInterface;
2122
use Symfony\Component\Serializer\Encoder\EncoderInterface;
2223
use Symfony\Component\Serializer\Encoder\JsonEncoder;
@@ -62,6 +63,7 @@
6263
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
6364
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
6465
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty;
66+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull;
6567
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
6668
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
6769
use Symfony\Component\Serializer\Tests\Fixtures\Php74Full;
@@ -818,6 +820,17 @@ public function testFalseBuiltInTypes()
818820
$this->assertEquals(new FalseBuiltInDummy(), $actual);
819821
}
820822

823+
/**
824+
* @requires PHP 8
825+
*/
826+
public function testDeserializeUntypedFormat()
827+
{
828+
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))], ['csv' => new CsvEncoder()]);
829+
$actual = $serializer->deserialize('value'.\PHP_EOL.',', DummyWithObjectOrNull::class, 'csv', [CsvEncoder::AS_COLLECTION_KEY => false]);
830+
831+
$this->assertEquals(new DummyWithObjectOrNull(null), $actual);
832+
}
833+
821834
private function serializerWithClassDiscriminator()
822835
{
823836
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));

0 commit comments

Comments
 (0)