← All results

Is array{foo: int} sealed?

Group: Arrays and shapes · Category: phpdoc · File: arrays_shape_sealed_by_default.php

A shape written without a trailing ... declares the keys it knows about and says nothing out loud about the rest, so the default has to be read off behaviour: sealed means an undeclared key is a type error, unsealed means it is admitted the way array{foo: int, ...} admits it. PHPStan and Psalm both seal by default — that is what makes ... worth spelling — and an analyzer that leaves shapes open instead accepts a payload the declaration never promised to handle. The first two calls are controls rather than the question: every analyzer that models shapes at all rejects them, so silence there means shapes are not checked, while silence on the undeclared key alone means the shape is open. References: - PHPStan array shapes - Psalm "Unsealed array and list shapes", where ... is the opt-in

Analyzer results

AnalyzerVersionResultDiagnostics
phan 6.0.10Phan 6.0.10 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 Fail
arrays_shape_sealed_by_default.php:35: Argument 1 ($shape) is [] of type array{} but \Conformance\Tests\ArraysShapeSealedByDefault\takesShape() takes array{foo:int} defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/arrays_shape_sealed_by_default.php:30 [PhanTypeMismatchArgument]
arrays_shape_sealed_by_default.php:36: Argument 1 ($shape) is ['foo'=>'one'] of type array{foo:'one'} but \Conformance\Tests\ArraysShapeSealedByDefault\takesShape() takes array{foo:int} defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/arrays_shape_sealed_by_default.php:30 [PhanTypeMismatchArgument]
Expectation diff
Line 40: Expected 1 error(s)
Line 49: Expected 1 error(s)

Notes: Shapes are open by default: `array{foo: int}` admits the undeclared `buz`, though the Phan wiki introduces the syntax as an "exact array structure". Phan also cannot parse the explicit `...` (arrays_unsealed_shape), so neither spelling seals and neither is rejected.

phpstan 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 Pass reported Lv.5+
arrays_shape_sealed_by_default.php:35: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{} given. [reported-from-level=5]
arrays_shape_sealed_by_default.php:36: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 'one'} given. [reported-from-level=5]
arrays_shape_sealed_by_default.php:40: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 2, buz: 42.0} given. [reported-from-level=5]
arrays_shape_sealed_by_default.php:49: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: int, buz: float} given. [reported-from-level=5]
With strict-rules
arrays_shape_sealed_by_default.php:35: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{} given.
arrays_shape_sealed_by_default.php:36: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 'one'} given.
arrays_shape_sealed_by_default.php:40: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 2, buz: 42.0} given.
arrays_shape_sealed_by_default.php:49: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: int, buz: float} given.
psalm 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c
next: 7.0.0-rc2
Pass
arrays_shape_sealed_by_default.php:35: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array<never, never> provided [InvalidArgument]
arrays_shape_sealed_by_default.php:36: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array{foo: 'one'} provided [InvalidArgument]
arrays_shape_sealed_by_default.php:40: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array{buz: float(42), foo: 2} with additional array shape fields (buz) was provided [InvalidArgument]
arrays_shape_sealed_by_default.php:49: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array{buz: float, foo: int} with additional array shape fields (buz) was provided [InvalidArgument]
mago 1.57.0mago 1.57.0 Pass
arrays_shape_sealed_by_default.php:35: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysShapeSealedByDefault\takesShape`: expected `array{'foo': int}`, but possibly received `array{}`. [possibly-invalid-argument]
arrays_shape_sealed_by_default.php:36: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysShapeSealedByDefault\takesShape`: expected `array{'foo': int}`, but possibly received `array{'foo': string('one')}`. [possibly-invalid-argument]
arrays_shape_sealed_by_default.php:40: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysShapeSealedByDefault\takesShape`: expected `array{'foo': int}`, but possibly received `array{'buz': float(42.0), 'foo': int(2)}`. [possibly-invalid-argument]
arrays_shape_sealed_by_default.php:49: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysShapeSealedByDefault\takesShape`: expected `array{'foo': int}`, but possibly received `array{'buz': float, 'foo': int}`. [possibly-invalid-argument]
mir 0.87.0mir 0.87.0 Fail
arrays_shape_sealed_by_default.php:35: InvalidArgument: Argument $shape of takesShape() expects 'array{'foo': int}', got 'array{}' [MIR0201]
arrays_shape_sealed_by_default.php:36: InvalidArgument: Argument $shape of takesShape() expects 'array{'foo': int}', got 'array{'foo': "one"}' [MIR0201]
Expectation diff
Line 40: Expected 1 error(s)
Line 49: Expected 1 error(s)

