Skip to content

Commit 8c376c8

Browse files
committed
[TypeInfo] Redesign Type methods and nullability
1 parent bc9f35c commit 8c376c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+683
-762
lines changed

src/Symfony/Bridge/Doctrine/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"symfony/security-core": "^6.4|^7.0",
4040
"symfony/stopwatch": "^6.4|^7.0",
4141
"symfony/translation": "^6.4|^7.0",
42-
"symfony/type-info": "^7.1",
42+
"symfony/type-info": "^7.2",
4343
"symfony/uid": "^6.4|^7.0",
4444
"symfony/validator": "^6.4|^7.0",
4545
"symfony/var-dumper": "^6.4|^7.0",

src/Symfony/Bundle/FrameworkBundle/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"symfony/string": "^6.4|^7.0",
6565
"symfony/translation": "^6.4|^7.0",
6666
"symfony/twig-bundle": "^6.4|^7.0",
67-
"symfony/type-info": "^7.1",
67+
"symfony/type-info": "^7.2",
6868
"symfony/validator": "^6.4|^7.0",
6969
"symfony/workflow": "^6.4|^7.0",
7070
"symfony/yaml": "^6.4|^7.0",

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ public static function typeWithCustomPrefixesProvider(): iterable
693693
yield ['f', Type::list(Type::object(\DateTimeImmutable::class))];
694694
yield ['g', Type::nullable(Type::array())];
695695
yield ['h', Type::nullable(Type::string())];
696-
yield ['i', Type::union(Type::int(), Type::string(), Type::null())];
696+
yield ['i', Type::nullable(Type::union(Type::int(), Type::string()))];
697697
yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))];
698698
yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))];
699699
yield ['nonNullableCollectionOfNullableElements', Type::list(Type::nullable(Type::int()))];

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,7 @@ public function testPseudoTypes(string $property, ?Type $type)
869869
public static function pseudoTypesProvider(): iterable
870870
{
871871
yield ['classString', Type::string()];
872-
yield ['classStringGeneric', Type::generic(Type::string(), Type::object(\stdClass::class))];
872+
yield ['classStringGeneric', Type::string()];
873873
yield ['htmlEscapedString', Type::string()];
874874
yield ['lowercaseString', Type::string()];
875875
yield ['nonEmptyLowercaseString', Type::string()];

src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ public static function php80TypesProvider(): iterable
771771
yield ['foo', Type::nullable(Type::array())];
772772
yield ['bar', Type::nullable(Type::int())];
773773
yield ['timeout', Type::union(Type::int(), Type::float())];
774-
yield ['optional', Type::union(Type::nullable(Type::int()), Type::nullable(Type::float()))];
774+
yield ['optional', Type::nullable(Type::union(Type::float(), Type::int()))];
775775
yield ['string', Type::union(Type::string(), Type::object(\Stringable::class))];
776776
yield ['payload', Type::mixed()];
777777
yield ['data', Type::mixed()];

src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php

+13-18
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ public function getType(DocType $varType): ?Type
128128
$nullable = true;
129129
}
130130

131-
return $this->createType($varType, $nullable);
131+
$type = $this->createType($varType);
132+
133+
return $nullable ? Type::nullable($type) : $type;
132134
}
133135

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

157159
$unionTypes = [];
158160
foreach ($varTypes as $varType) {
159-
$t = $this->createType($varType, $nullable);
160-
if (null !== $t) {
161+
if (null !== $t = $this->createType($varType)) {
161162
$unionTypes[] = $t;
162163
}
163164
}
@@ -183,7 +184,7 @@ private function createLegacyType(DocType $type, bool $nullable): ?LegacyType
183184

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

186-
$collection = \is_a($class, \Traversable::class, true) || \is_a($class, \ArrayAccess::class, true);
187+
$collection = is_a($class, \Traversable::class, true) || is_a($class, \ArrayAccess::class, true);
187188

188189
// it's safer to fall back to other extractors if the generic type is too abstract
189190
if (!$collection && !class_exists($class)) {
@@ -238,7 +239,7 @@ private function createLegacyType(DocType $type, bool $nullable): ?LegacyType
238239
/**
239240
* Creates a {@see Type} from a PHPDoc type.
240241
*/
241-
private function createType(DocType $docType, bool $nullable): ?Type
242+
private function createType(DocType $docType): ?Type
242243
{
243244
$docTypeString = (string) $docType;
244245

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

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

267-
return $nullable ? Type::nullable($type) : $type;
267+
return Type::collection($type, ...$variableTypes);
268268
}
269269

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

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

282-
return $nullable ? Type::nullable($type) : $type;
281+
return Type::list($collectionValueType);
283282
}
284283

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

291-
$type = Type::array($collectionValueType, $collectionKeyType);
292-
293-
return $nullable ? Type::nullable($type) : $type;
290+
return Type::array($collectionValueType, $collectionKeyType);
294291
}
295292

