Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Serializer][RFC] recursive object normalizer #57547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 7.3
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
abstract class AbstractObjectNormalizer extends AbstractNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;

public function setDenormalizer(DenormalizerInterface $denormalizer): void
{
$this->denormalizer = $denormalizer;
}

/**
* Set to true to respect the max depth metadata on fields.
*/
Expand Down Expand Up @@ -122,13 +129,15 @@
private array $typeCache = [];
private array $attributesCache = [];
private readonly \Closure $objectClassResolver;
private bool $consistentDenormalization;

public function __construct(
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
private ?PropertyTypeExtractorInterface $propertyTypeExtractor = null,
?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null,
?callable $objectClassResolver = null,
bool $consistentDenormalization = false,
array $defaultContext = [],
) {
parent::__construct($classMetadataFactory, $nameConverter, $defaultContext);
Expand All @@ -144,6 +153,7 @@
}
$this->classDiscriminatorResolver = $classDiscriminatorResolver;
$this->objectClassResolver = ($objectClassResolver ?? 'get_class')(...);
$this->consistentDenormalization = $consistentDenormalization;
}

public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
Expand Down Expand Up @@ -372,12 +382,16 @@

if (null !== $type = $this->getType($resolvedClass, $attribute)) {
try {
// BC layer for PropertyTypeExtractorInterface::getTypes().
// Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0).
if (\is_array($type)) {
$value = $this->validateAndDenormalizeLegacy($type, $resolvedClass, $attribute, $value, $format, $attributeContext);
if($this->consistentDenormalization) {
$value = $this->denormalizer->denormalize($value, $type, $format, $context);

Check failure on line 386 in src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidArgument

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:386:75: InvalidArgument: Argument 2 of Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize expects string, but Symfony\Component\TypeInfo\Type|list<Symfony\Component\PropertyInfo\Type> provided (see https://psalm.dev/004)

Check failure on line 386 in src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidArgument

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:386:75: InvalidArgument: Argument 2 of Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize expects string, but Symfony\Component\TypeInfo\Type|list<Symfony\Component\PropertyInfo\Type> provided (see https://psalm.dev/004)
} else {
$value = $this->validateAndDenormalize($type, $resolvedClass, $attribute, $value, $format, $attributeContext);
// BC layer for PropertyTypeExtractorInterface::getTypes().
// Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0).
if (\is_array($type)) {
$value = $this->validateAndDenormalizeLegacy($type, $resolvedClass, $attribute, $value, $format, $attributeContext);
} else {
$value = $this->validateAndDenormalize($type, $resolvedClass, $attribute, $value, $format, $attributeContext);
}
}
} catch (NotNormalizableValueException $exception) {
if (isset($context['not_normalizable_value_exceptions'])) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\UnionType;

use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;

/**
* Denormalizes arrays of objects.
*
Expand All @@ -44,27 +46,38 @@
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): array
{
$typeResolver = new \phpDocumentor\Reflection\TypeResolver();
$result = $typeResolver->resolve($type);

if (null === $this->denormalizer) {
throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!');
}
if (!\is_array($data)) {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, ['array'], $context['deserialization_path'] ?? null);
}
if (!str_ends_with($type, '[]')) {

if (!$result instanceof \phpDocumentor\Reflection\Types\AbstractList) {
throw new InvalidArgumentException('Unsupported class: '.$type);
}

$type = substr($type, 0, -2);

$type = (string) $result->getValueType();

$typeIdentifiers = [];
if (null !== $keyType = ($context['key_type'] ?? null)) {
$keyTYye = $result->getKeyType();
if ($keyType == null) {

Check failure on line 68 in src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedVariable

src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php:68:13: UndefinedVariable: Cannot find referenced variable $keyType (see https://psalm.dev/024)

Check failure on line 68 in src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedVariable

src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php:68:13: UndefinedVariable: Cannot find referenced variable $keyType (see https://psalm.dev/024)
// Overwrite if context provides keyType
$keyType = $context['key_type'] ?? null;
}
if (null !== $keyType) {
if ($keyType instanceof Type) {
$typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]);
} else {
$typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]);
}
}


foreach ($data as $key => $value) {
$subContext = $context;
$subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]";
Expand All @@ -83,8 +96,12 @@
throw new BadMethodCallException(sprintf('The nested denormalizer needs to be set to allow "%s()" to be used.', __METHOD__));
}

return str_ends_with($type, '[]')
&& $this->denormalizer->supportsDenormalization($data, substr($type, 0, -2), $format, $context);
$typeResolver = new \phpDocumentor\Reflection\TypeResolver();
$result = $typeResolver->resolve($type);


return $result instanceof \phpDocumentor\Reflection\Types\AbstractList
&& $this->denormalizer->supportsDenormalization($data, (string) $result->getValueType(), $format, $context);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,29 @@ final class ObjectNormalizer extends AbstractObjectNormalizer

private readonly \Closure $objectClassResolver;

public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyAccessorInterface $propertyAccessor = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, array $defaultContext = [], ?PropertyInfoExtractorInterface $propertyInfoExtractor = null)
public function __construct(
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null,
?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null,
?callable $objectClassResolver = null,
bool $consistentDenormalization = false,
array $defaultContext = [],
?PropertyInfoExtractorInterface $propertyInfoExtractor = null)
{
if (!class_exists(PropertyAccess::class)) {
throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Try running "composer require symfony/property-access".');
}

parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $consistentDenormalization, $defaultContext);

$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();

$this->objectClassResolver = ($objectClassResolver ?? static fn ($class) => \is_object($class) ? $class::class : $class)(...);
$this->propertyInfoExtractor = $propertyInfoExtractor ?: new ReflectionExtractor();
$this->writeInfoExtractor = new ReflectionExtractor();

}

public function getSupportedTypes(?string $format): array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ final class PropertyNormalizer extends AbstractObjectNormalizer
*/
public const NORMALIZE_VISIBILITY = 'normalize_visibility';

public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, array $defaultContext = [])
public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, bool $consistency = false, array $defaultContext = [])
{
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $consistency, $defaultContext);

if (!isset($this->defaultContext[self::NORMALIZE_VISIBILITY])) {
$this->defaultContext[self::NORMALIZE_VISIBILITY] = self::NORMALIZE_PUBLIC | self::NORMALIZE_PROTECTED | self::NORMALIZE_PRIVATE;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Serializer\Normalizer;

use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

/**
* @author Ian Bentley <ian@idbentley.com>
*/
final class UnionDenormalizer implements DenormalizerAwareInterface, DenormalizerInterface
{
use DenormalizerAwareTrait;

public function setDenormalizer(DenormalizerInterface $denormalizer): void
{
$this->denormalizer = $denormalizer;
}

public function getSupportedTypes(?string $format): array
{
return ['*' => true];
}

public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
if ($this->denormalizer === null) {
throw new \BadMethodCallException(sprintf('The nested denormalizer needs to be set to allow "%s()" to be used.', __METHOD__));
}
if (str_contains($type, '|')) {
$possibleTypes = explode('|', $type);
$support = true;

// all possible types must be supported
foreach ($possibleTypes as $possibleType) {
$typeSupport = $this->denormalizer->supportsDenormalization($data, $possibleType, $format, $context);
$support = $support && $typeSupport;
}
return $support;
}

return false;
}

/** @phpstan-ignore-next-line */
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
$typeResolver = new \phpDocumentor\Reflection\TypeResolver();
$result = $typeResolver->resolve($type);
$possibleTypes = explode('|', $type);

$extraAttributesException = null;
$missingConstructorArgumentsException = null;

if (count($possibleTypes) == 1) {
return $this->denormalizer->denormalize($data, $type, $format, $context);
}

foreach ($possibleTypes as $possibleType) {
if (null === $data && $possibleType->isNullable()) {

Check failure on line 72 in src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidMethodCall

src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php:72:50: InvalidMethodCall: Cannot call method on string variable $possibleType (see https://psalm.dev/091)

Check failure on line 72 in src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidMethodCall

src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php:72:50: InvalidMethodCall: Cannot call method on string variable $possibleType (see https://psalm.dev/091)
return null;
}

try {
return $this->denormalizer->denormalize($data, $possibleType, $format, $context);
} catch (MissingConstructorArgumentsException $e) {
echo "Couldn't denormalize $possibleType: $e\n";
}
}
throw new NotNormalizableValueException("Couldn't denormalize any of the possible types");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,32 @@ class DeserializeNestedArrayOfObjectsTest extends TestCase
public static function provider()
{
return [
// from property PhpDoc
[Zoo::class],
// from argument constructor PhpDoc
[ZooImmutable::class],
// from property PhpDoc, consistent
[Zoo::class, true],
// from property PhpDoc, legacy
[Zoo::class, false],
// from argument constructor PhpDoc, consistent
[ZooImmutable::class, true],
// from argument constructor PhpDoc, legacy
[ZooImmutable::class, false],

];
}

public static function consistencyProvider()
{
return [
// consistent
[true],
// legacy
[false],
];
}

/**
* @dataProvider provider
*/
public function testPropertyPhpDoc($class)
public function testPropertyPhpDoc($class, $consistent)
{
$json = <<<EOF
{
Expand All @@ -43,7 +58,7 @@ public function testPropertyPhpDoc($class)
}
EOF;
$serializer = new Serializer([
new ObjectNormalizer(null, null, null, new PhpDocExtractor()),
new ObjectNormalizer(null, null, null, new PhpDocExtractor(), null, null, $consistent),
new ArrayDenormalizer(),
], ['json' => new JsonEncoder()]);

Expand All @@ -54,7 +69,10 @@ public function testPropertyPhpDoc($class)
self::assertInstanceOf(Animal::class, $zoo->getAnimals()[0]);
}

public function testPropertyPhpDocWithKeyTypes()
/**
* @dataProvider consistencyProvider
*/
public function testPropertyPhpDocWithKeyTypes($consistent)
{
$json = <<<EOF
{
Expand All @@ -75,7 +93,7 @@ public function testPropertyPhpDocWithKeyTypes()
}
EOF;
$serializer = new Serializer([
new ObjectNormalizer(null, null, null, new PhpDocExtractor()),
new ObjectNormalizer(null, null, null, new PhpDocExtractor(), null, null, $consistent),
new ArrayDenormalizer(),
], ['json' => new JsonEncoder()]);

Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.