Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 452ad95

Browse filesBrowse files
committed
feature #59981 [TypeInfo] Add ArrayShapeType::$sealed (mtarld)
This PR was merged into the 7.3 branch. Discussion ---------- [TypeInfo] Add `ArrayShapeType::$sealed` | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | | License | MIT Implement sealed syntax for array shape type, as suggested in #59827 (comment). Commits ------- fded1eb [TypeInfo] Add `ArrayShapeType::$extraKeyType` and `ArrayShapeType::$extraValueType`
2 parents f803dc1 + fded1eb commit 452ad95
Copy full SHA for 452ad95

File tree

6 files changed

+120
-8
lines changed
Filter options

6 files changed

+120
-8
lines changed

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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.