296293
if ($docType instanceof PseudoType) {
297294
if ($docType->underlyingType() instanceof Integer) {
298-
return $nullable ? Type::nullable(Type::int()) : Type::int();
295+
return Type::int();
299296
} elseif ($docType->underlyingType() instanceof String_) {
300-
return $nullable ? Type::nullable(Type::string()) : Type::string();
297+
return Type::string();
301298
}
302299
}
303300

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

316313
if ('array' === $docTypeString) {
317-
return $nullable ? Type::nullable(Type::array()) : Type::array();
314+
return Type::array();
318315
}
319316

320-
$type = null !== $class ? Type::object($class) : Type::builtin($phpType);
321-
322-
return $nullable ? Type::nullable($type) : $type;
317+
return null !== $class ? Type::object($class) : Type::builtin($phpType);
323318
}
324319

325320
private function normalizeType(string $docType): string

src/Symfony/Component/PropertyInfo/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"require": {
2626
"php": ">=8.2",
2727
"symfony/string": "^6.4|^7.0",
28-
"symfony/type-info": "^7.1"
28+
"symfony/type-info": "^7.2"
2929
},
3030
"require-dev": {
3131
"symfony/serializer": "^6.4|^7.0",

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

+25-16
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
3333
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
3434
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
35-
use Symfony\Component\TypeInfo\Exception\LogicException as TypeInfoLogicException;
3635
use Symfony\Component\TypeInfo\Type;
36+
use Symfony\Component\TypeInfo\Type\BuiltinType;
3737
use Symfony\Component\TypeInfo\Type\CollectionType;
3838
use Symfony\Component\TypeInfo\Type\IntersectionType;
39+
use Symfony\Component\TypeInfo\Type\NullableType;
3940
use Symfony\Component\TypeInfo\Type\ObjectType;
4041
use Symfony\Component\TypeInfo\Type\UnionType;
42+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
4143
use Symfony\Component\TypeInfo\TypeIdentifier;
4244

4345
/**
@@ -644,11 +646,9 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass
644646
private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed
645647
{
646648
$expectedTypes = [];
647-
$isUnionType = $type->asNonNullable() instanceof UnionType;
648649
$e = null;
649650
$extraAttributesException = null;
650651
$missingConstructorArgumentsException = null;
651-
$isNullable = false;
652652

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

670-
$t = $t->getBaseType();
670+
while ($t instanceof WrappingTypeInterface) {
671+
$t = $t->getWrappedType();
672+
}
671673

672674
// Fix a collection that contains the only one element
673675
// This is special to xml format only
674-
if ('xml' === $format && $collectionValueType && !$collectionValueType->isA(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) {
676+
if ('xml' === $format && $collectionValueType && !$collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) {
675677
$data = [$data];
676678
}
677679

@@ -694,8 +696,6 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
694696
if (TypeIdentifier::STRING === $typeIdentifier) {
695697
return '';
696698
}
697-
698-
$isNullable = $isNullable ?: $type->isNullable();
699699
}
700700

701701
switch ($typeIdentifier) {
@@ -731,26 +731,35 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
731731
}
732732

733733
if ($collectionValueType) {
734-
try {
735-
$collectionValueBaseType = $collectionValueType->getBaseType();
736-
} catch (TypeInfoLogicException) {
737-
$collectionValueBaseType = Type::mixed();
734+
$collectionValueBaseType = $collectionValueType;
735+
while ($collectionValueBaseType instanceof WrappingTypeInterface) {
736+
$collectionValueBaseType = $collectionValueBaseType->getWrappedType();
738737
}
739738

740739
if ($collectionValueBaseType instanceof ObjectType) {
741740
$typeIdentifier = TypeIdentifier::OBJECT;
742741
$class = $collectionValueBaseType->getClassName().'[]';
743742
$context['key_type'] = $collectionKeyType;
744743
$context['value_type'] = $collectionValueType;
745-
} elseif (TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) {
744+
} elseif ($collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) {
746745
// get inner type for any nested array
747746
$innerType = $collectionValueType;
747+
if ($innerType instanceof NullableType) {
748+
$innerType = $innerType->getWrappedType();
749+
}
748750

749751
// note that it will break for any other builtinType
750752
$dimensions = '[]';
751753
while ($innerType instanceof CollectionType) {
752754
$dimensions .= '[]';
753755
$innerType = $innerType->getCollectionValueType();
756+
if ($innerType instanceof NullableType) {
757+
$innerType = $innerType->getWrappedType();
758+
}
759+
}
760+
761+
while ($innerType instanceof WrappingTypeInterface) {
762+
$innerType = $innerType->getWrappedType();
754763
}
755764

756765
if ($innerType instanceof ObjectType) {
@@ -832,17 +841,17 @@ private function validateAndDenormalize(Type $type, string $currentClass, string
832841
return $data;
833842
}
834843
} catch (NotNormalizableValueException|InvalidArgumentException $e) {
835-
if (!$isUnionType && !$isNullable) {
844+
if (!$type instanceof UnionType) {
836845
throw $e;
837846
}
838847
} catch (ExtraAttributesException $e) {
839-
if (!$isUnionType && !$isNullable) {
848+
if (!$type instanceof UnionType) {
840849
throw $e;
841850
}
842851

843852
$extraAttributesException ??= $e;
844853
} catch (MissingConstructorArgumentsException $e) {
845-
if (!$isUnionType && !$isNullable) {
854+
if (!$type instanceof UnionType) {
846855
throw $e;
847856
}
848857

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

865-
if (!$isUnionType && $e) {
874+
if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) {
866875
throw $e;
867876
}
868877

src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1717
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1818
use Symfony\Component\TypeInfo\Type;
19+
use Symfony\Component\TypeInfo\Type\BuiltinType;
1920
use Symfony\Component\TypeInfo\Type\UnionType;
21+
use Symfony\Component\TypeInfo\TypeIdentifier;
2022

2123
/**
2224
* Denormalizes arrays of objects.
@@ -54,7 +56,10 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
5456
$typeIdentifiers = [];
5557
if (null !== $keyType = ($context['key_type'] ?? null)) {
5658
if ($keyType instanceof Type) {
57-
$typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]);
59+
/** @var list<BuiltinType<TypeIdentifier::INT>|BuiltinType<TypeIdentifier::STRING>> */
60+
$keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType];
61+
62+
$typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes);
5863
} else {
5964
$typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]);
6065
}

src/Symfony/Component/Serializer/composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"symfony/property-access": "^6.4|^7.0",
3939
"symfony/property-info": "^6.4|^7.0",
4040
"symfony/translation-contracts": "^2.5|^3",
41-
"symfony/type-info": "^7.1.5",
41+
"symfony/type-info": "^7.2",
4242
"symfony/uid": "^6.4|^7.0",
4343
"symfony/validator": "^6.4|^7.0",
4444
"symfony/var-dumper": "^6.4|^7.0",
@@ -51,7 +51,7 @@
5151
"symfony/dependency-injection": "<6.4",
5252
"symfony/property-access": "<6.4",
5353
"symfony/property-info": "<6.4",
54-
"symfony/type-info": "<7.1.5",
54+
"symfony/type-info": "<7.2",
5555
"symfony/uid": "<6.4",
5656
"symfony/validator": "<6.4",
5757
"symfony/yaml": "<6.4"

src/Symfony/Component/TypeInfo/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ CHANGELOG
44
7.2
55
---
66

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

916
7.1

src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php

+6-27
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,21 @@
1212
namespace Symfony\Component\TypeInfo\Tests\Type;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
1516
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
1617
use Symfony\Component\TypeInfo\Type;
1718
use Symfony\Component\TypeInfo\Type\BackedEnumType;
18-
use Symfony\Component\TypeInfo\TypeIdentifier;
1919

2020
class BackedEnumTypeTest extends TestCase
2121
{
22-
public function testToString()
23-
{
24-
$this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int()));
25-
}
26-
27-
public function testIsNullable()
28-
{
29-
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isNullable());
30-
}
31-
32-
public function testGetBaseType()
22+
public function testCannotCreateInvalidBackingBuiltinType()
3323
{
34-
$this->assertEquals(new BackedEnumType(DummyBackedEnum::class, Type::int()), (new BackedEnumType(DummyBackedEnum::class, Type::int()))->getBaseType());
24+
$this->expectException(InvalidArgumentException::class);
25+
new BackedEnumType(DummyBackedEnum::class, Type::bool());
3526
}
3627

37-
public function testAsNonNullable()
38-
{
39-
$type = new BackedEnumType(DummyBackedEnum::class, Type::int());
40-
41-
$this->assertSame($type, $type->asNonNullable());
42-
}
43-
44-
public function testIsA()
28+
public function testToString()
4529
{
46-
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::ARRAY));
47-
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::OBJECT));
48-
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(self::class));
49-
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(DummyBackedEnum::class));
50-
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(\BackedEnum::class));
51-
$this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(\UnitEnum::class));
30+
$this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int()));
5231
}
5332
}

0 commit comments

Comments
 (0)