8000 Fix denormalizing empty string into object|null parameter · symfony/symfony@940e9ca · GitHub
[go: up one dir, main page]

Skip to content

Commit 940e9ca

Browse files
committed
Fix denormalizing empty string into object|null parameter
1 parent eee7e8f commit 940e9ca

File tree

6 files changed

+159
-10
lines changed

6 files changed

+159
-10
lines changed

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

Lines changed: 19 additions & 10 deletions
67E6
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,6 @@ abstract protected function extractAttributes(object $object, string $format = n
290290

291291
/**
292292
* Gets the attribute value.
293-
*
294-
* @return mixed
295293
*/
296294
abstract protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []);
297295

@@ -305,9 +303,6 @@ public function supportsDenormalization(mixed $data, string $type, string $forma
305303
return class_exists($type) || (interface_exists($type, false) && null !== $this->classDiscriminatorResolver?->getMappingForClass($type));
306304
}
307305

308-
/**
309-
* @return mixed
310-
*/
311306
public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
312307
{
313308
if (!isset($context['cache_key'])) {
@@ -434,11 +429,16 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
434429
$isUnionType = \count($types) > 1;
435430
$extraAttributesException = null;
436431
$missingConstructorArgumentsException = null;
432+
$hasNonObjectType = false;
433+
$isUnionTypeOrNullable = $isUnionType;
434+
437435
foreach ($types as $type) {
438436
if (null === $data && $type->isNullable()) {
439437
return null;
440438
}
441439

440+
$isUnionTypeOrNullable = $isUnionTypeOrNullable ?: $type->isNullable();
441+
$hasNonObjectType = $hasNonObjectType ?: Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType();
442442
$collectionValueType = $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null : null;
443443

444444
// Fix a collection that contains the only one element
@@ -456,9 +456,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
456456
// In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
457457
// if a value is meant to be a string, float, int or a boolean value from the serialized representation.
458458
// That's why we have to transform the values, if one of these non-string basic datatypes is expected.
459+
$builtinType = $type->getBuiltinType();
459460
if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
460461
if ('' === $data) {
461-
if (Type::BUILTIN_TYPE_ARRAY === $builtinType = $type->getBuiltinType()) {
462+
if (Type::BUILTIN_TYPE_ARRAY === $builtinType) {
462463
return [];
463464
}
464465

@@ -467,7 +468,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
467468
}
468469
}
469470

470-
switch ($builtinType ?? $type->getBuiltinType()) {
471+
switch ($builtinType) {
471472
case Type::BUILTIN_TYPE_BOOL:
472473
// according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
473474
if ('false' === $data || '0' === $data) {
@@ -564,24 +565,28 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
564565
return $data;
565566
}
566567
} catch (NotNormalizableValueException|InvalidArgumentException $e) {
567-
if (!$isUnionType) {
568+
if (!$isUnionTypeOrNullable) {
568569
throw $e;
569570
}
570571
} catch (ExtraAttributesException $e) {
571-
if (!$isUnionType) {
572+
if (!$isUnionTypeOrNullable) {
572573
throw $e;
573574
}
574575

575576
$extraAttributesException ??= $e;
576577
} catch (MissingConstructorArgumentsException $e) {
577-
if (!$isUnionType) {
578+
if (!$isUnionTypeOrNullable) {
578579
throw $e;
579580
}
580581

581582
$missingConstructorArgumentsException ??= $e;
582583
}
583584
}
584585

586+
if ('' === $data && $isUnionTypeOrNullable && !$hasNonObjectType && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
587+
return null;
588+
}
589+
585590
if ($extraAttributesException) {
586591
throw $extraAttributesException;
587592
}
@@ -590,6 +595,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
590595
throw $missingConstructorArgumentsException;
591596
}
592597

598+
if (!$isUnionType && isset($e)) {
599+
throw $e;
600+
}
601+
593602
if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
594603
return $data;
595604
}
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 <fabien@symfony.com>
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 <fabien@symfony.com>
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 <fabien@symfony.com>
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: 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 <fabien@symfony.com>
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, float|int|bool|array|string $data, string $format = null, array $context = [])
28+
{
29+
throw new NotNormalizableValueException();
30+
}
31+
}

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
1717
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
1818
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
19+
use Symfony\Component\Serializer\Encoder\CsvEncoder;
1920
use Symfony\Component\Serializer\Encoder\JsonEncoder;
2021
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
2122
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
2223
use Symfony\Component\Serializer\Exception\LogicException;
24+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
2325
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
2426
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
2527
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -57,6 +59,9 @@
5759
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
5860
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
5961
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty;
62+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable;
63+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrBool;
64+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull;
6065
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
6166
use Symfony\Component\Serializer\Tests\Fixtures\FooImplementationDummy;
6267
use Symfony\Component\Serializer\Tests\Fixtures\FooInterfaceDummyDenormalizer;
@@ -842,6 +847,44 @@ public function testTrueBuiltInTypes()
842847
$this->assertEquals(new TrueBuiltInDummy(), $actual);
843848
}
844849

850+
public function testDeserializeUntypedFormat()
851+
{
852+
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))], ['csv' => new CsvEncoder()]);
853+
$actual = $serializer->deserialize('value'.\PHP_EOL.',', DummyWithObjectOrNull::class, 'csv', [CsvEncoder::AS_COLLECTION_KEY => false]);
854+
855+
$this->assertEquals(new DummyWithObjectOrNull(null), $actual);
856+
}
857+
858+
public function testDenormalizeUntypedFormat()
859+
{
860+
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
861+
$actual = $serializer->denormalize(['value' => ''], DummyWithObjectOrNull::class, 'xml');
862+
863+
$this->assertEquals(new DummyWithObjectOrNull(null), $actual);
864+
}
865+
866+
public function testDenormalizeUntypedFormatNotNormalizable()
867+
{
868+
$this->expectException(NotNormalizableValueException::class);
869+
$serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
870+
$serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml');
871+
}
872+
873+
public function testDenormalizeUntypedFormatMissingArg()
874+
{
875+
$this->expectException(MissingConstructorArgumentsException::class);
876+
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
877+
$serializer->denormalize(['value' => 'invalid'], DummyWithObjectOrNull::class, 'xml');
878+
}
879+
880+
public function testDenormalizeUntypedFormatScalar()
881+
{
882+
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
883+
$actual = $serializer->denormalize(['value' => 'false'], DummyWithObjectOrBool::class, 'xml');
884+
885+
$this->assertEquals(new DummyWithObjectOrBool(false), $actual);
886+
}
887+
845888
private function serializerWithClassDiscriminator()
846889
{
847890
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());

0 commit comments

Comments
 (0)
0