8000 feature #59827 [TypeInfo] Add `ArrayShapeType` class (mtarld) · symfony/symfony@cf5270c · GitHub
[go: up one dir, main page]

Skip to content

Commit cf5270c

Browse files
committed
feature #59827 [TypeInfo] Add ArrayShapeType class (mtarld)
This PR was merged into the 7.3 branch. Discussion ---------- [TypeInfo] Add `ArrayShapeType` class | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | | License | MIT Introduces the `ArrayShapeType` that holds the exact shape of an array. Also enables the `StringTypeResolver` to parse `array{foo: int, bar?: string}` and to create the appropriate type. This PR needs #59824 to be merged first. Commits ------- af1231b [TypeInfo] Add `ArrayShapeType`
2 parents 6c0058a + af1231b commit cf5270c

File tree

8 files changed

+241
-3
lines changed

8 files changed

+241
-3
lines changed

src/Symfony/Component/TypeInfo/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Deprecate the third `$asList` argument of `TypeFactoryTrait::iterable()`, use `TypeFactoryTrait::list()` instead
1111
* Add type alias support in `TypeContext` and `StringTypeResolver`
1212
* Add `CollectionType::mergeCollectionValueTypes()` method
13+
* Add `ArrayShapeType` to represent the exact shape of an array
1314

1415
7.2
1516
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\TypeInfo\Tests\Type;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\TypeInfo\Type;
16+
use Symfony\Component\TypeInfo\Type\ArrayShapeType;
17+
18+
class ArrayShapeTypeTest extends TestCase
19+
{
20+
public function testGetCollectionKeyType()
21+
{
22+
$type = new ArrayShapeType([
23+
1 => ['type' => Type::bool(), 'optional' => false],
24+
]);
25+
$this->assertEquals(Type::int(), $type->getCollectionKeyType());
26+
27+
$type = new ArrayShapeType([
28+
'foo' => ['type' => Type::bool(), 'optional' => false],
29+
]);
30+
$this->assertEquals(Type::string(), $type->getCollectionKeyType());
31+
32+
$type = new ArrayShapeType([
33+
1 => ['type' => Type::bool(), 'optional' => false],
34+
'foo' => ['type' => Type::bool(), 'optional' => false],
35+
]);
36+
$this->assertEquals(Type::union(Type::int(), Type::string()), $type->getCollectionKeyType());
37+
}
38+
39+
public function testGetCollectionValueType()
40+
{
41+
$type = new ArrayShapeType([
42+
1 => ['type' => Type::bool(), 'optional' => false],
43+
]);
44+
$this->assertEquals(Type::bool(), $type->getCollectionValueType());
45+
46+
$type = new ArrayShapeType([
47+
'foo' => ['type' => Type::bool(), 'optional' => false],
48+
'bar' => ['type' => Type::int(), 'optional' => false],
49+
]);
50+
$this->assertEquals(Type::union(Type::int(), Type::bool()), $type->getCollectionValueType());
51+
52+
$type = new ArrayShapeType([
53+
'foo' => ['type' => Type::bool(), 'optional' => false],
54+
'bar' => ['type' => Type::nullable(Type::string()), 'optional' => false],
55+
]);
56+
$this->assertEquals(Type::nullable(Type::union(Type::bool(), Type::string())), $type->getCollectionValueType());
57+
58+
$type = new ArrayShapeType([
59+
'foo' => ['type' => Type::true(), 'optional' => false],
60+
'bar' => ['type' => Type::false(), 'optional' => false],
61+
]);
62+
$this->assertEquals(Type::bool(), $type->getCollectionValueType());
63+
}
64+
65+
public function testAccepts()
66+
{
67+
$type = new ArrayShapeType([
68+
'foo' => ['type' => Type::bool(), 'optional' => false],
69+
'bar' => ['type' => Type::string(), 'optional' => true],
70+
]);
71+
72+
$this->assertFalse($type->accepts('string'));
73+
$this->assertFalse($type->accepts([]));
74+
$this->assertFalse($type->accepts(['foo' => 'string']));
75+
$this->assertFalse($type->accepts(['foo' => true, 'other' => 'string']));
76+
77+
$this->assertTrue($type->accepts(['foo' => true]));
78+
$this->assertTrue($type->accepts(['foo' => true, 'bar' => 'string']));
79+
}
80+
81+
public function testToString()
82+
{
83+
$type = new ArrayShapeType([1 => ['type' => Type::bool(), 'optional' => false]]);
84+
$this->assertSame('array{1: bool}', (string) $type);
85+
86+
$type = new ArrayShapeType([
87+
2 => ['type' => Type::int(), 'optional' => true],
88+
1 => ['type' => Type::bool(), 'optional' => false],
89+
]);
90+
$this->assertSame('array{1: bool, 2?: int}', (string) $type);
91+
92+
$type = new ArrayShapeType([
93+
'foo' => ['type' => Type::bool(), 'optional' => false],
94+
'bar' => ['type' => Type::string(), 'optional' => true],
95+
]);
96+
$this->assertSame("array{'bar'?: string, 'foo': bool}", (string) $type);
97+
}
98+
}

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

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
1717
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
1818
use Symfony\Component\TypeInfo\Type;
19+
use Symfony\Component\TypeInfo\Type\ArrayShapeType;
1920
use Symfony\Component\TypeInfo\Type\BackedEnumType;
2021
use Symfony\Component\TypeInfo\Type\BuiltinType;
2122
use Symfony\Component\TypeInfo\Type\CollectionType;
@@ -205,6 +206,12 @@ public function testCreateNullable()
205206
);
206207
}
207208

209+
public function testCreateArrayShape()
210+
{
211+
$this->assertEquals(new ArrayShapeType(['foo' => ['type' => Type::bool(), 'optional' => true]]), Type::arrayShape(['foo' => ['type' => Type::bool(), 'optional' => true]]));
212+
$this->assertEquals(new ArrayShapeType(['foo' => ['type' => Type::bool(), 'optional' => false]]), Type::arrayShape(['foo' => Type::bool()]));
213+
}
214+
208215
/**
209216
* @dataProvider createFromValueProvider
210217
*/

src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public static function resolveDataProvider(): iterable
7474
yield [Type::list(Type::bool()), 'bool[]'];
7575

7676
// array shape
77-
yield [Type::array(), 'array{0: true, 1: false}'];
77+
yield [Type::arrayShape(['foo' => Type::true(), 1 => Type::false()]), 'array{foo: true, 1: false}'];
78+
yield [Type::arrayShape(['foo' => ['type' => Type::bool(), 'optional' => true]]), 'array{foo?: bool}'];
7879

7980
// object shape
8081
yield [Type::object(), 'object{foo: true, bar: false}'];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\TypeInfo\Type;
13+
14+
use Symfony\Component\TypeInfo\Type;
15+
use Symfony\Component\TypeInfo\TypeIdentifier;
16+
17+
/**
18+
* Represents the exact shape of an array.
19+
*
20+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
21+
*
22+
* @extends CollectionType<GenericType<BuiltinType<TypeIdentifier::ARRAY>>>
23+
*/
24+
final class ArrayShapeType extends CollectionType
25+
{
26+
/**
27+
* @var array<array{type: Type, optional: bool}>
28+
*/
29+
private readonly array $shape;
30+
31+
/**
32+
* @param array<array{type: Type, optional: bool}> $shape
33+
*/
34+
public function __construct(array $shape)
35+
{
36+
$keyTypes = [];
37+
$valueTypes = [];
38+
39+
foreach ($shape as $k => $v) {
40+
$keyTypes[] = self::fromValue($k);
41+
$valueTypes[] = $v['type'];
42+
}
43+
44+
if ($keyTypes) {
45+
$keyTypes = array_values(array_unique($keyTypes));
46+
$keyType = \count($keyTypes) > 1 ? self::union(...$keyTypes) : $keyTypes[0];
47+
} else {
48+
$keyType = Type::union(Type::int(), Type::string());
49+
}
50+
51+
$valueType = $valueTypes ? CollectionType::mergeCollectionValueTypes($valueTypes) : Type::mixed();
52+
53+
parent::__construct(self::generic(self::builtin(TypeIdentifier::ARRAY), $keyType, $valueType));
54+
55+
$sortedShape = $shape;
56+
ksort($sortedShape);
57+
58+
$this->shape = $sortedShape;
59+
}
60+
61+
/**
62+
* @return array<array{type: Type, optional: bool}>
63+
*/
64+
public function getShape(): array
65+
{
66+
return $this->shape;
67+
}
68+
69+
public function accepts(mixed $value): bool
70+
{
71+
if (!\is_array($value)) {
72+
return false;
73+
}
74+
75+
foreach ($this->shape as $key => $shapeValue) {
76+
if (!($shapeValue['optional'] ?? false) && !\array_key_exists($key, $value)) {
77+
return false;
78+
}
79+
}
80+
81+
foreach ($value as $key => $itemValue) {
82+
$valueType = $this->shape[$key]['type'] ?? false;
83+
if (!$valueType) {
84+
return false;
85+
}
86+
87+
if (!$valueType->accepts($itemValue)) {
88+
return false;
89+
}
90+
}
91+
92+
return true;
93+
}
94+
95+
public function __toString(): string
96+
{
97+
$items = [];
98+
99+
foreach ($this->shape as $key => $value) {
100+
$itemKey = \is_int($key) ? (string) $key : \sprintf("'%s'", $key);
101+
if ($value['optional'] ?? false) {
102+
$itemKey = \sprintf('%s?', $itemKey);
103+
}
104+
105+
$items[] = \sprintf('%s: %s', $itemKey, $value['type']);
106+
}
107+
108+
return \sprintf('array{%s}', implode(', ', $items));
109+
}
110+
}

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

