← 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.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9 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.2.15PHPStan - PHP Static Analysis Tool 2.2.15 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.18.0Psalm 6.18.0@536dd6236b39a5115385b0307b42d6d1ad431a02
next: 7.0.0-beta22
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.50.0mago 1.50.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.78.0mir 0.78.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.10.0phpantom_lsp 0.10.0 Fail
arrays_shape_sealed_by_default.php:35: Argument 1 ($shape) expects array{foo: int}, got array{} (missing required key 'foo') [type_mismatch_argument]
Expectation diff
Line 36: Expected 1 error(s)
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.06.23.0Phpactor 2026.06.23.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.19274phpy 1.0.19274 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.9437.196Qodana 262.9437.196 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.2.15PHPStan - PHP Static Analysis Tool 2.2.15 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-beta22Psalm 7.0.0-beta22@de27e5724ee5997a1e3baee4d394ca6f4e46eba0 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}
}