diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/ArrayShapeTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/ArrayShapeTypeTest.php index e54c832afd2e0..d68b7bcfe544b 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/ArrayShapeTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/ArrayShapeTypeTest.php @@ -12,11 +12,37 @@ namespace Symfony\Component\TypeInfo\Tests\Type; use PHPUnit\Framework\TestCase; +use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\ArrayShapeType; class ArrayShapeTypeTest extends TestCase { + /** + * @dataProvider cannotConstructWithInvalidExtraDataProvider + */ + public function testCannotConstructWithInvalidExtra(string $expectedMessage, ?Type $extraKeyType, ?Type $extraValueType) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); + + new ArrayShapeType( + shape: [1 => ['type' => Type::bool(), 'optional' => false]], + extraKeyType: $extraKeyType, + extraValueType: $extraValueType, + ); + } + + /** + * @return iterable + */ + public static function cannotConstructWithInvalidExtraDataProvider(): iterable + { + yield ['You must provide as value for "$extraValueType" when "$extraKeyType" is provided.', Type::string(), null]; + yield ['You must provide as value for "$extraKeyType" when "$extraValueType" is provided.', null, Type::string()]; + yield ['"float" is not a valid array key type.', Type::float(), Type::string()]; + } + public function testGetCollectionKeyType() { $type = new ArrayShapeType([ @@ -76,6 +102,17 @@ public function testAccepts() $this->assertTrue($type->accepts(['foo' => true])); $this->assertTrue($type->accepts(['foo' => true, 'bar' => 'string'])); + + $type = new ArrayShapeType( + shape: ['foo' => ['type' => Type::bool()]], + extraKeyType: Type::string(), + extraValueType: Type::string(), + ); + + $this->assertTrue($type->accepts(['foo' => true, 'other' => 'string'])); + $this->assertTrue($type->accepts(['other' => 'string', 'foo' => true])); + $this->assertFalse($type->accepts(['other' => 1, 'foo' => true])); + $this->assertFalse($type->accepts(['other' => 'string', 'foo' => 'foo'])); } public function testToString() @@ -94,5 +131,19 @@ public function testToString() 'bar' => ['type' => Type::string(), 'optional' => true], ]); $this->assertSame("array{'bar'?: string, 'foo': bool}", (string) $type); + + $type = new ArrayShapeType( + shape: ['foo' => ['type' => Type::bool()]], + extraKeyType: Type::union(Type::int(), Type::string()), + extraValueType: Type::mixed(), + ); + $this->assertSame("array{'foo': bool, ...}", (string) $type); + + $type = new ArrayShapeType( + shape: ['foo' => ['type' => Type::bool()]], + extraKeyType: Type::int(), + extraValueType: Type::string(), + ); + $this->assertSame("array{'foo': bool, ...}", (string) $type); } } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php index 7f5520cc7d01a..65a33739bf0fb 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php @@ -210,6 +210,16 @@ public function testCreateArrayShape() { $this->assertEquals(new ArrayShapeType(['foo' => ['type' => Type::bool(), 'optional' => true]]), Type::arrayShape(['foo' => ['type' => Type::bool(), 'optional' => true]])); $this->assertEquals(new ArrayShapeType(['foo' => ['type' => Type::bool(), 'optional' => false]]), Type::arrayShape(['foo' => Type::bool()])); + $this->assertEquals(new ArrayShapeType( + shape: ['foo' => ['type' => Type::bool(), 'optional' => false]], + extraKeyType: Type::union(Type::int(), Type::string()), + extraValueType: Type::mixed(), + ), Type::arrayShape(['foo' => Type::bool()], sealed: false)); + $this->assertEquals(new ArrayShapeType( + shape: ['foo' => ['type' => Type::bool(), 'optional' => false]], + extraKeyType: Type::string(), + extraValueType: Type::bool(), + ), Type::arrayShape(['foo' => Type::bool()], extraKeyType: Type::string(), extraValueType: Type::bool())); } /** diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index b2db7660f9026..21abd8d72c283 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -76,6 +76,9 @@ public static function resolveDataProvider(): iterable // array shape yield [Type::arrayShape(['foo' => Type::true(), 1 => Type::false()]), 'array{foo: true, 1: false}']; yield [Type::arrayShape(['foo' => ['type' => Type::bool(), 'optional' => true]]), 'array{foo?: bool}']; + yield [Type::arrayShape(['foo' => Type::bool()], sealed: false), 'array{foo: bool, ...}']; + yield [Type::arrayShape(['foo' => Type::bool()], extraKeyType: Type::int(), extraValueType: Type::string()), 'array{foo: bool, ...}']; + yield [Type::arrayShape(['foo' => Type::bool()], extraValueType: Type::int()), 'array{foo: bool, ...}']; // object shape yield [Type::object(), 'object{foo: true, bar: false}']; diff --git a/src/Symfony/Component/TypeInfo/Type/ArrayShapeType.php b/src/Symfony/Component/TypeInfo/Type/ArrayShapeType.php index 2c3819cc56dfd..504a59ac619ba 100644 --- a/src/Symfony/Component/TypeInfo/Type/ArrayShapeType.php +++ b/src/Symfony/Component/TypeInfo/Type/ArrayShapeType.php @@ -11,6 +11,7 @@ namespace Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeIdentifier; @@ -31,8 +32,11 @@ final class ArrayShapeType extends CollectionType /** * @param array $shape */ - public function __construct(array $shape) - { + public function __construct( + array $shape, + private readonly ?Type $extraKeyType = null, + private readonly ?Type $extraValueType = null, + ) { $keyTypes = []; $valueTypes = []; @@ -56,6 +60,14 @@ public function __construct(array $shape) ksort($sortedShape); $this->shape = $sortedShape; + + if ($this->extraKeyType xor $this->extraValueType) { + throw new InvalidArgumentException(\sprintf('You must provide a value for "$%s" when "$%s" is provided.', $this->extraKeyType ? 'extraValueType' : 'extraKeyType', $this->extraKeyType ? 'extraKeyType' : 'extraValueType')); + } + + if ($extraKeyType && !$extraKeyType->isIdentifiedBy(TypeIdentifier::INT, TypeIdentifier::STRING)) { + throw new InvalidArgumentException(\sprintf('"%s" is not a valid array key type.', (string) $extraKeyType)); + } } /** @@ -66,6 +78,21 @@ public function getShape(): array return $this->shape; } + public function isSealed(): bool + { + return null === $this->extraValueType; + } + + public function getExtraKeyType(): ?Type + { + return $this->extraKeyType; + } + + public function getExtraValueType(): ?Type + { + return $this->extraKeyType; + } + public function accepts(mixed $value): bool { if (!\is_array($value)) { @@ -80,11 +107,12 @@ public function accepts(mixed $value): bool foreach ($value as $key => $itemValue) { $valueType = $this->shape[$key]['type'] ?? false; - if (!$valueType) { + + if ($valueType && !$valueType->accepts($itemValue)) { return false; } - if (!$valueType->accepts($itemValue)) { + if (!$valueType && ($this->isSealed() || !$this->extraKeyType->accepts($key) || !$this->extraValueType->accepts($itemValue))) { return false; } } @@ -105,6 +133,12 @@ public function __toString(): string $items[] = \sprintf('%s: %s', $itemKey, $value['type']); } + if (!$this->isSealed()) { + $items[] = $this->extraKeyType->isIdentifiedBy(TypeIdentifier::INT) && $this->extraKeyType->isIdentifiedBy(TypeIdentifier::STRING) && $this->extraValueType->isIdentifiedBy(TypeIdentifier::MIXED) + ? '...' + : \sprintf('...<%s, %s>', $this->extraKeyType, $this->extraValueType); + } + return \sprintf('array{%s}', implode(', ', $items)); } } diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index 4fe2c7beb1609..125b3702016fb 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -198,13 +198,22 @@ public static function dict(?Type $value = null): CollectionType /** * @param array $shape */ - public static function arrayShape(array $shape): ArrayShapeType + public static function arrayShape(array $shape, bool $sealed = true, ?Type $extraKeyType = null, ?Type $extraValueType = null): ArrayShapeType { - return new ArrayShapeType(array_map(static function (array|Type $item): array { + $shape = array_map(static function (array|Type $item): array { return $item instanceof Type ? ['type' => $item, 'optional' => false] : ['type' => $item['type'], 'optional' => $item['optional'] ?? false]; - }, $shape)); + }, $shape); + + if ($extraKeyType || $extraValueType) { + $sealed = false; + } + + $extraKeyType ??= !$sealed ? Type::union(Type::int(), Type::string()) : null; + $extraValueType ??= !$sealed ? Type::mixed() : null; + + return new ArrayShapeType($shape, $extraKeyType, $extraValueType); } /** diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index ff31b711389e6..244563f602f7d 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -110,7 +110,12 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ ]; } - return Type::arrayShape($shape); + return Type::arrayShape( + $shape, + $node->sealed, + $node->unsealedType?->keyType ? $this->getTypeFromNode($node->unsealedType->keyType, $typeContext) : null, + $node->unsealedType?->valueType ? $this->getTypeFromNode($node->unsealedType->valueType, $typeContext) : null, + ); } if ($node instanceof ObjectShapeNode) { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy