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

Skip to content

Commit 1a725d3

Browse files
committed
Fix denormalizing empty string into object|null parameter
1 parent dad8af3 commit 1a725d3

File tree

6 files changed

+131
-43
lines changed

6 files changed

+131
-43
lines changed

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ abstract protected function extractAttributes(object $object, string $format = n
334334

335335
/**
336336
* Gets the attribute value.
337+
*
338+
* @return mixed
337339
*/
338340
abstract protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []);
339341

@@ -457,13 +459,12 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
457459
$isUnionType = \count($types) > 1;
458460
$extraAttributesException = null;
459461
$missingConstructorArgumentException = null;
462+
$isNullable = false;
460463
foreach ($types as $type) {
461464
if (null === $data && $type->isNullable()) {
462465
return null;
463466
}
464467

465-
$isUnionTypeOrNullable = $isUnionTypeOrNullable ?: $type->isNullable();
466-
$hasNonObjectType = $hasNonObjectType ?: Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType();
467468
$collectionValueType = $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null : null;
468469

469470
// Fix a collection that contains the only one element
@@ -488,9 +489,12 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
488489
return [];
489490
}
490491

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

496500
switch ($builtinType) {
@@ -594,19 +598,19 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
594598
return $data;
595599
}
596600
} catch (NotNormalizableValueException $e) {
597-
if (!$isUnionType) {
601+
if (!$isUnionType && !$isNullable) {
598602
throw $e;
599603
}
600604
} catch (ExtraAttributesException $e) {
601-
if (!$isUnionTypeOrNullable) {
605+
if (!$isUnionType && !$isNullable) {
602606
throw $e;
603607
}
604608

605609
if (!$extraAttributesException) {
606610
$extraAttributesException = $e;
607611
}
608612
} catch (MissingConstructorArgumentsException $e) {
609-
if (!$isUnionTypeOrNullable) {
613+
if (!$isUnionType && !$isNullable) {
610614
throw $e;
611615
}
612616

@@ -616,7 +620,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
616620
}
617621
}
618622

619-
if ('' === $data && $isUnionTypeOrNullable && !$hasNonObjectType && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
623+
if ($isNullable) {
620624
return null;
621625
}
622626

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 <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\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 <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 DummyWithStringObject
18+
{
19+
public function __construct(public DummyString|null $value)
20+
{
21+
}
22+
}

src/Symfony/Component/Serializer/Tests/Fixtures/NotNormalizableDummy.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function __construct()
2424
{
2525
}
2626

27-
public function denormalize(DenormalizerInterface $denormalizer, float|int|bool|array|string $data, string $format = null, array $context = [])
27+
public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = [])
2828
{
2929
throw new NotNormalizableValueException();
3030
}

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: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
2525
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
2626
use Symfony\Component\Serializer\Exception\LogicException;
27-
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
2827
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
2928
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
3029
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -64,8 +63,6 @@
6463
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
6564
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
6665
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty;
67-
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable;
68-
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrBool;
6966
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull;
7067
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
7168
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
@@ -823,44 +820,17 @@ public function testFalseBuiltInTypes()
823820
$this->assertEquals(new FalseBuiltInDummy(), $actual);
824821
}
825822

823+
/**
824+
* @requires PHP 8
825+
*/
826826
public function testDeserializeUntypedFormat()
827827
{
828-
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))], ['csv' => new CsvEncoder()]);
828+
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))], ['csv' => new CsvEncoder()]);
829829
$actual = $serializer->deserialize('value'.\PHP_EOL.',', DummyWithObjectOrNull::class, 'csv', [CsvEncoder::AS_COLLECTION_KEY => false]);
830830

831831
$this->assertEquals(new DummyWithObjectOrNull(null), $actual);
832832
}
833833

834-
public function testDenormalizeUntypedFormat()
835-
{
836-
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
837-
$actual = $serializer->denormalize(['value' => ''], DummyWithObjectOrNull::class, 'xml');
838-
839-
$this->assertEquals(new DummyWithObjectOrNull(null), $actual);
840-
}
841-
842-
public function testDenormalizeUntypedFormatNotNormalizable()
843-
{
844-
$this->expectException(NotNormalizableValueException::class);
845-
$serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
846-
$serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml');
847-
}
848-
849-
public function testDenormalizeUntypedFormatMissingArg()
850-
{
851-
$this->expectException(MissingConstructorArgumentsException::class);
852-
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
853-
$serializer->denormalize(['value' => 'invalid'], DummyWithObjectOrNull::class, 'xml');
854-
}
855-
856-
public function testDenormalizeUntypedFormatScalar()
857-
{
858-
$serializer = new Serializer([new ObjectNormalizer(propertyTypeExtractor: new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]);
859-
$actual = $serializer->denormalize(['value' => 'false'], DummyWithObjectOrBool::class, 'xml');
860-
861-
$this->assertEquals(new DummyWithObjectOrBool(false), $actual);
862-
}
863-
864834
private function serializerWithClassDiscriminator()
865835
{
866836
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));

0 commit comments

Comments
 (0)
0