Skip to content

Commit 4176d71

Browse files
committed
Merge branch '7.3' into 7.4
* 7.3: 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 7.3.8 Update VERSION for 7.3.7 Update CHANGELOG for 7.3.7 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 [HttpFoundation] Fix parsing pathinfo with no leading slash
2 parents 31cf2f6 + e9f668b commit 4176d71

10 files changed

+326
-49
lines changed

Normalizer/AbstractObjectNormalizer.php

Lines changed: 4 additions & 5 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

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
@@ -1120,15 +1120,15 @@ protected function createChildContext(array $parentContext, string $attribute, ?
11201120
$this->assertSame($firstChildContextCacheKey, $secondChildContextCacheKey);
11211121
}
11221122

1123-
public function testChildContextKeepsOriginalContextCacheKey()
1123+
public function testChildContextChangesContextCacheKey()
11241124
{
11251125
$foobar = new Dummy();
11261126
$foobar->foo = new EmptyDummy();
11271127
$foobar->bar = 'bar';
11281128
$foobar->baz = 'baz';
11291129

11301130
$normalizer = new class extends AbstractObjectNormalizerDummy {
1131-
public $childContextCacheKey;
1131+
public array $childContextCacheKeys = [];
11321132

11331133
protected function extractAttributes(object $object, ?string $format = null, array $context = []): array
11341134
{
@@ -1143,7 +1143,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string
11431143
protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
11441144
{
11451145
$childContext = parent::createChildContext($parentContext, $attribute, $format);
1146-
$this->childContextCacheKey = $childContext['cache_key'];
1146+
$this->childContextCacheKeys[$attribute] = $childContext['cache_key'];
11471147

11481148
return $childContext;
11491149
}
@@ -1152,7 +1152,7 @@ protected function createChildContext(array $parentContext, string $attribute, ?
11521152
$serializer = new Serializer([$normalizer]);
11531153
$serializer->normalize($foobar, null, ['cache_key' => 'hardcoded', 'iri' => '/dummy/1']);
11541154

1155-
$this->assertSame('hardcoded-foo', $normalizer->childContextCacheKey);
1155+
$this->assertSame(['foo' => 'hardcoded-foo', 'bar' => 'hardcoded-bar', 'baz' => 'hardcoded-baz'], $normalizer->childContextCacheKeys);
11561156
}
11571157

11581158
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
@@ -13,7 +13,6 @@
1313

1414
use PHPUnit\Framework\Attributes\DataProvider;
1515
use PHPUnit\Framework\Attributes\TestWith;
16-
use PHPUnit\Framework\MockObject\MockObject;
1716
use PHPUnit\Framework\TestCase;
1817
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
1918
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
@@ -36,7 +35,9 @@
3635
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\ClassWithIgnoreAttribute;
3736
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummy;
3837
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
38+
use Symfony\Component\Serializer\Tests\Fixtures\ScalarNormalizer;
3939
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
40+
use Symfony\Component\Serializer\Tests\Fixtures\StdClassNormalizer;
4041
use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait;
4142
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
4243
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
@@ -65,7 +66,7 @@ class GetSetMethodNormalizerTest extends TestCase
6566
use TypeEnforcementTestTrait;
6667

6768
private GetSetMethodNormalizer $normalizer;
68-
private SerializerInterface&NormalizerInterface&MockObject $serializer;
69+
private SerializerInterface&NormalizerInterface $serializer;
6970

7071
protected function setUp(): void
7172
{
@@ -74,8 +75,8 @@ protected function setUp(): void
7475

7576
private function createNormalizer(array $defaultContext = []): void
7677
{
77-
$this->serializer = $this->createMock(SerializerNormalizer::class);
7878
$this->normalizer = new GetSetMethodNormalizer(null, null, null, null, null, $defaultContext);
79+
$this->serializer = new Serializer([$this->normalizer, new StdClassNormalizer()]);
7980
$this->normalizer->setSerializer($this->serializer);
8081
}
8182

@@ -95,13 +96,6 @@ public function testNormalize()
9596
$obj->setCamelCase('camelcase');
9697
$obj->setObject($object);
9798

98-
$this->serializer
99-
->expects($this->once())
100-
->method('normalize')
101-
->with($object, 'any')
102-
->willReturn('string_object')
103-
;
104-
10599
$this->assertEquals(
106100
[
107101
'foo' => 'foo',
@@ -115,6 +109,29 @@ public function testNormalize()
115109
);
116110
}
117111

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

385402
public function testUnableToNormalizeObjectAttribute()
386403
{
387-
$serializer = $this->createMock(SerializerInterface::class);
388-
$this->normalizer->setSerializer($serializer);
404+
$this->normalizer->setSerializer($this->createMock(SerializerInterface::class));
389405

390406
$obj = new GetSetDummy();
391407
$object = new \stdClass();
@@ -530,6 +546,30 @@ public function testNormalizeWithMethodNamesSimilarToAccessors()
530546
$this->assertSame(['class' => 'class', 123 => 123], $normalizer->normalize(new GetSetWithAccessorishMethod()));
531547
}
532548

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

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

0 commit comments

Comments
 (0)