Skip to content

Initial implementation of unsealed array shapes#5501

Draft
ondrejmirtes wants to merge 12 commits into2.2.xfrom
unsealed
Draft

Initial implementation of unsealed array shapes#5501
ondrejmirtes wants to merge 12 commits into2.2.xfrom
unsealed

Conversation

@ondrejmirtes
Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes commented Apr 21, 2026

Array shapes like array{a: int} in PHPDocs are only sealed in Bleeding Edge.

Without Bleeding edge, the goal is to match the current flawed behaviour as close as possible.

Closes phpstan/phpstan#13565
Closes phpstan/phpstan#8438
Closes phpstan/phpstan#11494
Closes phpstan/phpstan#12110
Closes phpstan/phpstan#14032

@phpstan-bot
Copy link
Copy Markdown
Collaborator

You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x.

@mnapoli
Copy link
Copy Markdown
Contributor

mnapoli commented Apr 23, 2026

Could you explain why unsealed arrays are "flawed"?
Unsealed sounds to me like the expected behavior, just like interfaces allow for implementation with extra methods.

@ondrejmirtes
Copy link
Copy Markdown
Member Author

@mnapoli There are two meanings of "flawed" I'm referring to.

  1. Allowing extra keys passed into array{a: 1} type means that foreach ($a as $v) of this type is going to lead to imprecise analysis and unsafe code. The analyser would expect only the declared values of $v but in fact any type can occur there, leading to crashing or other unexpected behaviour of your code. Also, a type like array{a: 1, b?: 2} would accept array{a: 1, c: 2}, leading to silencing errors about typos - you meant to pass b but in fact you passed c. See also unsound assignability between unsealed array shapes with overlapping optional keys. phpstan#13565.
  2. PHPStan currently treats array{a: 1} in a flawed and inconsistent way. It accepts extra keys (but not if it's an empty array{}), but count($a) only counts the exact number of declared keys and doesn't account for the possibility of extra keys being passed in.

So this PR is trying to address these problems. With bleeding edge enabled, array{a: 1} will not accept extra keys. Both sealed and unsealed variants will no longer lie about how big the array might be.

@ondrejmirtes
Copy link
Copy Markdown
Member Author

Also, a big advantage of these changes is that array shape intersections are now possible! array{a: 1}&array{b: 2} does not make sense and is not currently supported by PHPStan, but: array{a: 1, ...}&array{b: 2, ...} makes a ton of sense and will lead to an array shape with both a and b keys!

@shaedrich
Copy link
Copy Markdown

This can also be incredibly helpful with sealed arrays when they are defined elsewhere. Imagine, you have @phpstan-type in two different files and you then want to have the intersection of those in a third file, which currently is not possible, leading to duplication as you always have to implicitly define this

@mnapoli
Copy link
Copy Markdown
Contributor

mnapoli commented Apr 23, 2026

Thanks for the details, I see!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment