From c210d2bf4d81e47e1444eaf246c5d0c60ad8672d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 14 Nov 2024 10:51:05 +0100 Subject: [PATCH 1/6] prevent failures around not existing TypeInfo classes Having a getType() method on an extractor is not enough. Such a method may exist to be forward-compatible with the TypeInfo component. We still must not call it if the TypeInfo component is not installed to prevent running into errors for not-defined classes when the TypeInfo component is not installed. --- Normalizer/AbstractObjectNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 63068420ba1..3e3ef426ab8 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -938,7 +938,7 @@ private function getType(string $currentClass, string $attribute): Type|array|nu */ private function getPropertyType(string $className, string $property): Type|array|null { - if (method_exists($this->propertyTypeExtractor, 'getType')) { + if (class_exists(Type::class) && method_exists($this->propertyTypeExtractor, 'getType')) { return $this->propertyTypeExtractor->getType($className, $property); } From 7afcfbb87f161666091f1c4d0382ffdd5a07ac26 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 14 Nov 2024 13:01:20 +0100 Subject: [PATCH 2/6] [Serializer][PropertyInfo][Validator] TypeInfo 7.2 compatibility --- Normalizer/AbstractObjectNormalizer.php | 72 +++++++++++++++++++++---- Normalizer/ArrayDenormalizer.php | 12 ++++- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 63068420ba1..f8a3a41b5b0 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -34,10 +34,13 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\TypeInfo\Exception\LogicException as TypeInfoLogicException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; use Symfony\Component\TypeInfo\TypeIdentifier; /** @@ -644,7 +647,14 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; - $isUnionType = $type->asNonNullable() instanceof UnionType; + + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'asNonNullable')) { + $isUnionType = $type->asNonNullable() instanceof UnionType; + } else { + $isUnionType = $type instanceof UnionType; + } + $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; @@ -667,12 +677,23 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $collectionValueType = $t->getCollectionValueType(); } - $t = $t->getBaseType(); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $t = $t->getBaseType(); + } else { + while ($t instanceof WrappingTypeInterface) { + $t = $t->getWrappedType(); + } + } // Fix a collection that contains the only one element // This is special to xml format only - if ('xml' === $format && $collectionValueType && !$collectionValueType->isA(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { - $data = [$data]; + if ('xml' === $format && $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) { + // BC layer for type-info < 7.2 + $isMixedType = method_exists(Type::class, 'isA') ? $collectionValueType->isA(TypeIdentifier::MIXED) : $collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED); + if (!$isMixedType) { + $data = [$data]; + } } // This try-catch should cover all NotNormalizableValueException (and all return branches after the first @@ -695,7 +716,10 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return ''; } - $isNullable = $isNullable ?: $type->isNullable(); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'isNullable')) { + $isNullable = $isNullable ?: $type->isNullable(); + } } switch ($typeIdentifier) { @@ -732,7 +756,16 @@ private function validateAndDenormalize(Type $type, string $currentClass, string if ($collectionValueType) { try { - $collectionValueBaseType = $collectionValueType->getBaseType(); + $collectionValueBaseType = $collectionValueType; + + // BC layer for type-info < 7.2 + if (!interface_exists(WrappingTypeInterface::class)) { + $collectionValueBaseType = $collectionValueType->getBaseType(); + } else { + while ($collectionValueBaseType instanceof WrappingTypeInterface) { + $collectionValueBaseType = $collectionValueBaseType->getWrappedType(); + } + } } catch (TypeInfoLogicException) { $collectionValueBaseType = Type::mixed(); } @@ -742,15 +775,29 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $class = $collectionValueBaseType->getClassName().'[]'; $context['key_type'] = $collectionKeyType; $context['value_type'] = $collectionValueType; - } elseif (TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { + } elseif ( + // BC layer for type-info < 7.2 + !class_exists(NullableType::class) && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + || $collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + ) { // get inner type for any nested array $innerType = $collectionValueType; + if ($innerType instanceof NullableType) { + $innerType = $innerType->getWrappedType(); + } // note that it will break for any other builtinType $dimensions = '[]'; while ($innerType instanceof CollectionType) { $dimensions .= '[]'; $innerType = $innerType->getCollectionValueType(); + if ($innerType instanceof NullableType) { + $innerType = $innerType->getWrappedType(); + } + } + + while ($innerType instanceof WrappingTypeInterface) { + $innerType = $innerType->getWrappedType(); } if ($innerType instanceof ObjectType) { @@ -862,8 +909,15 @@ private function validateAndDenormalize(Type $type, string $currentClass, string throw $missingConstructorArgumentsException; } - if (!$isUnionType && $e) { - throw $e; + // BC layer for type-info < 7.2 + if (!class_exists(NullableType::class)) { + if (!$isUnionType && $e) { + throw $e; + } + } else { + if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) { + throw $e; + } } if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index 347030c244c..964d74b61a8 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -16,7 +16,9 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Denormalizes arrays of objects. @@ -59,7 +61,15 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $typeIdentifiers = []; if (null !== $keyType = ($context['key_type'] ?? null)) { if ($keyType instanceof Type) { - $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + } else { + /** @var list|BuiltinType> */ + $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; + + $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); + } } else { $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); } From 5fff3abe545e26b2e024d18ad6e3f797e649f513 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 14 Nov 2024 13:01:20 +0100 Subject: [PATCH 3/6] [TypeInfo][Serializer][PropertyInfo][Validator] TypeInfo 7.1 compatibility --- Normalizer/AbstractObjectNormalizer.php | 59 ++++++++++++++++++++----- Normalizer/ArrayDenormalizer.php | 13 ++++-- composer.json | 3 +- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 3dbb750f1d4..fb45a924bee 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -32,6 +32,7 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Exception\LogicException as TypeInfoLogicException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; @@ -646,6 +647,14 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; + + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'asNonNullable')) { + $isUnionType = $type->asNonNullable() instanceof UnionType; + } else { + $isUnionType = $type instanceof UnionType; + } + $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; @@ -667,14 +676,23 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $collectionValueType = $t->getCollectionValueType(); } - while ($t instanceof WrappingTypeInterface) { - $t = $t->getWrappedType(); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $t = $t->getBaseType(); + } else { + while ($t instanceof WrappingTypeInterface) { + $t = $t->getWrappedType(); + } } // Fix a collection that contains the only one element // This is special to xml format only - if ('xml' === $format && $collectionValueType && !$collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { - $data = [$data]; + if ('xml' === $format && $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) { + // BC layer for type-info < 7.2 + $isMixedType = method_exists(Type::class, 'isA') ? $collectionValueType->isA(TypeIdentifier::MIXED) : $collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED); + if (!$isMixedType) { + $data = [$data]; + } } // This try-catch should cover all NotNormalizableValueException (and all return branches after the first @@ -731,9 +749,19 @@ private function validateAndDenormalize(Type $type, string $currentClass, string } if ($collectionValueType) { - $collectionValueBaseType = $collectionValueType; - while ($collectionValueBaseType instanceof WrappingTypeInterface) { - $collectionValueBaseType = $collectionValueBaseType->getWrappedType(); + try { + $collectionValueBaseType = $collectionValueType; + + // BC layer for type-info < 7.2 + if (!interface_exists(WrappingTypeInterface::class)) { + $collectionValueBaseType = $collectionValueType->getBaseType(); + } else { + while ($collectionValueBaseType instanceof WrappingTypeInterface) { + $collectionValueBaseType = $collectionValueBaseType->getWrappedType(); + } + } + } catch (TypeInfoLogicException) { + $collectionValueBaseType = Type::mixed(); } if ($collectionValueBaseType instanceof ObjectType) { @@ -741,7 +769,11 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $class = $collectionValueBaseType->getClassName().'[]'; $context['key_type'] = $collectionKeyType; $context['value_type'] = $collectionValueType; - } elseif ($collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { + } elseif ( + // BC layer for type-info < 7.2 + !class_exists(NullableType::class) && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + || $collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + ) { // get inner type for any nested array $innerType = $collectionValueType; if ($innerType instanceof NullableType) { @@ -871,8 +903,15 @@ private function validateAndDenormalize(Type $type, string $currentClass, string throw $missingConstructorArgumentsException; } - if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) { - throw $e; + // BC layer for type-info < 7.2 + if (!class_exists(NullableType::class)) { + if (!$isUnionType && $e) { + throw $e; + } + } else { + if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) { + throw $e; + } } if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index 08fae04df85..96c4d259cde 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -56,10 +56,15 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $typeIdentifiers = []; if (null !== $keyType = ($context['key_type'] ?? null)) { if ($keyType instanceof Type) { - /** @var list|BuiltinType> */ - $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; - - $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + } else { + /** @var list|BuiltinType> */ + $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; + + $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); + } } else { $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); } diff --git a/composer.json b/composer.json index 4e6865523a7..d8809fa079e 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.2", + "symfony/type-info": "^7.1", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", @@ -51,7 +51,6 @@ "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", - "symfony/type-info": "<7.2", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" From 81f032d2ee6a3cd8b75990941a1e3f87c8ad086e Mon Sep 17 00:00:00 2001 From: wanxiangchwng Date: Sat, 23 Nov 2024 10:47:03 +0800 Subject: [PATCH 4/6] chore: fix some typos Signed-off-by: wanxiangchwng --- Tests/Encoder/XmlEncoderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 31d2ddfc69c..0eb332e80ce 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -149,7 +149,7 @@ public static function validEncodeProvider(): iterable ], ]; - yield 'encode remvoing empty tags' => [ + yield 'encode removing empty tags' => [ ''."\n". 'Peter'."\n", ['person' => ['firstname' => 'Peter', 'lastname' => null]], From c9a49af37c46114a884a9c0945f7f75a3e8b1ec0 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Mon, 25 Nov 2024 01:26:19 +0100 Subject: [PATCH 5/6] CS: re-apply trailing_comma_in_multiline --- Tests/Normalizer/ObjectNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 93ed5e468b8..d45586b4444 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -976,7 +976,7 @@ public function testNormalizeWithMethodNamesSimilarToAccessors() 'tell' => true, 'class' => true, 'responsibility' => true, - 123 => 321 + 123 => 321, ], $normalized); } } From 3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 16 Nov 2024 15:49:06 +0100 Subject: [PATCH 6/6] Proofread UPGRADE guide --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b7a1fac345..4c36d5885a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ CHANGELOG 7.2 --- - * Deprecate the `csv_escape_char` context option of `CsvEncoder` and the `CsvEncoder::ESCAPE_CHAR_KEY` constant - * Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method + * Deprecate the `csv_escape_char` context option of `CsvEncoder`, the `CsvEncoder::ESCAPE_CHAR_KEY` constant + and the `CsvEncoderContextBuilder::withEscapeChar()` method, following its deprecation in PHP 8.4 * Add `SnakeCaseToCamelCaseNameConverter` * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization * Add the `UidNormalizer::NORMALIZATION_FORMAT_RFC9562` constant