Skip to content

Commit 2f1a7ea

Browse files
Merge branch '5.4' into 6.3
* 5.4: [Serializer] Fix test Fix denormalizing empty string into object|null parameter [Serializer] Move discrimination to abstract [Serializer] Fix access to private when Ignore
2 parents bf01d35 + 1bc8d26 commit 2f1a7ea

14 files changed

+379
-33
lines changed

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

+37-20
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,12 @@ public function normalize(mixed $object, string $format = null, array $context =
185185
}
186186

187187
$attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context);
188+
$discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);
188189

189190
try {
190-
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $attributeContext);
191+
$attributeValue = $attribute === $discriminatorMapping?->getTypeProperty()
192+
? $discriminatorMapping
193+
: $this->getAttributeValue($object, $attribute, $format, $attributeContext);
191194
} catch (UninitializedPropertyException $e) {
192195
if ($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) {
193196
continue;
@@ -258,22 +261,18 @@ protected function getAttributes(object $object, ?string $format, array $context
258261
return $this->attributesCache[$key];
259262
}
260263

261-
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
262-
263-
if (false !== $allowedAttributes) {
264-
if ($context['cache_key']) {
265-
$this->attributesCache[$key] = $allowedAttributes;
266-
}
267-
268-
return $allowedAttributes;
269-
}
270-
271264
$attributes = $this->extractAttributes($object, $format, $context);
272265

273266
if ($mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object)) {
274267
array_unshift($attributes, $mapping->getTypeProperty());
275268
}
276269

270+
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
271+
272+
if (false !== $allowedAttributes) {
273+
$attributes = array_intersect($attributes, $allowedAttributes);
274+
}
275+
277276
if ($context['cache_key'] && \stdClass::class !== $class) {
278277
$this->attributesCache[$key] = $attributes;
279278
}
@@ -364,8 +363,12 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
364363
}
365364

366365
if ($attributeContext[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
366+
$discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);
367+
367368
try {
368-
$attributeContext[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $attributeContext);
369+
$attributeContext[self::OBJECT_TO_POPULATE] = $attribute === $discriminatorMapping?->getTypeProperty()
370+
? $discriminatorMapping
371+
: $this->getAttributeValue($object, $attribute, $format, $attributeContext);
369372
} catch (NoSuchPropertyException) {
370373
}
371374
}
@@ -432,8 +435,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
432435
{
433436
$expectedTypes = [];
434437
$isUnionType = \count($types) > 1;
438+
$e = null;
435439
$extraAttributesException = null;
436440
$missingConstructorArgumentsException = null;
441+
$isNullable = false;
437442
foreach ($types as $type) {
438443
if (null === $data && $type->isNullable()) {
439444
return null;
@@ -456,18 +461,22 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
456461
// In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
457462
// if a value is meant to be a string, float, int or a boolean value from the serialized representation.
458463
// That's why we have to transform the values, if one of these non-string basic datatypes is expected.
464+
$builtinType = $type->getBuiltinType();
459465
if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
460466
if ('' === $data) {
461-
if (Type::BUILTIN_TYPE_ARRAY === $builtinType = $type->getBuiltinType()) {
467+
if (Type::BUILTIN_TYPE_ARRAY === $builtinType) {
462468
return [];
463469
}
464470

465-
if ($type->isNullable() && \in_array($builtinType, [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) {
466-
return null;
471+
if (Type::BUILTIN_TYPE_STRING === $builtinType) {
472+
return '';
467473
}
474+
475+
// Don't return null yet because Object-types that come first may accept empty-string too
476+
$isNullable = $isNullable ?: $type->isNullable();
468477
}
469478

470-
switch ($builtinType ?? $type->getBuiltinType()) {
479+
switch ($builtinType) {
471480
case Type::BUILTIN_TYPE_BOOL:
472481
// according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
473482
if ('false' === $data || '0' === $data) {
@@ -564,24 +573,28 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
564573
return $data;
565574
}
566575
} catch (NotNormalizableValueException|InvalidArgumentException $e) {
567-
if (!$isUnionType) {
576+
if (!$isUnionType && !$isNullable) {
568577
throw $e;
569578
}
570579
} catch (ExtraAttributesException $e) {
571-
if (!$isUnionType) {
580+
if (!$isUnionType && !$isNullable) {
572581
throw $e;
573582
}
574583

575584
$extraAttributesException ??= $e;
576585
} catch (MissingConstructorArgumentsException $e) {
577-
if (!$isUnionType) {
586+
if (!$isUnionType && !$isNullable) {
578587
throw $e;
579588
}
580589

581590
$missingConstructorArgumentsException ??= $e;
582591
}
583592
}
584593

594+
if ($isNullable) {
595+
return null;
596+
}
597+
585598
if ($extraAttributesException) {
586599
throw $extraAttributesException;
587600
}
@@ -590,6 +603,10 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
590603
throw $missingConstructorArgumentsException;
591604
}
592605

606+
if (!$isUnionType && $e) {
607+
throw $e;
608+
}
609+
593610
if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
594611
return $data;
595612
}
@@ -629,7 +646,7 @@ private function getTypes(string $currentClass, string $attribute): ?array
629646
return $this->typesCache[$key] = $types;
630647
}
631648

632-
if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
649+
if ($discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForClass($currentClass)) {
633650
if ($discriminatorMapping->getTypeProperty() === $attribute) {
634651
return $this->typesCache[$key] = [
635652
new Type(Type::BUILTIN_TYPE_STRING),

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

+4
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public function hasCacheableSupportsMethod(): bool
7676
*/
7777
private function supports(string $class): bool
7878
{
79+
if ($this->classDiscriminatorResolver?->getMappingForClass($class)) {
80+
return true;
81+
}
82+
7983
$class = new \ReflectionClass($class);
8084
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
8185
foreach ($methods as $method) {

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

+4-12
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ class ObjectNormalizer extends AbstractObjectNormalizer
3232
{
3333
protected $propertyAccessor;
3434

35-
/** @var array<string, string|null> */
36-
private array $discriminatorCache = [];
37-
3835
private readonly \Closure $objectClassResolver;
3936

4037
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
@@ -130,16 +127,11 @@ protected function extractAttributes(object $object, string $format = null, arra
130127

131128
protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed
132129
{
133-
$cacheKey = $object::class;
134-
if (!\array_key_exists($cacheKey, $this->discriminatorCache)) {
135-
$this->discriminatorCache[$cacheKey] = null;
136-
if (null !== $this->classDiscriminatorResolver) {
137-
$mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object);
138-
$this->discriminatorCache[$cacheKey] = $mapping?->getTypeProperty();
139-
}
140-
}
130+
$mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);
141131

142-
return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute);
132+
return $attribute === $mapping?->getTypeProperty()
133+
? $mapping
134+
: $this->propertyAccessor->getValue($object, $attribute);
143135
}
144136

145137
/**

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

+4
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ public function hasCacheableSupportsMethod(): bool
9292
*/
9393
private function supports(string $class): bool
9494
{
95+
if ($this->classDiscriminatorResolver?->getMappingForClass($class)) {
96+
return true;
97+
}
98+
9599
$class = new \ReflectionClass($class);
96100

97101
// We look for at least one non-static property
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+
}
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+
}
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+
}
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+
}
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+
}
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, $data, string $format = null, array $context = [])
28+
{
29+
throw new NotNormalizableValueException();
30+
}
31+
}

0 commit comments

Comments
 (0)