Skip to content

Navigation Menu

Sign in
Appearance settings

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.4
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
draft recursive object normalizer
  • Loading branch information
idbentley committed Jun 26, 2024
commit 30977f4d6249d385a0711f920f92678dc7518005
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 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
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 @@ public function __construct(
}
$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 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a

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);
} 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 getSupportedTypes(?string $format): array
*/
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) {
// 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 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form
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
@@ -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\NotNormalizableValueException;

/**
* @author Ian Bentley <ian@idbentley.com>
*/
final class PrimitiveDenormalizer implements DenormalizerInterface
{

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

public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
if ($type === 'int' || $type === 'float' || $type === 'string' || $type === 'bool' || $type === 'null') {
return true;
}
return false;
}

public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
// In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
// if a value is meant to be a string, float, int or a boolean value from the serialized representation.
// That's why we have to transform the values, if one of these non-string basic datatypes is expected.
if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
if ('' === $data) {
if (LegacyType::BUILTIN_TYPE_STRING === $builtinType) {
return '';
}
}

switch ($type) {
case 'bool':
// according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
if ('false' === $data || '0' === $data) {
return false;
} elseif ('true' === $data || '1' === $data) {
return true;
} else {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data of type bool expected ("%s" given).', $data));
}
break;
case "int":
if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) {
$data = (int) $data;
} else {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data of type int expected ("%s" given).', $data));
}
break;
case "float":
if (is_numeric($data)) {
return (float) $data;
}

return match ($data) {
'NaN' => \NAN,
'INF' => \INF,
'-INF' => -\INF,
default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data of type float expected ("%s" given).', $data)),
};
case "string":
return $data;
case "null":
return null;
}
}

}
}
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()) {
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");
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.