Skip to content

Commit c5a661a

Browse files
bug #58473 [Serializer] Fix AbstractObjectNormalizer to allow scalar values to be normalized (Hanmac, xabbuh)
This PR was merged into the 6.4 branch. Discussion ---------- [Serializer] Fix `AbstractObjectNormalizer` to allow scalar values to be normalized | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix #58056 | License | MIT It still does work when the Normalizer is used with Serializer (like it normally would?), Commits ------- e195833 ObjectNormalizer: allow null and scalar
2 parents 88b0d7f + e195833 commit c5a661a

File tree

6 files changed

+167
-37
lines changed

6 files changed

+167
-37
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,11 @@ public function normalize(mixed $object, ?string $format = null, array $context
210210
foreach ($stack as $attribute => $attributeValue) {
211211
$attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context);
212212

213-
if (null === $attributeValue || \is_scalar($attributeValue)) {
214-
$data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata);
215-
continue;
216-
}
217-
218213
if (!$this->serializer instanceof NormalizerInterface) {
214+
if (null === $attributeValue || \is_scalar($attributeValue)) {
215+
$data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata);
216+
continue;
217+
}
219218
throw new LogicException(\sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute));
220219
}
221220

@@ -465,7 +464,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
465464

466465
// This try-catch should cover all NotNormalizableValueException (and all return branches after the first
467466
// exception) so we could try denormalizing all types of an union type. If the target type is not an union
468-
// type, we will just re-throw the catched exception.
467+
// type, we will just re-throw the caught exception.
469468
// In the case of no denormalization succeeds with an union type, it will fall back to the default exception
470469
// with the acceptable types list.
471470
try {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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\NormalizerInterface;
15+
16+
class ScalarNormalizer implements NormalizerInterface
17+
{
18+
public function normalize(mixed $object, ?string $format = null, array $context = []): string
19+
{
20+
$data = $object;
21+
22+
if (!\is_string($data)) {
23+
$data = (string) $object;
24+
}
25+
26+
return strtoupper($data);
27+
}
28+
29+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
30+
{
31+
return \is_scalar($data);
32+
}
33+
34+
public function getSupportedTypes(?string $format): array
35+
{
36+
return [
37+
'native-boolean' => true,
38+
'native-integer' => true,
39+
'native-string' => true,
40+
];
41+
}
42+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\NormalizerInterface;
15+
16+
class StdClassNormalizer implements NormalizerInterface
17+
{
18+
public function normalize(mixed $object, ?string $format = null, array $context = []): string
19+
{
20+
return 'string_object';
21+
}
22+
23+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
24+
{
25+
return $data instanceof \stdClass;
26+
}
27+
28+
public function getSupportedTypes(?string $format): array
29+
{
30+
return [
31+
\stdClass::class => true,
32+
];
33+
}
34+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -960,15 +960,15 @@ protected function createChildContext(array $parentContext, string $attribute, ?
960960
$this->assertSame($firstChildContextCacheKey, $secondChildContextCacheKey);
961961
}
962962

963-
public function testChildContextKeepsOriginalContextCacheKey()
963+
public function testChildContextChangesContextCacheKey()
964964
{
965965
$foobar = new Dummy();
966966
$foobar->foo = new EmptyDummy();
967967
$foobar->bar = 'bar';
968968
$foobar->baz = 'baz';
969969

970970
$normalizer = new class extends AbstractObjectNormalizerDummy {
971-
public $childContextCacheKey;
971+
public array $childContextCacheKeys = [];
972972

973973
protected function extractAttributes(object $object, ?string $format = null, array $context = []): array
974974
{
@@ -983,7 +983,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string
983983
protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
984984
{
985985
$childContext = parent::createChildContext($parentContext, $attribute, $format);
986-
$this->childContextCacheKey = $childContext['cache_key'];
986+
$this->childContextCacheKeys[$attribute] = $childContext['cache_key'];
987987

988988
return $childContext;
989989
}
@@ -992,7 +992,7 @@ protected function createChildContext(array $parentContext, string $attribute, ?
992992
$serializer = new Serializer([$normalizer]);
993993
$serializer->normalize($foobar, null, ['cache_key' => 'hardcoded', 'iri' => '/dummy/1']);
994994

995-
$this->assertSame('hardcoded-foo', $normalizer->childContextCacheKey);
995+
$this->assertSame(['foo' => 'hardcoded-foo', 'bar' => 'hardcoded-bar', 'baz' => 'hardcoded-baz'], $normalizer->childContextCacheKeys);
996996
}
997997

998998
public function testChildContextCacheKeyStaysFalseWhenOriginalCacheKeyIsFalse()

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

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Component\Serializer\Tests\Normalizer;
1313

14-
use PHPUnit\Framework\MockObject\MockObject;
1514
use PHPUnit\Framework\TestCase;
1615
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
1716
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
@@ -34,7 +33,9 @@
3433
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\ClassWithIgnoreAttribute;
3534
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummy;
3635
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
36+
use Symfony\Component\Serializer\Tests\Fixtures\ScalarNormalizer;
3737
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
38+
use Symfony\Component\Serializer\Tests\Fixtures\StdClassNormalizer;
3839
use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait;
3940
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
4041
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
@@ -61,7 +62,7 @@ class GetSetMethodNormalizerTest extends TestCase
6162
use TypeEnforcementTestTrait;
6263

6364
private GetSetMethodNormalizer $normalizer;
64-
private SerializerInterface&NormalizerInterface&MockObject $serializer;
65+
private SerializerInterface&NormalizerInterface $serializer;
6566

6667
protected function setUp(): void
6768
{
@@ -70,8 +71,8 @@ protected function setUp(): void
7071

7172
private function createNormalizer(array $defaultContext = []): void
7273
{
73-
$this->serializer = $this->createMock(SerializerNormalizer::class);
7474
$this->normalizer = new GetSetMethodNormalizer(null, null, null, null, null, $defaultContext);
75+
$this->serializer = new Serializer([$this->normalizer, new StdClassNormalizer()]);
7576
$this->normalizer->setSerializer($this->serializer);
7677
}
7778

@@ -91,13 +92,6 @@ public function testNormalize()
9192
$obj->setCamelCase('camelcase');
9293
$obj->setObject($object);
9394

94-
$this->serializer
95-
->expects($this->once())
96-
->method('normalize')
97-
->with($object, 'any')
98-
->willReturn('string_object')
99-
;
100-
10195
$this->assertEquals(
10296
[
10397
'foo' => 'foo',
@@ -111,6 +105,29 @@ public function testNormalize()
111105
);
112106
}
113107

108+
public function testNormalizeWithoutSerializer()
109+
{
110+
$obj = new GetSetDummy();
111+
$obj->setFoo('foo');
112+
$obj->setBar('bar');
113+
$obj->setBaz(true);
114+
$obj->setCamelCase('camelcase');
115+
116+
$this->normalizer = new GetSetMethodNormalizer();
117+
118+
$this->assertEquals(
119+
[
120+
'foo' => 'foo',
121+
'bar' => 'bar',
122+
'baz' => true,
123+
'fooBar' => 'foobar',
124+
'camelCase' => 'camelcase',
125+
'object' => null,
126+
],
127+
$this->normalizer->normalize($obj, 'any')
128+
);
129+
}
130+
114131
public function testDenormalize()
115132
{
116133
$obj = $this->normalizer->denormalize(
@@ -377,8 +394,7 @@ public function testUnableToNormalizeObjectAttribute()
377394
{
378395
$this->expectException(LogicException::class);
379396
$this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer');
380-
$serializer = $this->createMock(SerializerInterface::class);
381-
$this->normalizer->setSerializer($serializer);
397+
$this->normalizer->setSerializer($this->createMock(SerializerInterface::class));
382398

383399
$obj = new GetSetDummy();
384400
$object = new \stdClass();
@@ -523,6 +539,30 @@ public function testNormalizeWithMethodNamesSimilarToAccessors()
523539
$this->assertSame(['class' => 'class', 123 => 123], $normalizer->normalize(new GetSetWithAccessorishMethod()));
524540
}
525541

542+
public function testNormalizeWithScalarValueNormalizer()
543+
{
544+
$normalizer = new GetSetMethodNormalizer();
545+
$normalizer->setSerializer(new Serializer([$normalizer, new ScalarNormalizer()]));
546+
547+
$obj = new GetSetDummy();
548+
$obj->setFoo('foo');
549+
$obj->setBar(10);
550+
$obj->setBaz(true);
551+
$obj->setCamelCase('camelcase');
552+
553+
$this->assertSame(
554+
[
555+
'foo' => 'FOO',
556+
'bar' => '10',
557+
'baz' => '1',
558+
'fooBar' => 'FOO10',
559+
'camelCase' => 'CAMELCASE',
560+
'object' => null,
561+
],
562+
$normalizer->normalize($obj, 'any')
563+
);
564+
}
565+
526566
public function testDenormalizeWithDiscriminator()
527567
{
528568
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
@@ -701,10 +741,6 @@ public function otherMethod()
701741
}
702742
}
703743

704-
abstract class SerializerNormalizer implements SerializerInterface, NormalizerInterface
705-
{
706-
}
707-
708744
class GetConstructorOptionalArgsDummy
709745
{
710746
protected $foo;

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

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\Serializer\Tests\Normalizer;
1313

1414
use PHPStan\PhpDocParser\Parser\PhpDocParser;
15-
use PHPUnit\Framework\MockObject\MockObject;
1615
use PHPUnit\Framework\TestCase;
1716
use Symfony\Component\PropertyAccess\PropertyAccessorBuilder;
1817
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
@@ -49,6 +48,7 @@
4948
use Symfony\Component\Serializer\Tests\Fixtures\Php74DummyPrivate;
5049
use Symfony\Component\Serializer\Tests\Fixtures\Php80Dummy;
5150
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
51+
use Symfony\Component\Serializer\Tests\Fixtures\StdClassNormalizer;
5252
use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait;
5353
use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait;
5454
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
@@ -86,7 +86,7 @@ class ObjectNormalizerTest extends TestCase
8686
use TypeEnforcementTestTrait;
8787

8888
private ObjectNormalizer $normalizer;
89-
private SerializerInterface&NormalizerInterface&MockObject $serializer;
89+
private SerializerInterface&NormalizerInterface $serializer;
9090

9191
protected function setUp(): void
9292
{
@@ -95,8 +95,8 @@ protected function setUp(): void
9595

9696
private function createNormalizer(array $defaultContext = [], ?ClassMetadataFactoryInterface $classMetadataFactory = null): void
9797
{
98-
$this->serializer = $this->createMock(ObjectSerializerNormalizer::class);
9998
$this->normalizer = new ObjectNormalizer($classMetadataFactory, null, null, null, null, null, $defaultContext);
99+
$this->serializer = new Serializer([new StdClassNormalizer(), $this->normalizer]);
100100
$this->normalizer->setSerializer($this->serializer);
101101
}
102102

@@ -111,13 +111,6 @@ public function testNormalize()
111111
$obj->setObject($object);
112112
$obj->setGo(true);
113113

114-
$this->serializer
115-
->expects($this->once())
116-
->method('normalize')
117-
->with($object, 'any')
118-
->willReturn('string_object')
119-
;
120-
121114
$this->assertEquals(
122115
[
123116
'foo' => 'foo',
@@ -132,6 +125,32 @@ public function testNormalize()
132125
);
133126
}
134127

128+
public function testNormalizeWithoutSerializer()
129+
{
130+
$obj = new ObjectDummy();
131+
$obj->setFoo('foo');
132+
$obj->bar = 'bar';
133+
$obj->setBaz(true);
134+
$obj->setCamelCase('camelcase');
135+
$obj->setObject(null);
136+
$obj->setGo(true);
137+
138+
$this->normalizer = new ObjectNormalizer();
139+
140+
$this->assertEquals(
141+
[
142+
'foo' => 'foo',
143+
'bar' => 'bar',
144+
'baz' => true,
145+
'fooBar' => 'foobar',
146+
'camelCase' => 'camelcase',
147+
'object' => null,
148+
'go' => true,
149+
],
150+
$this->normalizer->normalize($obj, 'any')
151+
);
152+
}
153+
135154
public function testNormalizeObjectWithUninitializedProperties()
136155
{
137156
$obj = new Php74Dummy();
@@ -962,7 +981,7 @@ public function testObjectNormalizerWithAttributeLoaderAndObjectHasStaticPropert
962981

963982
protected function getNormalizerForAccessors($accessorPrefixes = null): ObjectNormalizer
964983
{
965-
$accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
984+
$accessorPrefixes ??= ReflectionExtractor::$defaultAccessorPrefixes;
966985
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
967986
$propertyAccessorBuilder = (new PropertyAccessorBuilder())
968987
->setReadInfoExtractor(

0 commit comments

Comments
 (0)