Skip to content

Commit 2bf88ad

Browse files
committed
[TypeInfo] Add CollectionType::mergeCollectionValueTypes()
1 parent 68944f5 commit 2bf88ad

File tree

5 files changed

+78
-18
lines changed

5 files changed

+78
-18
lines changed

src/Symfony/Component/TypeInfo/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Deprecate constructing a `CollectionType` instance as a list that is not an array
1010
* Deprecate the third `$asList` argument of `TypeFactoryTrait::iterable()`, use `TypeFactoryTrait::list()` instead
1111
* Add type alias support in `TypeContext` and `StringTypeResolver`
12+
* Add `CollectionType::mergeCollectionValueTypes()` method
1213

1314
7.2
1415
---

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

+22
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,26 @@ public function testCannotCreateIterableList()
134134
$this->expectUserDeprecationMessage('Since symfony/type-info 7.3: Creating a "Symfony\Component\TypeInfo\Type\CollectionType" that is a list and not an array is deprecated and will throw a "Symfony\Component\TypeInfo\Exception\InvalidArgumentException" in 8.0.');
135135
new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ITERABLE), Type::bool()), isList: true);
136136
}
137+
138+
public function testMergeCollectionValueTypes()
139+
{
140+
$this->assertEquals(Type::int(), CollectionType::mergeCollectionValueTypes([Type::int()]));
141+
$this->assertEquals(Type::union(Type::int(), Type::string()), CollectionType::mergeCollectionValueTypes([Type::int(), Type::string()]));
142+
143+
$this->assertEquals(Type::mixed(), CollectionType::mergeCollectionValueTypes([Type::int(), Type::mixed()]));
144+
145+
$this->assertEquals(Type::union(Type::int(), Type::true()), CollectionType::mergeCollectionValueTypes([Type::int(), Type::true()]));
146+
$this->assertEquals(Type::bool(), CollectionType::mergeCollectionValueTypes([Type::true(), Type::false(), Type::true()]));
147+
148+
$this->assertEquals(Type::union(Type::object(\Stringable::class), Type::object(\Countable::class)), CollectionType::mergeCollectionValueTypes([Type::object(\Stringable::class), Type::object(\Countable::class)]));
149+
$this->assertEquals(Type::object(), CollectionType::mergeCollectionValueTypes([Type::object(\Stringable::class), Type::object()]));
150+
}
151+
152+
public function testCannotMergeEmptyCollectionValueTypes()
153+
{
154+
$this->expectException(InvalidArgumentException::class);
155+
$this->expectExceptionMessage('The $types cannot be empty.');
156+
157+
CollectionType::mergeCollectionValueTypes([]);
158+
}
137159
}

src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ public static function createFromValueProvider(): iterable
227227
// object
228228
yield [Type::object(\DateTimeImmutable::class), new \DateTimeImmutable()];
229229
yield [Type::object(), new \stdClass()];
230+
yield [Type::list(Type::object()), [new \stdClass(), new \DateTimeImmutable()]];
230231

231232
// collection
232233
$arrayAccess = new class implements \ArrayAccess {

src/Symfony/Component/TypeInfo/Type/CollectionType.php

+53
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,59 @@ public function __construct(
5252
}
5353
}
5454

55+
/**
56+
* @param array<Type> $types
57+
*/
58+
public static function mergeCollectionValueTypes(array $types): Type
59+
{
60+
if (!$types) {
61+
throw new InvalidArgumentException('The $types cannot be empty.');
62+
}
63+
64+
$normalizedTypes = [];
65+
$boolTypes = [];
66+
$objectTypes = [];
67+
68+
foreach ($types as $t) {
69+
// cannot create an union with a standalone type
70+
if ($t->isIdentifiedBy(TypeIdentifier::MIXED)) {
71+
return Type::mixed();
72+
}
73+
74+
if ($t->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE, TypeIdentifier::BOOL)) {
75+
$boolTypes[] = $t;
76+
77+
continue;
78+
}
79+
80+
if ($t->isIdentifiedBy(TypeIdentifier::OBJECT)) {
81+
$objectTypes[] = $t;
82+
83+
continue;
84+
}
85+
86+
$normalizedTypes[] = $t;
87+
}
88+
89+
$boolTypes = array_unique($boolTypes);
90+
$objectTypes = array_unique($objectTypes);
91+
92+
// cannot create an union with either "true" and "false", "bool" must be used instead
93+
if ($boolTypes) {
94+
$normalizedTypes[] = \count($boolTypes) > 1 ? Type::bool() : $boolTypes[0];
95+
}
96+
97+
// cannot create a union with either "object" and a class name, "object" must be used instead
98+
if ($objectTypes) {
99+
$hasBuiltinObjectType = array_filter($objectTypes, static fn (Type $t): bool => $t->isSatisfiedBy(static fn (Type $t): bool => $t instanceof BuiltinType));
100+
$normalizedTypes = [...$normalizedTypes, ...($hasBuiltinObjectType ? [Type::object()] : $objectTypes)];
101+
}
102+
103+
$normalizedTypes = array_values(array_unique($normalizedTypes));
104+
105+
return \count($normalizedTypes) > 1 ? self::union(...$normalizedTypes) : $normalizedTypes[0];
106+
}
107+
55108
public function getWrappedType(): Type
56109
{
57110
return $this->type;

src/Symfony/Component/TypeInfo/TypeFactoryTrait.php

+1-18
Original file line numberDiff line numberDiff line change
@@ -407,24 +407,7 @@ public static function fromValue(mixed $value): Type
407407
if ([] !== $keyTypes) {
408408
$keyTypes = array_values($keyTypes);
409409
$keyType = \count($keyTypes) > 1 ? self::union(...$keyTypes) : $keyTypes[0];
410-
411-
$valueType = null;
412-
foreach ($valueTypes as &$v) {
413-
if ($v->isIdentifiedBy(TypeIdentifier::MIXED)) {
414-
$valueType = Type::mixed();
415-
416-
break;
417-
}
418-
419-
if ($v->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE)) {
420-
$v = Type::bool();
421-
}
422-
}
423-
424-
if (!$valueType) {
425-
$valueTypes = array_values(array_unique($valueTypes));
426-
$valueType = \count($valueTypes) > 1 ? self::union(...$valueTypes) : $valueTypes[0];
427-
}
410+
$valueType = CollectionType::mergeCollectionValueTypes($valueTypes);
428411
} else {
429412
$keyType = Type::union(Type::int(), Type::string());
430413
$valueType = Type::mixed();

0 commit comments

Comments
 (0)