Skip to content

[PropertyInfo][Serializer][Validator] TypeInfo 7.2 compatibility #58872

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

Merged
merged 1 commit into from
Nov 14, 2024
Merged
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 @@ -27,6 +27,7 @@
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\NullableType;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand Down Expand Up @@ -562,7 +563,14 @@ public static function typeProvider(): iterable
yield ['f', Type::list(Type::object(\DateTimeImmutable::class)), null, null];
yield ['g', Type::nullable(Type::array()), 'Nullable array.', null];
yield ['h', Type::nullable(Type::string()), null, null];
yield ['i', Type::union(Type::int(), Type::string(), Type::null()), null, null];

// BC layer for type-info < 7.2
if (!class_exists(NullableType::class)) {
yield ['i', Type::union(Type::int(), Type::string(), Type::null()), null, null];
} else {
yield ['i', Type::nullable(Type::union(Type::int(), Type::string())), null, null];
}

yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class)), null, null];
yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int())), null, null];
yield ['donotexist', null, null, null];
Expand Down Expand Up @@ -629,7 +637,14 @@ public static function typeWithNoPrefixesProvider()
yield ['f', null];
yield ['g', Type::nullable(Type::array())];
yield ['h', Type::nullable(Type::string())];
yield ['i', Type::union(Type::int(), Type::string(), Type::null())];

// BC layer for type-info < 7.2
if (!class_exists(NullableType::class)) {
yield ['i', Type::union(Type::int(), Type::string(), Type::null())];
} else {
yield ['i', Type::nullable(Type::union(Type::int(), Type::string()))];
}

yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))];
yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))];
yield ['donotexist', null];
Expand Down Expand Up @@ -693,7 +708,14 @@ public static function typeWithCustomPrefixesProvider(): iterable
yield ['f', Type::list(Type::object(\DateTimeImmutable::class))];
yield ['g', Type::nullable(Type::array())];
yield ['h', Type::nullable(Type::string())];
yield ['i', Type::union(Type::int(), Type::string(), Type::null())];

// BC layer for type-info < 7.2
if (!class_exists(NullableType::class)) {
yield ['i', Type::union(Type::int(), Type::string(), Type::null())];
} else {
yield ['i', Type::nullable(Type::union(Type::int(), Type::string()))];
}

yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))];
yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))];
yield ['nonNullableCollectionOfNullableElements', Type::list(Type::nullable(Type::int()))];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Exception\LogicException;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;

require_once __DIR__.'/../Fixtures/Extractor/DummyNamespace.php';