+1-1
Original file line numberDiff line numberDiff 10000 line change
@@ -25,7 +25,7 @@
2525
*
2626
* @implements WrappingTypeInterface<T>
2727
*/
28-
final class CollectionType extends Type implements WrappingTypeInterface
28+
class CollectionType extends Type implements WrappingTypeInterface
2929
{
3030
/**
3131
* @param T $type

src/Symfony/Component/TypeInfo/TypeFactoryTrait.php

+13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\TypeInfo;
1313

14+
use Symfony\Component\TypeInfo\Type\ArrayShapeType;
1415
use Symfony\Component\TypeInfo\Type\BackedEnumType;
1516
use Symfony\Component\TypeInfo\Type\BuiltinType;
1617
use Symfony\Component\TypeInfo\Type\CollectionType;
@@ -194,6 +195,18 @@ public static function dict(?Type $value = null): CollectionType
194195
return self::array($value, self::string());
195196
}
196197

198+
/**
199+
* @param array<array{type: Type, optional?: bool}|Type> $shape
200+
*/
201+
public static function arrayShape(array $shape): ArrayShapeType
202+
{
203+
return new ArrayShapeType(array_map(static function (array|Type $item): array {
204+
return $item instanceof Type
205+
? ['type' => $item, 'optional' => false]
206+
: ['type' => $item['type'], 'optional' => $item['optional'] ?? false];
207+
}, $shape));
208+
}
209+
197210
/**
198211
* @template T of class-string
199212
*

src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,15 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
102102
}
103103

104104
if ($node instanceof ArrayShapeNode) {
105-
return Type::array();
105+
$shape = [];
106+
foreach ($node->items as $item) {
107+
$shape[(string) $item->keyName] = [
108+
'type' => $this->getTypeFromNode($item->valueType, $typeContext),
109+
'optional' => $item->optional,
110+
];
111+
}
112+
113+
return Type::arrayShape($shape);
106114
}
107115

108116
if ($node instanceof ObjectShapeNode) {

0 commit comments

Comments
 (0)
0