Skip to content

[TypeInfo] Redesign Type methods and nullability #57630

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 13, 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
2 changes: 1 addition & 1 deletion src/Symfony/Bridge/Doctrine/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"symfony/security-core": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0",
"symfony/translation": "^6.4|^7.0",
"symfony/type-info": "^7.1",
"symfony/type-info": "^7.2",
"symfony/uid": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0",
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"symfony/string": "^6.4|^7.0",
"symfony/translation": "^6.4|^7.0",
"symfony/twig-bundle": "^6.4|^7.0",
"symfony/type-info": "^7.1",
"symfony/type-info": "^7.2",
"symfony/validator": "^6.4|^7.0",
"symfony/workflow": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ 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())];
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 @@ -869,7 +869,7 @@ 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))];
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 @@ -771,7 +771,7 @@ 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()))];
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
31 changes: 13 additions & 18 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 All @@ -183,7 +184,7 @@ private function createLegacyType(DocType $type, bool $nullable): ?LegacyType

[$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen);

$collection = \is_a($class, \Traversable::class, true) || \is_a($class, \ArrayAccess::class, true);
$collection = is_a($class, \Traversable::class, true) || is_a($class, \ArrayAccess::class, true);

// it's safer to fall back to other extractors if the generic type is too abstract
if (!$collection && !class_exists($class)) {
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
2 changes: 1 addition & 1 deletion src/Symfony/Component/PropertyInfo/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"require": {
"php": ">=8.2",
"symfony/string": "^6.4|^7.0",
"symfony/type-info": "^7.1"
"symfony/type-info": "^7.2"
},
"require-dev": {
"symfony/serializer": "^6.4|^7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
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;
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,11 +646,9 @@ 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;
$e = null;
$extraAttributesException = null;
$missingConstructorArgumentsException = null;
$isNullable = false;

$types = match (true) {
$type instanceof IntersectionType => throw new LogicException('Unable to handle intersection type.'),
Expand All @@ -667,11 +667,13 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
$collectionValueType = $t->getCollectionValueType();
}

$t = $t->getBaseType();
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)))) {
if ('xml' === $format && $collectionValueType && !$collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) {
$data = [$data];
}

Expand All @@ -694,8 +696,6 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
if (TypeIdentifier::STRING === $typeIdentifier) {
return '';
}

$isNullable = $isNullable ?: $type->isNullable();
}

switch ($typeIdentifier) {
Expand Down Expand Up @@ -731,26 +731,35 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
}

if ($collectionValueType) {
try {
$collectionValueBaseType = $collectionValueType->getBaseType();
} catch (TypeInfoLogicException) {
$collectionValueBaseType = Type::mixed();
$collectionValueBaseType = $collectionValueType;
while ($collectionValueBaseType instanceof WrappingTypeInterface) {
$collectionValueBaseType = $collectionValueBaseType->getWrappedType();
}

if ($collectionValueBaseType instanceof ObjectType) {
$typeIdentifier = TypeIdentifier::OBJECT;
$class = $collectionValueBaseType->getClassName().'[]';
$context['key_type'] = $collectionKeyType;
$context['value_type'] = $collectionValueType;
} elseif (TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) {
} elseif ($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) {
Expand Down Expand Up @@ -832,17 +841,17 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
return $data;
}
} catch (NotNormalizableValueException|InvalidArgumentException $e) {
if (!$isUnionType && !$isNullable) {
if (!$type instanceof UnionType) {
throw $e;
}
} catch (ExtraAttributesException $e) {
if (!$isUnionType && !$isNullable) {
if (!$type instanceof UnionType) {
throw $e;
}

$extraAttributesException ??= $e;
} catch (MissingConstructorArgumentsException $e) {
if (!$isUnionType && !$isNullable) {
if (!$type instanceof UnionType) {
throw $e;
}

Expand All @@ -862,7 +871,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
throw $missingConstructorArgumentsException;
}

if (!$isUnionType && $e) {
if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) {
throw $e;
}

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 @@ -54,7 +56,10 @@ 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]);
/** @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
4 changes: 2 additions & 2 deletions src/Symfony/Component/Serializer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.1.5",
"symfony/type-info": "^7.2",
"symfony/uid": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0",
Expand All @@ -51,7 +51,7 @@
"symfony/dependency-injection": "<6.4",
"symfony/property-access": "<6.4",
"symfony/property-info": "<6.4",
"symfony/type-info": "<7.1.5",
"symfony/type-info": "<7.2",
"symfony/uid": "<6.4",
"symfony/validator": "<6.4",
"symfony/yaml": "<6.4"
Expand Down
7 changes: 7 additions & 0 deletions src/Symfony/Component/TypeInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ CHANGELOG
7.2
---

* Add construction validation for `BackedEnumType`, `CollectionType`, `GenericType`, `IntersectionType`, and `UnionType`
* Add `TypeIdentifier::isStandalone()`, `TypeIdentifier::isScalar()`, and `TypeIdentifier::isBool()` methods
* Add `WrappingTypeInterface` and `CompositeTypeInterface` type interfaces
* Add `NullableType` type class
* Rename `Type::isA()` to `Type::isIdentifiedBy()` and `Type::is()` to `Type::isSatisfiedBy()`
* Remove `Type::getBaseType()`, `Type::asNonNullable()` and `Type::__call()` methods
* Remove `CompositeTypeTrait`
* Add `PhpDocAwareReflectionTypeResolver` resolver

7.1
Expand Down
33 changes: 6 additions & 27 deletions src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,21 @@
namespace Symfony\Component\TypeInfo\Tests\Type;

use PHPUnit\Framework\TestCase;
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\TypeIdentifier;

class BackedEnumTypeTest extends TestCase
{
public function testToString()
{
$this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int()));
}

public function testIsNullable()
{
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isNullable());
}

public function testGetBaseType()
public function testCannotCreateInvalidBackingBuiltinType()
{
$this->assertEquals(new BackedEnumType(DummyBackedEnum::class, Type::int()), (new BackedEnumType(DummyBackedEnum::class, Type::int()))->getBaseType());
$this->expectException(InvalidArgumentException::class);
new BackedEnumType(DummyBackedEnum::class, Type::bool());
}

public function testAsNonNullable()
{
$type = new BackedEnumType(DummyBackedEnum::class, Type::int());

$this->assertSame($type, $type->asNonNullable());
}

public function testIsA()
public function testToString()
{
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::ARRAY));
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::OBJECT));
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(self::class));
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(DummyBackedEnum::class));
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(\BackedEnum::class));
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(\UnitEnum::class));
$this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int()));
}
}
Loading