Skip to content

Commit e9f668b

Browse files
committed
Merge branch '6.4' into 7.3
* 6.4: Fix Warning: curl_multi_select(): timeout must be positive [PropertyInfo] Fix ReflectionExtractor handling of underscore-only property names ObjectNormalizer: allow null and scalar [Security] Fix `HttpUtils::createRequest()` when the context’s base URL isn’t empty [Serializer] fix Inherited properties normalization [OptionsResolver] Fix missing prototype key in nested error paths Bump Symfony version to 6.4.30 Update VERSION for 6.4.29 Update CHANGELOG for 6.4.29 [Yaml] Fix parsing of unquoted multiline scalars with comments or blank lines [Clock] Align MockClock::sleep() behavior with NativeClock for negative values [OptionsResolver] Ensure remove() also unsets deprecation status Remove review state for Belarusian translations (entries 141 and 142) [ExpressionLanguage] Compile numbers with var_export in Compiler::repr for thread-safety compatibility with ext-redis 6.3 [Serializer] Fix BackedEnumNormalizer behavior with partial denormalization
2 parents ba2e50a + d7976be commit e9f668b

10 files changed

+327
-50
lines changed

Normalizer/AbstractObjectNormalizer.php

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

216-
if (null === $attributeValue || \is_scalar($attributeValue)) {
217-
$normalizedData = $this->updateData($normalizedData, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata);
218-
continue;
219-
}
220-
221216
if (!$this->serializer instanceof NormalizerInterface) {
217+
if (null === $attributeValue || \is_scalar($attributeValue)) {
218+
$normalizedData = $this->updateData($normalizedData, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata);
219+
continue;
220+
}
222221
throw new LogicException(\sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute));
223222
}
224223

@@ -459,7 +458,7 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass
459458

460459
// This try-catch should cover all NotNormalizableValueException (and all return branches after the first
461460
// exception) so we could try denormalizing all types of an union type. If the target type is not an union
462-
// type, we will just re-throw the catched exception.
461+
// type, we will just re-throw the caught exception.
463462
// In the case of no denormalization succeeds with an union type, it will fall back to the default exception
464463
// with the acceptable types list.
465464
try {

Normalizer/BackedEnumNormalizer.php

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,30 +56,28 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
5656
throw new InvalidArgumentException('The data must belong to a backed enumeration.');
5757
}
5858

59-
if ($context[self::ALLOW_INVALID_VALUES] ?? false) {
60-
if (null === $data || (!\is_int($data) && !\is_string($data))) {
61-
return null;
62-
}
59+
$allowInvalidValues = $context[self::ALLOW_INVALID_VALUES] ?? false;
6360

64-
try {
65-
return $type::tryFrom($data);
66-
} catch (\TypeError) {
61+
if (null === $data || (!\is_int($data) && !\is_string($data))) {
62+
if ($allowInvalidValues && !isset($context['not_normalizable_value_exceptions'])) {
6763
return null;
6864
}
69-
}
7065

71-
if (!\is_int($data) && !\is_string($data)) {
7266
throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, ['int', 'string'], $context['deserialization_path'] ?? null, true);
7367
}
7468

7569
try {
7670
return $type::from($data);
77-
} catch (\ValueError $e) {
71+
} catch (\ValueError|\TypeError $e) {
7872
if (isset($context['has_constructor'])) {
7973
throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type, 0, $e);
8074
}
8175

82-
throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e);
76+
if ($allowInvalidValues && !isset($context['not_normalizable_value_exceptions'])) {
77+
return null;
78+
}
79+
80+
throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, ['int', 'string'], $context['deserialization_path'] ?? null, true, 0, $e);
8381
}
8482
}
8583

