编程 PHPStan 2.2:array{a:int} 开始真的密封,array<string,X> 的键会被收窄成 int|string

2026-10-28 00:04:44

PHPStan 2.2:未封闭数组形状与更安全的数组键

PHPStan 2.2 于 2026-05-28 发布,距离上一个 minor 版本(2.0)大约一年半,2.1.x 期间发了 55+ 个 release。这个版本主要改了两件与数组有关的事:数组形状(array shape)终于可以表达「已知键 + 额外键」,以及 array 这类标注里一直存在的键类型隐患有了显式检查和配置开关。

相关链接:

  • 官方博客:https://phpstan.org/blog/phpstan-2-2-unsealed-array-shapes-safer-array-keys
  • 配套文章:https://phpstan.org/blog/why-array-string-keys-are-not-type-safe
  • Release:https://github.com/phpstan/phpstan/releases/tag/2.2.0
  • 未封闭数组形状 PR:https://github.com/phpstan/phpstan-src/pull/5501
  • PHPDoc 类型文档:https://phpstan.org/writing-php-code/phpdoc-types
  • 仓库:https://github.com/phpstan/phpstan

同批变更还包括:新类型 decimal-int-string 与 non-decimal-int-string(#5279);新配置 reportUnsafeArrayStringKeyCasting;对参数里常量的检查(#5256、#12850),新增 error identifier argument.exclusiveConstants、argument.invalidConstant、argument.bitmaskNotAllowed;检测命名参数在子类型中被改名(#7434、#5279),新增 argument.parameterRenamedInSubtype。未封闭数组形状由 #5501、#13565、#8438、#11494、#12110、#14032 完成。

问题复现

数组键的隐式转换

PHP 里字符串键 '8' 会被存成 int 8;'08' 保持字符串,因为它不是合法十进制整数。float 键截断为 int(8.7 → 8,PHP 8.1 起 deprecated);bool 转 int;null 变空字符串 ''(PHP 8.5 起 deprecated)。

所以标注 array 时,只要键来自外部输入或变量拼接,运行时可能出现 int 键,foreach 拿到的 $key 不一定是 string。判定一个字符串是否会保持字符串键:

if ((string) (int) $x === $x) {
// $x is decimal-int-string
} else {
// $x is non-decimal-int-string
}

array shape 一直不密封

/**
* @param array{a: int} $a
*/
function doFoo(array $a): void { ... }

PHPStan 不检查是否有多余键,传 array{a: int, b: string} 也接受;甚至传普通 array,只要 isset($b['a']) 检查过就放行:

/**
* @param array $b
*/
function doBar(array $b): void {
if (isset($b['a'])) {
doFoo($b); // No errors!
}
}

后果是类型推断会给出错误结论:

function doFoo(array $a): void {
\PHPStan\dumpType(count($a)); // reports 1, but that's a lie
echo $a['b'] ?? 'foo'; // PHPStan reports the offset doesn't exist - also a lie, b may well exist at runtime
foreach ($a as $k => $v) {
\PHPStan\dumpType($k); // Reports 'a', but user might pass more keys
\PHPStan\dumpType($v); // Reports int, but should be mixed
}
}

新类型:decimal-int-string 与 non-decimal-int-string

decimal-int-string 指 PHP 视为合法十进制整数、用作数组键会被转 int 的字符串,例如 '0'、'123'、'-1'。

non-decimal-int-string 是其余字符串,例如 'hello'、'08'、'+1'、'1.5'、'00'、'18E+3'、'1,3'。这类字符串用作键永远不会被转换。

/** @param array $data */
function processData(array $data): void
{
foreach ($data as $key => $value) {
processKey($key); // 一定是 string
}
}

function doFoo(string $s)
{
$a = [];
$a[$s] = 1; // might produce an int key
// 报错原文:
// Parameter #1 $data of function processData
// expects array, non-empty-array given.
processData($a);
}

配置 reportUnsafeArrayStringKeyCasting 三档

null(默认):与今天一致,不做额外检查。

'detect':类型标注的 array 仍接受其他 array,但取键时(foreach、array_keys)键类型变为 int|non-decimal-int-string,从而暴露问题而不必改所有标注。

'prevent':array 实际按 array 处理,只接受安全键数组;任何 string 用作数组键都会被收窄为 int|non-decimal-int-string。

parameters:
reportUnsafeArrayStringKeyCasting: detect # or: prevent

官方建议先在 detect 下看影响面,再切到 prevent。两种严格模式在真实项目里都会多出几百个错误。

未封闭数组形状的语法与交叉结果

  • array{foo: int, ...}:约束额外键的键/值类型
  • array{foo: int, ...}:<> 内单个类型是值类型,键隐含为 array-key(int|string 的 benevolent union)
  • list{X, Y, ...}:list 在列出的键之后只允许单一值类型,键始终 int
  • array{foo: int, ...}:裸 ... 等价 ...

交叉运算结果:

  • array{a: int, ...}&array{b: string, ...} → array{a: int, b: string, ...}
  • array{a: int, b: string, ...}&array{a: int, ...} → array{a: int, b: string, ...}
  • array{a: int, ...}&array{a: string, ...} → never(同一已知键值类型冲突)

交叉只在至少一侧允许额外键时才有意义;array{a: int}&array{b: string} 依旧无意义、不支持。密封的数组形状被拒绝时,table formatter 会带 💡 前缀给出提示:

  • Sealed array shape can only accept a constant array. Extra keys are not allowed.
  • Sealed array shape does not accept array with extra key 'year'.
  • Sealed array shape does not accept unsealed array shape.

未封闭数组形状的工作由 Sovereign Tech Fund 资助。

bleeding edge:密封形状才真正密封

在 bleeding edge 下,sealed array shape 真正密封:array{a: int, b: string} 不接受额外键,也不接受普通数组。启用方式:

includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon

升级步骤与取舍

用 composer 把 phpstan/* 一起升到 ^2.2(phpstan-doctrine、phpstan-phpunit、phpstan-strict-rules、larastan 等扩展同步),命令:

composer update 'phpstan/*' -W

2.2 的默认行为基本兼容:未封闭形状可用、密封形状维持旧的不一致行为、键转换检查默认关闭。要提前使用严格行为才 include bleedingEdge.neon,它没有兼容承诺,minor 版本内也会变。

新增 identifier 可以按需用 ignoreErrors 的 identifier 字段精确抑制,例如 argument.bitmaskNotAllowed。

复制全文 生成海报 PHPStan PHP 静态分析 数组 类型系统

推荐文章

程序员茄子在线接单