← 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.8PHPStan - PHP Static Analysis Tool 2.2.8 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. [identifier=argument.type] [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. [identifier=argument.type] [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. [identifier=argument.type] [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. [identifier=argument.type] [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. [identifier=argument.type]
arrays_shape_sealed_by_default.php:36: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 'one'} given. [identifier=argument.type]
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. [identifier=argument.type]
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. [identifier=argument.type]
psalm 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
next: 7.0.0-beta19
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.46.0mago 1.46.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.70.1mir 0.70.1 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.9.0phpantom_lsp 0.9.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)
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.

phpy 0.2.0phpy 0.2.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)

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.8665.325Qodana 262.8665.325 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.5steins 0.1.5 (2026-08-12 revision 39b4cc1) 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: mixed, 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.8PHPStan - PHP Static Analysis Tool 2.2.8 Pass
arrays_shape_sealed_by_default.php:35: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{} given. [identifier=argument.type]
arrays_shape_sealed_by_default.php:36: Parameter #1 $shape of function Conformance\Tests\ArraysShapeSealedByDefault\takesShape expects array{foo: int}, array{foo: 'one'} given. [identifier=argument.type]
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. [identifier=argument.type]
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. [identifier=argument.type]
psalm-next 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 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}
}