10000 feature #59827 [TypeInfo] Add `ArrayShapeType` class (mtarld) · symfony/type-info@e8b25e8 · GitHub
[go: up one dir, main page]

Skip to content

Commit e8b25e8

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 symfony/symfony#59824 to be merged first. Commits ------- af1231b0dd [TypeInfo] Add `ArrayShapeType`
2 parents fe1581c + 7064aae commit e8b25e8

File tree

8 files changed

+241
-3
lines changed
  • TypeResolver
  • Type
  • 8 files changed

    +241
    -3
    lines changed

    CHANGELOG.md

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

    Tests/Type/ArrayShapeTypeTest.php

    Lines changed: 98 additions & 0 deletions
    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+
    }

    Tests/TypeFactoryTest.php

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

    Tests/TypeResolver/StringTypeResolverTest.php

    Lines changed: 2 additions & 1 deletion
    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}'];

    Type/ArrayShapeType.php

    Lines changed: 110 additions & 0 deletions
    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+
    }

    Type/CollectionType.php

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff 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

    TypeFactoryTrait.php

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

    TypeResolver/StringTypeResolver.php

    Lines changed: 9 additions & 1 deletion
    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