8000 [TypeInfo] Add `ArrayShapeType::$extraKeyType` and `ArrayShapeType::$… · symfony/symfony@fded1eb · GitHub
[go: up one dir, main page]

Skip to content

Commit fded1eb

Browse files
mtarldfabpot
authored andcommitted
[TypeInfo] Add ArrayShapeType::$extraKeyType and ArrayShapeType::$extraValueType
1 parent d768193 commit fded1eb

File tree

6 files changed

+120
-8
lines changed

6 files changed

+120
-8
lines changed

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

+51
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,37 @@
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\Type;
1617
use Symfony\Component\TypeInfo\Type\ArrayShapeType;
1718

1819
class ArrayShapeTypeTest extends TestCase
1920
{
21+
/**
22+
* @dataProvider cannotConstructWithInvalidExtraDataProvider
23+
*/
24+
public function testCannotConstructWithInvalidExtra(string $expectedMessage, ?Type $extraKeyType, ?Type $extraValueType)
25+
{
26+
$this->expectException(InvalidArgumentException::class);
27+
$this->expectExceptionMessage($expectedMessage);
28+
29+
new ArrayShapeType(
30+
shape: [1 => ['type' => Type::bool(), 'optional' => false]],
31+
extraKeyType: $extraKeyType,
32+
extraValueType: $extraValueType,
33+
);
34+
}
35+
36+
/**
37+
* @return iterable<array{0: string, 1: ?Type, 2: ?Type}>
38+
*/
39+
public static function cannotConstructWithInvalidExtraDataProvider(): iterable
40+
{
41+
yield ['You must provide as value for "$extraValueType" when "$extraKeyType" is provided.', Type::string(), null];
42+
yield ['You must provide as value for "$extraKeyType" when "$extraValueType" is provided.', null, Type::string()];
43+
yield ['"float" is not a valid array key type.', Type::float(), Type::string()];
44+
}
45+
2046
public function testGetCollectionKeyType()
2147
{
2248
$type = new ArrayShapeType([
@@ -76,6 +102,17 @@ public function testAccepts()
76102

77103
$this->assertTrue($type->accepts(['foo' => true]));
78104
$this->assertTrue($type->accepts(['foo' => true, 'bar' => 'string']));
105+
106+
$type = new ArrayShapeType(
107+
shape: ['foo' => ['type' => Type::bool()]],
108+
extraKeyType: Type::string(),
109+
extraValueType: Type::string(),
110+
);
111+
112+
$this->assertTrue($type->accepts(['foo' => true, 'other' => 'string']));
113+
$this->assertTrue($type->accepts(['other' => 'string', 'foo' => true]));
114+
$this->assertFalse($type->accepts(['other' => 1, 'foo' => true]));
115+
$this->assertFalse($type->accepts(['other' => 'string', 'foo' => 'foo']));
79116
}
80117

81118
public function testToString()
@@ -94,5 +131,19 @@ public function testToString()
94131
'bar' => ['type' => Type::string(), 'optional' => true],
95132
]);
96133
$this->assertSame("array{'bar'?: string, 'foo': bool}", (string) $type);
134+
135+
$type = new ArrayShapeType(
136+
shape: ['foo' => ['type' => Type::bool()]],
137+
extraKeyType: Type::union(Type::int(), Type::string()),
138+
extraValueType: Type::mixed(),
139+
);
140+
$this->assertSame("array{'foo': bool, ...}", (string) $type);
141+
142+
$type = new ArrayShapeType(
143+
shape: ['foo' => ['type' => Type::bool()]],
144+
extraKeyType: Type::int(),
145+
extraValueType: Type::string(),
146+
);
147+
$this->assertSame("array{'foo': bool, ...<int, string>}", (string) $type);
97148
}
98149
}

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

+10
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ public function testCreateArrayShape()
210210
{
211211
$this->assertEquals(new ArrayShapeType(['foo' => ['type' => Type::bool(), 'optional' => true]]), Type::arrayShape(['foo' => ['type' => Type::bool(), 'optional' => true]]));
212212
$this->assertEquals(new ArrayShapeType(['foo' => ['type' => Type::bool(), 'optional' => false]]), Type::arrayShape(['foo' => Type::bool()]));
213+
$this->assertEquals(new ArrayShapeType(
214+
shape: ['foo' => ['type' => Type::bool(), 'optional' => false]],
215+
extraKeyType: Type::union(Type::int(), Type::string()),
216+
extraValueType: Type::mixed(),
217+
), Type::arrayShape(['foo' => Type::bool()], sealed: false));
218+
$this->assertEquals(new ArrayShapeType(
219+
shape: ['foo' => ['type' => Type::bool(), 'optional' => false]],
220+
extraKeyType: Type::string(),
221+
extraValueType: Type::bool(),
222+
), Type::arrayShape(['foo' => Type::bool()], extraKeyType: Type::string(), extraValueType: Type::bool()));
213223
}
214224

215225
/**

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

+3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public static function resolveDataProvider(): iterable
7676
// array shape
7777
yield [Type::arrayShape(['foo' => Type::true(), 1 => Type::false()]), 'array{foo: true, 1: false}'];
7878
yield [Type::arrayShape(['foo' => ['type' => Type::bool(), 'optional' => true]]), 'array{foo?: bool}'];
79+
yield [Type::arrayShape(['foo' => Type::bool()], sealed: false), 'array{foo: bool, ...}'];
80+
yield [Type::arrayShape(['foo' => Type::bool()], extraKeyType: Type::int(), extraValueType: Type::string()), 'array{foo: bool, ...<int, string>}'];
81+
yield [Type::arrayShape(['foo' => Type::bool()], extraValueType: Type::int()), 'array{foo: bool, ...<int>}'];
7982

8083
// object shape
8184
yield [Type::object(), 'object{foo: true, bar: false}'];

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

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

1212
namespace Symfony\Component\TypeInfo\Type;
1313

14+
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
1415
use Symfony\Component\TypeInfo\Type;
1516
use Symfony\Component\TypeInfo\TypeIdentifier;
1617

@@ -31,8 +32,11 @@ final class ArrayShapeType extends CollectionType
3132
/**
3233
* @param array<array{type: Type, optional: bool}> $shape
3334
*/
34-
public function __construct(array $shape)
35-
{
35+
public function __construct(
36+
array $shape,
37+
private readonly ?Type $extraKeyType = null,
38+
private readonly ?Type $extraValueType = null,
39+
) {
3640
$keyTypes = [];
3741
$valueTypes = [];
3842

@@ -56,6 +60,14 @@ public function __construct(array $shape)
5660
ksort($sortedShape);
5761

5862
$this->shape = $sortedShape;
63+
64+
if ($this->extraKeyType xor $this->extraValueType) {
65+
throw new InvalidArgumentException(\sprintf('You must provide a value for "$%s" when "$%s" is provided.', $this->extraKeyType ? 'extraValueType' : 'extraKeyType', $this->extraKeyType ? 'extraKeyType' : 'extraValueType'));
66+
}
67+
68+
if ($extraKeyType && !$extraKeyType->isIdentifiedBy(TypeIdentifier::INT, TypeIdentifier::STRING)) {
69+
throw new InvalidArgumentException(\sprintf('"%s" is not a valid array key type.', (string) $extraKeyType));
70+
}
5971
}
6072

6173
/**
@@ -66,6 +78,21 @@ public function getShape(): array
6678
return $this->shape;
6779
}
6880

81+
public function isSealed(): bool
82+
{
83+
return null === $this->extraValueType;
84+
}
85+
86+
public function getExtraKeyType(): ?Type
87+
{
88+
return $this->extraKeyType;
89+
}
90+
91+
public function getExtraValueType(): ?Type
92+
{
93+
return $this->extraKeyType;
94+
}
95+
6996
public function accepts(mixed $value): bool
7097
{
7198
if (!\is_array($value)) {
@@ -80,11 +107,12 @@ public function accepts(mixed $value): bool
80107

81108
foreach ($value as $key => $itemValue) {
82109
$valueType = $this->shape[$key]['type'] ?? false;
83-
if (!$valueType) {
110+
111+
if ($valueType && !$valueType->accepts($itemValue)) {
84112
return false;
85113
}
86114

87-
if (!$valueType->accepts($itemValue)) {
115+
if (!$valueType && ($this->isSealed() || !$this->extraKeyType->accepts($key) || !$this->extraValueType->accepts($itemValue))) {
88116
return false;
89117
}
90118
}
@@ -105,6 +133,12 @@ public function __toString(): string
105133
$items[] = \sprintf('%s: %s', $itemKey, $value['type']);
106134
}
107135

136+
if (!$this->isSealed()) {
137+
$items[] = $this->extraKeyType->isIdentifiedBy(TypeIdentifier::INT) && $this->extraKeyType->isIdentifiedBy(TypeIdentifier::STRING) && $this->extraValueType->isIdentifiedBy(TypeIdentifier::MIXED)
138+
? '...'
139+
: \sprintf('...<%s, %s>', $this->extraKeyType, $this->extraValueType);
140+
}
141+
108142
return \sprintf('array{%s}', implode(', ', $items));
109143
}
110144
}

src/Symfony/Component/TypeInfo/TypeFactoryTrait.php

+12-3
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,22 @@ public static function dict(?Type $value = null): CollectionType
198198
/**
199199
* @param array<array{type: Type, optional?: bool}|Type> $shape
200200
*/
201-
public static function arrayShape(array $shape): ArrayShapeType
201+
public static function arrayShape(array $shape, bool $sealed = true, ?Type $extraKeyType = null, ?Type $extraValueType = null): ArrayShapeType
202202
{
203-
return new ArrayShapeType(array_map(static function (array|Type $item): array {
203+
$shape = array_map(static function (array|Type $item): array {
204204
return $item instanceof Type
205205
? ['type' => $item, 'optional' => false]
206206
: ['type' => $item['type'], 'optional' => $item['optional'] ?? false];
207-
}, $shape));
207+
}, $shape);
208+
209+
if ($extraKeyType || $extraValueType) {
210+
$sealed = false;
211+
}
212+
213+
$extraKeyType ??= !$sealed ? Type::union(Type::int(), Type::string()) : null;
214+
$extraValueType ??= !$sealed ? Type::mixed() : null;
215+
216+
return new ArrayShapeType($shape, $extraKeyType, $extraValueType);
208217
}
209218

210219
/**

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,12 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
110110
];
111111
}
112112

113-
return Type::arrayShape($shape);
113+
return Type::arrayShape(
114+
$shape,
115+
$node->sealed,
116+
$node->unsealedType?->keyType ? $this->getTypeFromNode($node->unsealedType->keyType, $typeContext) : null,
117+
$node->unsealedType?->valueType ? $this->getTypeFromNode($node->unsealedType->valueType, $typeContext) : null,
118+
);
114119
}
115120

116121
if ($node instanceof ObjectShapeNode) {

0 commit comments

Comments
 (0)
0