Notes: Shapes are open by default: the missing key and the mistyped value are caught, but the undeclared `buz` is admitted both from a literal and from a wider declared shape. mir accepts the same calls under `array{foo: int, ...}` (arrays_unsealed_shape), so the two spellings are indistinguishable to it.

phpantom 0.11.1phpantom_lsp 0.11.1 Fail
arrays_shape_sealed_by_default.php:35: Argument 1 ($shape) expects array{foo: int}, got array{} (missing required key 'foo') [type_mismatch_argument]
arrays_shape_sealed_by_default.php:36: Argument 1 ($shape) expects array{foo: int}, got array{foo: 'one'} [type_mismatch_argument]
Expectation diff
Line 40: Expected 1 error(s)
Line 49: Expected 1 error(s)
intelephense 1.18.5intelephense 1.18.5 Fail

No diagnostics reported.

Expectation diff
Line 35: Expected 1 error(s)
Line 36: Expected 1 error(s)
Line 40: Expected 1 error(s)
Line 49: Expected 1 error(s)

Notes: Checks nothing here rather than answering the question: Intelephense does not compare arguments against array shapes at all, so even the missing-key and wrong-value controls go unreported.

phpactor 2026.10.07.0Phpactor 2026.10.07.0 Fail

No diagnostics reported.

Expectation diff
Line 35: Expected 1 error(s)
Line 36: Expected 1 error(s)
Line 40: Expected 1 error(s)
Line 49: Expected 1 error(s)
phpy 1.0.19409phpy 1.0.19409 Fail

No diagnostics reported.

Expectation diff
Line 35: Expected 1 error(s)
Line 36: Expected 1 error(s)
Line 40: Expected 1 error(s)
Line 49: Expected 1 error(s)

Notes: Checks nothing here rather than answering the question: phpy does not compare arguments against array shapes at all, so even the missing-key and wrong-value controls go unreported.

qodana 262.10968.76PhpStorm 262.10968.76 Fail
arrays_shape_sealed_by_default.php:35: Incomplete array according to shape declaration. Missing key(s): foo [PhpMissingArrayKeyInspection]
arrays_shape_sealed_by_default.php:36: Expected parameter of type 'int', 'string' provided [PhpParamsInspection]
arrays_shape_sealed_by_default.php:40: Value should be one of: "foo" [PhpArrayKeyDoesNotMatchArrayShapeInspection]
Expectation diff
Line 49: Expected 1 error(s)

Notes: Seals the shape against the literal at line 40 but not against a value whose extra key comes from a declaration: `array{foo: int, buz: float}` passes through at line 49. Same split as steins.

noverify 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 Fail
arrays_shape_sealed_by_default.php:30: param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]
arrays_shape_sealed_by_default.php:45: param $wider miss matched with phpdoc type <<array{foo: int, buz: float}>> [funcParamTypeMissMatch]
Expectation diff
Line 35: Expected 1 error(s)
Line 36: Expected 1 error(s)
Line 40: Expected 1 error(s)
Line 49: Expected 1 error(s)
Line 30: Unexpected errors ["param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]"]
Line 45: Unexpected errors ["param $wider miss matched with phpdoc type <<array{foo: int, buz: float}>> [funcParamTypeMissMatch]"]