Expand Down Expand Up @@ -869,7 +870,14 @@ public function testPseudoTypes(string $property, ?Type $type)
public static function pseudoTypesProvider(): iterable
{
yield ['classString', Type::string()];
yield ['classStringGeneric', Type::generic(Type::string(), Type::object(\stdClass::class))];

// BC layer for type-info < 7.2
if (!interface_exists(WrappingTypeInterface::class)) {
yield ['classStringGeneric', Type::generic(Type::string(), Type::object(\stdClass::class))];
} else {
yield ['classStringGeneric', Type::string()];
}

yield ['htmlEscapedString', Type::string()];
yield ['lowercaseString', Type::string()];
yield ['nonEmptyLowercaseString', Type::string()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Symfony\Component\PropertyInfo\Tests\Fixtures\SnakeCaseDummy;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\NullableType;
use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver;

/**
Expand Down Expand Up @@ -772,7 +773,14 @@ public static function php80TypesProvider(): iterable
yield ['foo', Type::nullable(Type::array())];
yield ['bar', Type::nullable(Type::int())];
yield ['timeout', Type::union(Type::int(), Type::float())];
yield ['optional', Type::union(Type::nullable(Type::int()), Type::nullable(Type::float()))];

// BC layer for type-info < 7.2
if (!class_exists(NullableType::class)) {
yield ['optional', Type::union(Type::nullable(Type::int()), Type::nullable(Type::float()))];
} else {
yield ['optional', Type::nullable(Type::union(Type::float(), Type::int()))];
}

yield ['string', Type::union(Type::string(), Type::object(\Stringable::class))];
yield ['payload', Type::mixed()];
yield ['data', Type::mixed()];
Expand Down
29 changes: 12 additions & 17 deletions src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ public function getType(DocType $varType): ?Type
$nullable = true;
}

return $this->createType($varType, $nullable);
$type = $this->createType($varType);

return $nullable ? Type::nullable($type) : $type;
}

$varTypes = [];
Expand Down Expand Up @@ -156,8 +158,7 @@ public function getType(DocType $varType): ?Type

$unionTypes = [];
foreach ($varTypes as $varType) {
$t = $this->createType($varType, $nullable);
if (null !== $t) {
if (null !== $t = $this->createType($varType)) {
$unionTypes[] = $t;
}
}
Expand Down Expand Up @@ -238,7 +239,7 @@ private function createLegacyType(DocType $type, bool $nullable): ?LegacyType
/**
* Creates a {@see Type} from a PHPDoc type.
*/
private function createType(DocType $docType, bool $nullable): ?Type
private function createType(DocType $docType): ?Type
{
$docTypeString = (string) $docType;

Expand All @@ -262,9 +263,8 @@ private function createType(DocType $docType, bool $nullable): ?Type
}

$type = null !== $class ? Type::object($class) : Type::builtin($phpType);
$type = Type::collection($type, ...$variableTypes);

return $nullable ? Type::nullable($type) : $type;
return Type::collection($type, ...$variableTypes);
}

if (!$docTypeString) {
Expand All @@ -277,9 +277,8 @@ private function createType(DocType $docType, bool $nullable): ?Type

if (str_starts_with($docTypeString, 'list<') && $docType instanceof Array_) {
$collectionValueType = $this->getType($docType->getValueType());
$type = Type::list($collectionValueType);

return $nullable ? Type::nullable($type) : $type;
return Type::list($collectionValueType);
}

if (str_starts_with($docTypeString, 'array<') && $docType instanceof Array_) {
Expand All @@ -288,16 +287,14 @@ private function createType(DocType $docType, bool $nullable): ?Type
$collectionKeyType = $this->getType($docType->getKeyType());
$collectionValueType = $this->getType($docType->getValueType());

$type = Type::array($collectionValueType, $collectionKeyType);

return $nullable ? Type::nullable($type) : $type;
return Type::array($collectionValueType, $collectionKeyType);
}

if ($docType instanceof PseudoType) {
if ($docType->underlyingType() instanceof Integer) {
return $nullable ? Type::nullable(Type::int()) : Type::int();
return Type::int();
} elseif ($docType->underlyingType() instanceof String_) {
return $nullable ? Type::nullable(Type::string()) : Type::string();
return Type::string();
}
}

Expand All @@ -314,12 +311,10 @@ private function createType(DocType $docType, bool $nullable): ?Type
[$phpType, $class] = $this->getPhpTypeAndClass($docTypeString);

if ('array' === $docTypeString) {
return $nullable ? Type::nullable(Type::array()) : Type::array();
return Type::array();
}

$type = null !== $class ? Type::object($class) : Type::builtin($phpType);

return $nullable ? Type::nullable($type) : $type;
return null !== $class ? Type::object($class) : Type::builtin($phpType);
}

private function normalizeType(string $docType): string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -644,7 +647,14 @@
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;
Expand All @@ -667,12 +677,23 @@
$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) {

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:684:38: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\WrappingTypeInterface does not exist (see https://psalm.dev/019)

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:684:38: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\WrappingTypeInterface does not exist (see https://psalm.dev/019)
$t = $t->getWrappedType();

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:685:26: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\WrappingTypeInterface does not exist (see https://psalm.dev/019)

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:685:26: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\WrappingTypeInterface does not exist (see https://psalm.dev/019)
}
}

// 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
Expand All @@ -695,7 +716,10 @@
return '';
}

$isNullable = $isNullable ?: $type->isNullable();
// BC layer for type-info < 7.2
if (method_exists(Type::class, 'isNullable')) {
$isNullable = $isNullable ?: $type->isNullable();
}
}

switch ($typeIdentifier) {
Expand Down Expand Up @@ -732,7 +756,16 @@

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();
}
Expand All @@ -742,15 +775,29 @@
$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) {

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:785:51: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\NullableType does not exist (see https://psalm.dev/019)
$innerType = $innerType->getWrappedType();

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:786:42: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\NullableType does not exist (see https://psalm.dev/019)
}

// note that it will break for any other builtinType
$dimensions = '[]';
while ($innerType instanceof CollectionType) {
$dimensions .= '[]';
$innerType = $innerType->getCollectionValueType();
if ($innerType instanceof NullableType) {

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:794:55: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\NullableType does not exist (see https://psalm.dev/019)
$innerType = $innerType->getWrappedType();

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

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:795:46: UndefinedClass: Class, interface or enum named Symfony\Component\TypeInfo\Type\NullableType does not exist (see https://psalm.dev/019)
}
}

while ($innerType instanceof WrappingTypeInterface) {
$innerType = $innerType->getWrappedType();
}

if ($innerType instanceof ObjectType) {
Expand Down Expand Up @@ -862,8 +909,15 @@
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<TypeIdentifier::INT>|BuiltinType<TypeIdentifier::STRING>> */
$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]);
}
Expand Down
Loading
Loading