Normalizer/ObjectNormalizer.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ protected function extractAttributes(object $object, ?string $format = null, arr
9595
'i' => str_starts_with($name, 'is') && isset($name[$i = 2]),
9696
default => false,
9797
} && !ctype_lower($name[$i])) {
98-
if ($reflClass->hasProperty($name)) {
98+
if ($this->hasProperty($reflMethod, $name)) {
9999
$attributeName = $name;
100100
} else {
101101
$attributeName = substr($name, $i);
@@ -127,6 +127,19 @@ protected function extractAttributes(object $object, ?string $format = null, arr
127127
return array_keys($attributes);
128128
}
129129

130+
private function hasProperty(\ReflectionMethod $method, string $propName): bool
131+
{
132+
$class = $method->getDeclaringClass();
133+
134+
do {
135+
if ($class->hasProperty($propName)) {
136+
return true;
137+
}
138+
} while ($class = $class->getParentClass());
139+
140+
return false;
141+
}
142+
130143
protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
131144
{
132145
$mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);
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 <[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\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 <[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\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+
}

Tests/Normalizer/AbstractObjectNormalizerTest.php

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

1126-
public function testChildContextKeepsOriginalContextCacheKey()
1126+
public function testChildContextChangesContextCacheKey()
11271127
{
11281128
$foobar = new Dummy();
11291129
$foobar->foo = new EmptyDummy();
11301130
$foobar->bar = 'bar';
11311131
$foobar->baz = 'baz';
11321132

11331133
$normalizer = new class extends AbstractObjectNormalizerDummy {
1134-
public $childContextCacheKey;
1134+
public array $childContextCacheKeys = [];
11351135

11361136
protected function extractAttributes(object $object, ?string $format = null, array $context = []): array
11371137
{
@@ -1146,7 +1146,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string
11461146
protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
11471147
{
11481148
$childContext = parent::createChildContext($parentContext, $attribute, $format);
1149-
$this->childContextCacheKey = $childContext['cache_key'];
1149+
$this->childContextCacheKeys[$attribute] = $childContext['cache_key'];
11501150

11511151
return $childContext;
11521152
}
@@ -1155,7 +1155,7 @@ protected function createChildContext(array $parentContext, string $attribute, ?
11551155
$serializer = new Serializer([$normalizer]);
11561156
$serializer->normalize($foobar, null, ['cache_key' => 'hardcoded', 'iri' => '/dummy/1']);
11571157

1158-
$this->assertSame('hardcoded-foo', $normalizer->childContextCacheKey);
1158+
$this->assertSame(['foo' => 'hardcoded-foo', 'bar' => 'hardcoded-bar', 'baz' => 'hardcoded-baz'], $normalizer->childContextCacheKeys);
11591159
}
11601160

11611161
public function testChildContextCacheKeyStaysFalseWhenOriginalCacheKeyIsFalse()

Tests/Normalizer/BackedEnumNormalizerTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,30 @@ public function testItUsesTryFromIfContextIsPassed()
126126

127127
$this->assertSame(StringBackedEnumDummy::GET, $this->normalizer->denormalize('GET', StringBackedEnumDummy::class, null, [BackedEnumNormalizer::ALLOW_INVALID_VALUES => true]));
128128
}
129+
130+
public function testDenormalizeNullWithAllowInvalidAndCollectErrorsThrows()
131+
{
132+
$this->expectException(NotNormalizableValueException::class);
133+
$this->expectExceptionMessage('The data is neither an integer nor a string');
134+
135+
$context = [
136+
BackedEnumNormalizer::ALLOW_INVALID_VALUES => true,
137+
'not_normalizable_value_exceptions' => [], // Indicate that we want to collect errors
138+
];
139+
140+
$this->normalizer->denormalize(null, StringBackedEnumDummy::class, null, $context);
141+
}
142+
143+
public function testDenormalizeInvalidValueWithAllowInvalidAndCollectErrorsThrows()
144+
{
145+
$this->expectException(NotNormalizableValueException::class);
146+
$this->expectExceptionMessage('The data must belong to a backed enumeration of type');
147+
148+
$context = [
149+
BackedEnumNormalizer::ALLOW_INVALID_VALUES => true,
150+
'not_normalizable_value_exceptions' => [],
151+
];
152+
153+
$this->normalizer->denormalize('invalid-value', StringBackedEnumDummy::class, null, $context);
154+
}
129155
}

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;
@@ -63,7 +64,7 @@ class GetSetMethodNormalizerTest extends TestCase
6364
use TypeEnforcementTestTrait;
6465

6566
private GetSetMethodNormalizer $normalizer;
66-
private SerializerInterface&NormalizerInterface&MockObject $serializer;
67+
private SerializerInterface&NormalizerInterface $serializer;
6768

6869
protected function setUp(): void
6970
{
@@ -72,8 +73,8 @@ protected function setUp(): void
7273

7374
private function createNormalizer(array $defaultContext = []): void
7475
{
75-
$this->serializer = $this->createMock(SerializerNormalizer::class);
7676
$this->normalizer = new GetSetMethodNormalizer(null, null, null, null, null, $defaultContext);
77+
$this->serializer = new Serializer([$this->normalizer, new StdClassNormalizer()]);
7778
$this->normalizer->setSerializer($this->serializer);
7879
}
7980

@@ -93,13 +94,6 @@ public function testNormalize()
9394
$obj->setCamelCase('camelcase');
9495
$obj->setObject($object);
9596

96-
$this->serializer
97-
->expects($this->once())
98-
->method('normalize')
99-
->with($object, 'any')
100-
->willReturn('string_object')
101-
;
102-
10397
$this->assertEquals(
10498
[
10599
'foo' => 'foo',
@@ -113,6 +107,29 @@ public function testNormalize()
113107
);
114108
}
115109

110+
public function testNormalizeWithoutSerializer()
111+
{
112+
$obj = new GetSetDummy();
113+
$obj->setFoo('foo');
114+
$obj->setBar('bar');
115+
$obj->setBaz(true);
116+
$obj->setCamelCase('camelcase');
117+
118+
$this->normalizer = new GetSetMethodNormalizer();
119+
120+
$this->assertEquals(
121+
[
122+
'foo' => 'foo',
123+
'bar' => 'bar',
124+
'baz' => true,
125+
'fooBar' => 'foobar',
126+
'camelCase' => 'camelcase',
127+
'object' => null,
128+
],
129+
$this->normalizer->normalize($obj, 'any')
130+
);
131+
}
132+
116133
public function testDenormalize()
117134
{
118135
$obj = $this->normalizer->denormalize(
@@ -382,8 +399,7 @@ protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer
382399

383400
public function testUnableToNormalizeObjectAttribute()
384401
{
385-
$serializer = $this->createMock(SerializerInterface::class);
386-
$this->normalizer->setSerializer($serializer);
402+
$this->normalizer->setSerializer($this->createMock(SerializerInterface::class));
387403

388404
$obj = new GetSetDummy();
389405
$object = new \stdClass();
@@ -529,6 +545,30 @@ public function testNormalizeWithMethodNamesSimilarToAccessors()
529545
$this->assertSame(['class' => 'class', 123 => 123], $normalizer->normalize(new GetSetWithAccessorishMethod()));
530546
}
531547

548+
public function testNormalizeWithScalarValueNormalizer()
549+
{
550+
$normalizer = new GetSetMethodNormalizer();
551+
$normalizer->setSerializer(new Serializer([$normalizer, new ScalarNormalizer()]));
552+
553+
$obj = new GetSetDummy();
554+
$obj->setFoo('foo');
555+
$obj->setBar(10);
556+
$obj->setBaz(true);
557+
$obj->setCamelCase('camelcase');
558+
559+
$this->assertSame(
560+
[
561+
'foo' => 'FOO',
562+
'bar' => '10',
563+
'baz' => '1',
564+
'fooBar' => 'FOO10',
565+
'camelCase' => 'CAMELCASE',
566+
'object' => null,
567+
],
568+
$normalizer->normalize($obj, 'any')
569+
);
570+
}
571+
532572
public function testDenormalizeWithDiscriminator()
533573
{
534574
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
@@ -707,10 +747,6 @@ public function otherMethod()
707747
}
708748
}
709749

710-
abstract class SerializerNormalizer implements SerializerInterface, NormalizerInterface
711-
{
712-
}
713-
714750
class GetConstructorOptionalArgsDummy
715751
{
716752
protected $foo;

0 commit comments

Comments
 (0)