Notes: Does not model array shapes at all. It rejects the shape spelling itself on both declaration lines (30 and 45) and checks none of the call sites, so it has no default to read off.

steins 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) Pass
arrays_shape_sealed_by_default.php:35: argument [] to takesShape() violates declared @param array{foo: int} $shape — declared contract violation [phpdoc.param-mismatch]
arrays_shape_sealed_by_default.php:36: argument ['foo' => 'one'] to takesShape() violates declared @param array{foo: int} $shape — declared contract violation [phpdoc.param-mismatch]
arrays_shape_sealed_by_default.php:40: argument ['foo' => 2, 'buz' => 42.0] to takesShape() violates declared @param array{foo: int} $shape — declared contract violation [phpdoc.param-mismatch]
arrays_shape_sealed_by_default.php:49: argument a value of type array{buz: float, foo: int} to takesShape() violates declared @param array{foo: int} $shape — declared contract violation [phpdoc.param-mismatch]

Notes: Seals the shape against the literal at line 40 but not against a value whose extra key comes from a declaration: `array{foo: int, buz: float}` passes through at line 49.

phpstan-strict 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 Pass
arrays_shape_sealed_by_default.php:35: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{} given.
arrays_shape_sealed_by_default.php:36: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 'one'} given.
arrays_shape_sealed_by_default.php:40: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 2, buz: 42.0} given.
arrays_shape_sealed_by_default.php:49: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: int, buz: float} given.
psalm-next 7.0.0-rc2Psalm 7.0.0-rc2@20339e3fe5b8d03bcd8061030eb2a83ad50e5783 Pass
arrays_shape_sealed_by_default.php:35: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array<never, never> provided [InvalidArgument]
arrays_shape_sealed_by_default.php:36: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array{foo: 'one'} provided [InvalidArgument]
arrays_shape_sealed_by_default.php:40: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array{buz: float(42), foo: 2} with additional array shape fields (buz) was provided [InvalidArgument]
arrays_shape_sealed_by_default.php:49: Argument 1 of Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, but array{buz: float, foo: int} with additional array shape fields (buz) was provided [InvalidArgument]

Source

<?php

declare(strict_types=1);

namespace Conformance\Tests\ArraysShapeSealedByDefault;

/**
 * Is `array{foo: int}` sealed?
 *
 * A shape written without a trailing `...` declares the keys it knows about
 * and says nothing out loud about the rest, so the default has to be read off
 * behaviour: sealed means an undeclared key is a type error, unsealed means it
 * is admitted the way `array{foo: int, ...}` admits it. PHPStan and Psalm both
 * seal by default — that is what makes `...` worth spelling — and an analyzer
 * that leaves shapes open instead accepts a payload the declaration never
 * promised to handle.
 *
 * The first two calls are controls rather than the question: every analyzer
 * that models shapes at all rejects them, so silence there means shapes are not
 * checked, while silence on the undeclared key alone means the shape is open.
 *
 * References:
 * - PHPStan array shapes
 * - Psalm "Unsealed array and list shapes", where `...` is the opt-in
 */

/**
 * @param array{foo: int} $shape
 */
function takesShape(array $shape): void
{
}

// Controls: the declared key is required, and it is an int.
takesShape([]); // E: array{} does not have the required key foo
takesShape(['foo' => 'one']); // E: foo is int, not string

// The question, asked of a literal.
takesShape(['foo' => 1]);
takesShape(['foo' => 2, 'buz' => 42.0]); // E: a sealed array{foo: int} does not admit the undeclared key buz

/**
 * @param array{foo: int, buz: float} $wider
 */
function passesWiderShapeAlong(array $wider): void
{
    // And of a value whose extra key comes from a declaration rather than from
    // a literal at the call site.
    takesShape($wider); // E: array{foo: int, buz: float} is not a subtype of a sealed array{foo: int}
}