From 30977f4d6249d385a0711f920f92678dc7518005 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 26 Jun 2024 15:27:32 -0400 Subject: [PATCH 1/2] draft recursive object normalizer --- .../Normalizer/AbstractObjectNormalizer.php | 26 ++- .../Normalizer/ArrayDenormalizer.php | 27 ++- .../Normalizer/ObjectNormalizer.php | 14 +- .../Normalizer/PrimitiveDenormalizer.php | 84 ++++++++ .../Normalizer/PropertyNormalizer.php | 4 +- .../Normalizer/UnionDenormalizer.php | 84 ++++++++ .../DeserializeNestedArrayOfObjectsTest.php | 34 ++- .../Serializer/Tests/SerializerTest.php | 201 +++++++++++++----- 8 files changed, 394 insertions(+), 80 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php create mode 100644 src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index df3d693f21fc9..b3ac8293e2a5c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -45,8 +45,15 @@ * * @author Kévin Dunglas */ -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. */ @@ -122,6 +129,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer private array $typeCache = []; private array $attributesCache = []; private readonly \Closure $objectClassResolver; + private bool $consistentDenormalization; public function __construct( ?ClassMetadataFactoryInterface $classMetadataFactory = null, @@ -129,6 +137,7 @@ public function __construct( private ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, + bool $consistentDenormalization = false, array $defaultContext = [], ) { parent::__construct($classMetadataFactory, $nameConverter, $defaultContext); @@ -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 @@ -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'])) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php index 1bd6c54b374ce..55504a96f0a52 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php @@ -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. * @@ -44,20 +46,30 @@ 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 { @@ -65,6 +77,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } } + foreach ($data as $key => $value) { $subContext = $context; $subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]"; @@ -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); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 1b51b729c660b..299199353384d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -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 diff --git a/src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php new file mode 100644 index 0000000000000..86f62818c1fbf --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php @@ -0,0 +1,84 @@ + + * + * 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 + */ +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; + } + } + + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index e1d893be89771..341546558e5cb 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -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; diff --git a/src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php new file mode 100644 index 0000000000000..615a264d6fa25 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/UnionDenormalizer.php @@ -0,0 +1,84 @@ + + * + * 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 + */ +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"); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php index 8da1b471bd567..d0a368c0ed801 100644 --- a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php +++ b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php @@ -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 = << new JsonEncoder()]); @@ -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 = << new JsonEncoder()]); diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 1bf5905adf971..55ea354dc6a48 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -50,6 +50,8 @@ use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Normalizer\UidNormalizer; use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; +use Symfony\Component\Serializer\Normalizer\UnionDenormalizer; +use Symfony\Component\Serializer\Normalizer\PrimitiveDenormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; @@ -80,6 +82,17 @@ class SerializerTest extends TestCase { + + public static function consistencyProvider() + { + return [ + // consistent + [true], + // legacy + [false], + ]; + } + public function testItThrowsExceptionOnInvalidNormalizer() { $this->expectException(InvalidArgumentException::class); @@ -392,6 +405,12 @@ public function testDeserializeArray() $expectedData, $serializer->deserialize($jsonData, __NAMESPACE__.'\Model[]', 'json') ); + // Test that PHPDoc template type format supported. + $this->assertEquals( + $expectedData, + $serializer->deserialize($jsonData, 'array<'.__NAMESPACE__.'\Model>', 'json') + ); + } public function testNormalizerAware() @@ -412,16 +431,22 @@ public function testDenormalizerAware() new Serializer([$denormalizerAware]); } - public function testDeserializeObjectConstructorWithObjectTypeHint() + /** + * @dataProvider consistencyProvider + */ + public function testDeserializeObjectConstructorWithObjectTypeHint($consistency) { $jsonData = '{"bar":{"value":"baz"}}'; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([new ObjectNormalizer(consistentDenormalization: $consistency)], ['json' => new JsonEncoder()]); $this->assertEquals(new Foo(new Bar('baz')), $serializer->deserialize($jsonData, Foo::class, 'json')); } - public function testDeserializeAndSerializeAbstractObjectsWithTheClassMetadataDiscriminatorResolver() + /** + * @dataProvider consistencyProvider + */ + public function testDeserializeAndSerializeAbstractObjectsWithTheClassMetadataDiscriminatorResolver($consistency) { $example = new AbstractDummyFirstChild('foo-value', 'bar-value'); $example->setQuux(new DummyFirstChildQuux('quux')); @@ -449,7 +474,7 @@ public function hasMetadataFor($value): bool }; $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PhpDocExtractor(), $discriminatorResolver)], ['json' => new JsonEncoder()]); + $serializer = new Serializer([new PrimitiveDenormalizer(), new UnionDenormalizer(), new ObjectNormalizer(null, null, null, new PhpDocExtractor(), $discriminatorResolver, null, $consistency)], ['json' => new JsonEncoder()]); $jsonData = '{"type":"first","quux":{"value":"quux"},"bar":"bar-value","foo":"foo-value"}'; @@ -460,14 +485,17 @@ public function hasMetadataFor($value): bool $this->assertEquals($jsonData, $serialized); } - public function testDeserializeAndSerializeInterfacedObjectsWithTheClassMetadataDiscriminatorResolver() + /** + * @dataProvider consistencyProvider + */ + public function testDeserializeAndSerializeInterfacedObjectsWithTheClassMetadataDiscriminatorResolver($consistency) { $example = new DummyMessageNumberOne(); $example->one = 1; $jsonData = '{"type":"one","one":1,"two":null}'; - $serializer = $this->serializerWithClassDiscriminator(); + $serializer = $this->serializerWithClassDiscriminator($consistency); $deserialized = $serializer->deserialize($jsonData, DummyMessageInterface::class, 'json'); $this->assertEquals($example, $deserialized); @@ -475,12 +503,15 @@ public function testDeserializeAndSerializeInterfacedObjectsWithTheClassMetadata $this->assertEquals($jsonData, $serialized); } - public function testDeserializeAndSerializeInterfacedObjectsWithTheClassMetadataDiscriminatorResolverAndGroups() + /** + * @dataProvider consistencyProvider + */ + public function testDeserializeAndSerializeInterfacedObjectsWithTheClassMetadataDiscriminatorResolverAndGroups($consistent) { $example = new DummyMessageNumberOne(); $example->two = 2; - $serializer = $this->serializerWithClassDiscriminator(); + $serializer = $this->serializerWithClassDiscriminator($consistent); $deserialized = $serializer->deserialize('{"type":"one","one":1,"two":2}', DummyMessageInterface::class, 'json', [ 'groups' => ['two'], ]); @@ -494,7 +525,10 @@ public function testDeserializeAndSerializeInterfacedObjectsWithTheClassMetadata $this->assertEquals('{"two":2,"type":"one"}', $serialized); } - public function testDeserializeAndSerializeNestedInterfacedObjectsWithTheClassMetadataDiscriminator() + /** + * @dataProvider consistencyProvider + */ + public function testDeserializeAndSerializeNestedInterfacedObjectsWithTheClassMetadataDiscriminator($consistent) { $nested = new DummyMessageNumberOne(); $nested->one = 'foo'; @@ -502,7 +536,7 @@ public function testDeserializeAndSerializeNestedInterfacedObjectsWithTheClassMe $example = new DummyMessageNumberTwo(); $example->setNested($nested); - $serializer = $this->serializerWithClassDiscriminator(); + $serializer = $this->serializerWithClassDiscriminator($consistent); $serialized = $serializer->serialize($example, 'json'); $deserialized = $serializer->deserialize($serialized, DummyMessageInterface::class, 'json'); @@ -510,11 +544,14 @@ public function testDeserializeAndSerializeNestedInterfacedObjectsWithTheClassMe $this->assertEquals($example, $deserialized); } - public function testDeserializeAndSerializeNestedAbstractAndInterfacedObjectsWithTheClassMetadataDiscriminator() + /** + * @dataProvider consistencyProvider + */ + public function testDeserializeAndSerializeNestedAbstractAndInterfacedObjectsWithTheClassMetadataDiscriminator($consistent) { $example = new DummyMessageNumberThree(); - $serializer = $this->serializerWithClassDiscriminator(); + $serializer = $this->serializerWithClassDiscriminator($consistent); $serialized = $serializer->serialize($example, 'json'); $deserialized = $serializer->deserialize($serialized, DummyMessageInterface::class, 'json'); @@ -522,10 +559,13 @@ public function testDeserializeAndSerializeNestedAbstractAndInterfacedObjectsWit $this->assertEquals($example, $deserialized); } - public function testExceptionWhenTypeIsNotKnownInDiscriminator() + /** + * @dataProvider consistencyProvider + */ + public function testExceptionWhenTypeIsNotKnownInDiscriminator($consistent) { try { - $this->serializerWithClassDiscriminator()->deserialize('{"type":"second","one":1}', DummyMessageInterface::class, 'json'); + $this->serializerWithClassDiscriminator($consistent)->deserialize('{"type":"second","one":1}', DummyMessageInterface::class, 'json'); $this->fail(); } catch (\Throwable $e) { @@ -538,10 +578,13 @@ public function testExceptionWhenTypeIsNotKnownInDiscriminator() } } - public function testExceptionWhenTypeIsNotInTheBodyToDeserialiaze() + /** + * @dataProvider consistencyProvider + */ + public function testExceptionWhenTypeIsNotInTheBodyToDeserialiaze($consistent) { try { - $this->serializerWithClassDiscriminator()->deserialize('{"one":1}', DummyMessageInterface::class, 'json'); + $this->serializerWithClassDiscriminator($consistent)->deserialize('{"one":1}', DummyMessageInterface::class, 'json'); $this->fail(); } catch (\Throwable $e) { @@ -562,12 +605,15 @@ public function testNotNormalizableValueExceptionMessageForAResource() (new Serializer())->normalize(tmpfile()); } - public function testNormalizeTransformEmptyArrayObjectToArray() + /** + * @dataProvider consistencyProvider + */ + public function testNormalizeTransformEmptyArrayObjectToArray($consistency) { $serializer = new Serializer( [ new PropertyNormalizer(), - new ObjectNormalizer(), + new ObjectNormalizer(consistentDenormalization: $consistency), new ArrayDenormalizer(), ], [ @@ -587,6 +633,17 @@ public function testNormalizeTransformEmptyArrayObjectToArray() public static function provideObjectOrCollectionTests() { + $consistentSerializer = new Serializer( + [ + new PropertyNormalizer(), + new ObjectNormalizer(consistentDenormalization: true), + new ArrayDenormalizer(), + ], + [ + 'json' => new JsonEncoder(), + ] + ); + $serializer = new Serializer( [ new PropertyNormalizer(), @@ -633,7 +690,10 @@ public function __construct(\ArrayObject $map) $data['g1'] = new Baz([]); $data['g2'] = new Baz(['greg']); - yield [$serializer, $data]; + return [ + [$serializer, $data], + [$consistentSerializer, $data], + ]; } /** @dataProvider provideObjectOrCollectionTests */ @@ -761,9 +821,11 @@ public function testDeserializeInconsistentScalarArray() $serializer->deserialize('["42"]', 'int[]', 'json'); } - public function testDeserializeOnObjectWithObjectCollectionProperty() + /** @dataProvider consistencyProvider */ + public function testDeserializeOnObjectWithObjectCollectionProperty($consistent) { - $serializer = new Serializer([new FooInterfaceDummyDenormalizer(), new ObjectNormalizer(null, null, null, new PhpDocExtractor())], [new JsonEncoder()]); + $consistent = false; //TO_DO + $serializer = new Serializer([new FooInterfaceDummyDenormalizer(), new ObjectNormalizer(null, null, null, new PhpDocExtractor(), null, null, $consistent)], [new JsonEncoder()]); $obj = $serializer->deserialize('{"foo":[{"name":"bar"}]}', ObjectCollectionPropertyDummy::class, 'json'); $this->assertInstanceOf(ObjectCollectionPropertyDummy::class, $obj); @@ -793,14 +855,16 @@ public function testDeserializeNullableIntInXml() $this->assertNull($obj->value); } - public function testUnionTypeDeserializable() + /** @dataProvider consistencyProvider */ + public function testUnionTypeDeserializable($consistent) { + $consistent = false; // TO_DO $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( [ new DateTimeNormalizer(), - new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory)), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory), null, $consistent), ], ['json' => new JsonEncoder()] ); @@ -827,6 +891,7 @@ public function testUnionTypeDeserializable() public function testUnionTypeDeserializableWithoutAllowedExtraAttributes() { + // TO_DO $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( @@ -860,48 +925,56 @@ public function testUnionTypeDeserializableWithoutAllowedExtraAttributes() ]); } - public function testFalseBuiltInTypes() + /** @dataProvider consistencyProvider */ + public function testFalseBuiltInTypes($consistent) { + $consistent = false;// TO_DO $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['json' => new JsonEncoder()]); + $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor, null, null, $consistent)], ['json' => new JsonEncoder()]); $actual = $serializer->deserialize('{"false":false}', FalseBuiltInDummy::class, 'json'); $this->assertEquals(new FalseBuiltInDummy(), $actual); } - public function testTrueBuiltInTypes() + /** @dataProvider consistencyProvider */ + public function testTrueBuiltInTypes($consistent) { + $consistent = false;// TO_DO + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['json' => new JsonEncoder()]); + $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor, null, null, $consistent)], ['json' => new JsonEncoder()]); $actual = $serializer->deserialize('{"true":true}', TrueBuiltInDummy::class, 'json'); $this->assertEquals(new TrueBuiltInDummy(), $actual); } - public function testDeserializeUntypedFormat() + /** @dataProvider consistencyProvider */ + public function testDeserializeUntypedFormat($consistent) { - $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))], ['csv' => new CsvEncoder()]); + $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]), null, null, $consistent)], ['csv' => new CsvEncoder()]); $actual = $serializer->deserialize('value'.\PHP_EOL.',', DummyWithObjectOrNull::class, 'csv', [CsvEncoder::AS_COLLECTION_KEY => false]); $this->assertEquals(new DummyWithObjectOrNull(null), $actual); } - private function serializerWithClassDiscriminator() + + private function serializerWithClassDiscriminator($consistent=false) { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); - return new Serializer([new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor(), new ClassDiscriminatorFromClassMetadata($classMetadataFactory))], ['json' => new JsonEncoder()]); + return new Serializer([new PrimitiveDenormalizer(), new UnionDenormalizer(), new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor(), new ClassDiscriminatorFromClassMetadata($classMetadataFactory), null, $consistent)], ['json' => new JsonEncoder()]); } - public function testDeserializeAndUnwrap() + /** @dataProvider consistencyProvider */ + public function testDeserializeAndUnwrap($consistent) { $jsonData = '{"baz": {"foo": "bar", "inner": {"title": "value", "numbers": [5,3]}}}'; $expectedData = Model::fromArray(['title' => 'value', 'numbers' => [5, 3]]); - $serializer = new Serializer([new UnwrappingDenormalizer(new PropertyAccessor()), new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([new UnwrappingDenormalizer(new PropertyAccessor()), new ObjectNormalizer(consistentDenormalization: $consistent)], ['json' => new JsonEncoder()]); $this->assertEquals( $expectedData, @@ -912,8 +985,9 @@ public function testDeserializeAndUnwrap() /** * @dataProvider provideCollectDenormalizationErrors */ - public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMetadataFactory) + public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMetadataFactory, bool $consistent) { + $consistent = false; //TO_DO $json = ' { "string": null, @@ -956,7 +1030,7 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet new DateTimeZoneNormalizer(), new DataUriNormalizer(), new UidNormalizer(), - new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null, null, $consistent), ], ['json' => new JsonEncoder()] ); @@ -1151,8 +1225,9 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet /** * @dataProvider provideCollectDenormalizationErrors */ - public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMetadataFactory) + public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMetadataFactory, $consistent) { + $consistent = false; //TO_DO $json = ' [ { @@ -1168,7 +1243,7 @@ public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMe $serializer = new Serializer( [ new ArrayDenormalizer(), - new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null, null, $consistent), ], ['json' => new JsonEncoder()] ); @@ -1219,7 +1294,8 @@ public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMe $this->assertSame($expected, $exceptionsAsArray); } - public function testCollectDenormalizationErrorsWithoutTypeExtractor() + /** @dataProvider consistencyProvider */ + public function testCollectDenormalizationErrorsWithoutTypeExtractor($consistency) { $json = ' { @@ -1228,7 +1304,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() "float": [] }'; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([new ObjectNormalizer(consistentDenormalization: $consistency)], ['json' => new JsonEncoder()]); try { $serializer->deserialize($json, Php74Full::class, 'json', [ @@ -1286,15 +1362,16 @@ class_exists(InvalidTypeException::class) ? 'float' : 'unknown', /** * @dataProvider provideCollectDenormalizationErrors */ - public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFactory $classMetadataFactory) + public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFactory $classMetadataFactory, $consistent) { + $consistent = false; //TO_DO $json = '{"bool": "bool"}'; $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( [ - new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null, null, $consistent), ], ['json' => new JsonEncoder()] ); @@ -1352,14 +1429,15 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa $this->assertSame($expected, $exceptionsAsArray); } - public function testCollectDenormalizationErrorsWithInvalidConstructorTypes() + /** @dataProvider consistencyProvider */ + public function testCollectDenormalizationErrorsWithInvalidConstructorTypes($consistent) { $json = '{"string": "some string", "bool": "bool", "int": true}'; $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); $serializer = new Serializer( - [new ObjectNormalizer(null, null, null, $extractor)], + [new ObjectNormalizer(null, null, null, $extractor, null, null, $consistent)], ['json' => new JsonEncoder()] ); @@ -1413,12 +1491,13 @@ public function testCollectDenormalizationErrorsWithInvalidConstructorTypes() $this->assertSame($expected, $exceptionsAsArray); } - public function testCollectDenormalizationErrorsWithEnumConstructor() + /** @dataProvider consistencyProvider */ + public function testCollectDenormalizationErrorsWithEnumConstructor($consistent) { $serializer = new Serializer( [ new BackedEnumNormalizer(), - new ObjectNormalizer(), + new ObjectNormalizer(consistentDenormalization: $consistent), ], ['json' => new JsonEncoder()] ); @@ -1448,7 +1527,8 @@ public function testCollectDenormalizationErrorsWithEnumConstructor() $this->assertSame($expected, $exceptionsAsArray); } - public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruct() + /** @dataProvider consistencyProvider */ + public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruct($consistent) { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $reflectionExtractor = new ReflectionExtractor(); @@ -1457,7 +1537,7 @@ public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruc $serializer = new Serializer( [ new BackedEnumNormalizer(), - new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor), + new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor, null, null, $consistent), ], ['json' => new JsonEncoder()] ); @@ -1489,12 +1569,13 @@ public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruc $this->assertSame($expected, $exceptionsAsArray); } - public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor() + /** @dataProvider consistencyProvider */ + public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor($consistent) { $serializer = new Serializer( [ new BackedEnumNormalizer(), - new ObjectNormalizer(), + new ObjectNormalizer(consistentDenormalization: $consistent), ], ['json' => new JsonEncoder()] ); @@ -1509,7 +1590,8 @@ public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor() } } - public function testGroupsOnClassSerialization() + /** @dataProvider consistencyProvider */ + public function testGroupsOnClassSerialization($consistent) { $obj = new Fixtures\Attributes\GroupClassDummy(); $obj->setFoo('foo'); @@ -1518,7 +1600,7 @@ public function testGroupsOnClassSerialization() $serializer = new Serializer( [ - new ObjectNormalizer(), + new ObjectNormalizer(consistentDenormalization: $consistent), ], [ 'json' => new JsonEncoder(), @@ -1534,12 +1616,15 @@ public function testGroupsOnClassSerialization() public static function provideCollectDenormalizationErrors(): array { return [ - [null], - [new ClassMetadataFactory(new AttributeLoader())], + [null, false], + [null, true], + [new ClassMetadataFactory(new AttributeLoader()), false], + [new ClassMetadataFactory(new AttributeLoader()), true], ]; } - public function testSerializerUsesSupportedTypesMethod() + /** @dataProvider consistencyProvider */ + public function testSerializerUsesSupportedTypesMethod($consistent) { $neverCalledNormalizer = $this->createMock(DummyNormalizer::class); $neverCalledNormalizer @@ -1564,7 +1649,7 @@ public function testSerializerUsesSupportedTypesMethod() [ $neverCalledNormalizer, $supportedAndCachedNormalizer, - new ObjectNormalizer(), + new ObjectNormalizer(consistentDenormalization: $consistent), ], ['json' => new JsonEncoder()] ); @@ -1609,14 +1694,16 @@ public function testSerializerUsesSupportedTypesMethod() $serializer->denormalize('foo', Model::class, 'json'); } - public function testPartialDenormalizationWithMissingConstructorTypes() + /** @dataProvider consistencyProvider */ + public function testPartialDenormalizationWithMissingConstructorTypes($consistent) { + $consistent = false; // TO_DO $json = '{"one": "one string", "three": "three string"}'; $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); $serializer = new Serializer( - [new ObjectNormalizer(null, null, null, $extractor)], + [new ObjectNormalizer(null, null, null, $extractor, null, null, $consistent)], ['json' => new JsonEncoder()] ); From eda9113a89e6d4c01ba4ac68a1d58abd622a8944 Mon Sep 17 00:00:00 2001 From: Ian Bentley Date: Wed, 26 Jun 2024 15:45:39 -0400 Subject: [PATCH 2/2] remove primitiveDenormalizer --- .../Normalizer/PrimitiveDenormalizer.php | 84 ------------------- .../Serializer/Tests/SerializerTest.php | 6 +- 2 files changed, 3 insertions(+), 87 deletions(-) delete mode 100644 src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php diff --git a/src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php deleted file mode 100644 index 86f62818c1fbf..0000000000000 --- a/src/Symfony/Component/Serializer/Normalizer/PrimitiveDenormalizer.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * 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 - */ -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; - } - } - - } -} diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 55ea354dc6a48..c6c4683203c28 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -51,7 +51,7 @@ use Symfony\Component\Serializer\Normalizer\UidNormalizer; use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; use Symfony\Component\Serializer\Normalizer\UnionDenormalizer; -use Symfony\Component\Serializer\Normalizer\PrimitiveDenormalizer; +use Symfony\Component\Serializer\Normalizer\NullDenormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; @@ -474,7 +474,7 @@ public function hasMetadataFor($value): bool }; $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); - $serializer = new Serializer([new PrimitiveDenormalizer(), new UnionDenormalizer(), new ObjectNormalizer(null, null, null, new PhpDocExtractor(), $discriminatorResolver, null, $consistency)], ['json' => new JsonEncoder()]); + $serializer = new Serializer([new NullDenormalizer(), new UnionDenormalizer(), new ObjectNormalizer(null, null, null, new PhpDocExtractor(), $discriminatorResolver, null, $consistency)], ['json' => new JsonEncoder()]); $jsonData = '{"type":"first","quux":{"value":"quux"},"bar":"bar-value","foo":"foo-value"}'; @@ -964,7 +964,7 @@ private function serializerWithClassDiscriminator($consistent=false) { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); - return new Serializer([new PrimitiveDenormalizer(), new UnionDenormalizer(), new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor(), new ClassDiscriminatorFromClassMetadata($classMetadataFactory), null, $consistent)], ['json' => new JsonEncoder()]); + return new Serializer([new NullDenormalizer(), new UnionDenormalizer(), new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor(), new ClassDiscriminatorFromClassMetadata($classMetadataFactory), null, $consistent)], ['json' => new JsonEncoder()]); } /** @dataProvider consistencyProvider */