8000 bug #59824 [TypeInfo] Add `CollectionType::mergeCollectionValueTypes(… · symfony/symfony@1a1f81c · GitHub
[go: up one dir, main page]

Skip to content

Commit 1a1f81c

Browse files
committed
bug #59824 [TypeInfo] Add CollectionType::mergeCollectionValueTypes() (mtarld)
This PR was merged into the 7.3 branch. Discussion ---------- [TypeInfo] Add `CollectionType::mergeCollectionValueTypes()` | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | yes | Deprecations? | no | Issues | | License | MIT At the moment, when calling the following code: ```php $type = Type::fromValue([new \stdClass(), new \DateTimeImmutable()]); ``` used to throw an exception saying: `Cannot create union with both "object" and class type.`. This PR fixes that, allowing the method to return the proper `Type::list(Type::object())`. It also creates the `CollectionType::mergeCollectionValueTypes()` method to encapsulate this logic (as it'll be used in future PRs). Commits ------- b990f68 [TypeInfo] Add `CollectionType::mergeCollectionValueTypes()`
2 parents 9a918ee + b990f68 commit 1a1f81c

File tree

5 files changed

+82
-25
lines changed

5 files changed

+82
-25
lines changed

src/Symfony/Component/TypeInfo/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 22 additions & 0 deletions
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::ITERA 8000 BLE), 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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ public static function createFromValueProvider(): iterable
231231
// object
232232
yield [Type::object(\DateTimeImmutable::class), new \DateTimeImmutable()];
233233
yield [Type::object(), new \stdClass()];
234+
yield [Type::list(Type::object()), [new \stdClass(), new \DateTimeImmutable()]];
234235

235236
// collection
236237
$arrayAccess = new class implements \ArrayAccess {

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

Lines changed: 53 additions & 0 deletions
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

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -398,45 +398,25 @@ public static function fromValue(mixed $value): Type
398398
/** @var list<BuiltinType<TypeIdentifier::INT>|BuiltinType<TypeIdentifier::STRING>> $keyTypes */
399399
$keyTypes = [];
400400

401-
/** @var list<Type|null> $valueTypes */
401+
/** @var list<Type> $valueTypes */
402402
$valueTypes = [];
403403

404404
$i = 0;
405405

406406
foreach ($value as $k => $v) {
407407
$keyTypes[] = self::fromValue($k);
408-
$keyTypes = array_unique($keyTypes);
409-
410408
$valueTypes[] = self::fromValue($v);
411-
$valueTypes = array_unique($valueTypes);
412409
}
413410

414-
if ([] !== $keyTypes) {
415-
$keyTypes = array_values($keyTypes);
411+
if ($keyTypes) {
412+
$keyTypes = array_values(array_unique($keyTypes));
416413
$keyType = \count($keyTypes) > 1 ? self::union(...$keyTypes) : $keyTypes[0];
417-
418-
$valueType = null;
419-
foreach ($valueTypes as &$v) {
420-
if ($v->isIdentifiedBy(TypeIdentifier::MIXED)) {
421-
$valueType = Type::mixed();
422-
423-
break;
424-
}
425-
426-
if ($v->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE)) {
427-
$v = Type::bool();
428-
}
429-
}
430-
431-
if (!$valueType) {
432-
$valueTypes = array_values(array_unique($valueTypes));
433-
$valueType = \count($valueTypes) > 1 ? self::union(...$valueTypes) : $valueTypes[0];
434-
}
435414
} else {
436415
$keyType = Type::union(Type::int(), Type::string());
437-
$valueType = Type::mixed();
438416
}
439417

418+
$valueType = $valueTypes ? CollectionType::mergeCollectionValueTypes($valueTypes) : Type::mixed();
419+
440420
return self::collection($type, $valueType, $keyType, \is_array($value) && array_is_list($value));
441421
}
442422

0 commit comments

Comments
 (0)
0