PHPStan Level 8 逐步升级
解读
国内一线互联网公司与头部SaaS厂商在2024年的PHP技术栈面试里,普遍把“静态分析能力”作为区分初中高级的重要标尺。PHPStan Level 8 是官方最高级别,要求“零隐式 mixed、零未捕获异常、零未初始化属性”。面试官抛出“逐步升级”话题,想考察三方面:
- 你是否真在存量业务里落地过,而不是跑个 hello world;
- 对各级别核心差异是否有体系化认知,能否给出“可灰度、可回滚”的升级路径;
- 遇到历史包袱(如祖传 array、全局变量、魔术方法)时,能否权衡“改代码”与“写注解”与“调级别”的边界,而不是一股脑冲到 Level 8 导致全员加班。
因此,回答要突出“分阶段、可量化、可验证”,并给出具体命令、CI 卡点、业务无损方案。
知识点
-
PHPStan 级别定义(Level 0~8)与新增规则矩阵:
- Level 0:语法死路径、部分死方法
- Level 1:未定义变量/函数/类
- Level 2:类型收缩失败、非法数组偏移
- Level 3:方法调用签名不匹配、null 调用
- Level 4:未捕获异常、泛型数量不符
- Level 5:非通用集合迭代类型、非法标量转换
- Level 6:未初始化属性、非法标量默认
- Level 7:非法整型运算、非法字符串运算
- Level 8:一切隐式 mixed、未声明返回、未声明属性类型
-
升级四件套:
- baseline:phpstan analyse --generate-baseline 生成 phpstan-baseline.neon,把存量错误一次性“快照”,只拦新增;
- 渐进式收紧:在 CI 里用 --level max 跑全量,用 baseline 保证旧代码不炸,新代码必须干净;
- 注解策略:@phpstan-assert、@phpstan-var、@phpstan-ignore-line,优先注解,其次重构;
- 自动化补全:rector/rector 规则集 TYPE_DECLARATION 与 PHPSTAN_LEVEL_SET,可一键加返回类型、属性类型。
-
国内常见坑:
- Hyperf/Laravel 的 Facade 静态代理,PHPStan 认不出,需要 ide-helper:meta + 自定义 extension;
- 微服务用 JSON 字段当“万能数组”,Level 8 会报 mixed,要用 array shape 或 value object 封装;
- 老项目用 global $db 连接,PHPStan 判定未定义,需转单例或容器注入。
答案
“我在上家公司用 6 周完成 120 万行 PHP 代码从 Level 2 到 Level 8 的灰度升级,核心思路是‘双轨 + 三板斧’。
第一步,环境准备:
- Composer 引入 phpstan/phpstan 1.10+、rector/rector、slam/phpstan-extensions;
- 在 phpstan.neon 里先把 level 设成 2,excludePaths 把 vendor、runtime 剔掉;
- GitLab CI 新增 job:phpstan-lint,允许失败(allow_failure: true),先跑一周观察基数。
第二步,生成 baseline:
- 跑 phpstan analyse --level=2 --generate-baseline,得到 1 800 条错误快照;
- 把 phpstan-baseline.neon 纳入版本库,CI 里加 --baseline=phpstan-baseline.neon,从此只拦增量。
第三步,周级迭代:
- 每周升一级,升级顺序 2→3→4→5→6→7→8,禁止跳级,防止一次性爆炸;
- 每周一 rebase baseline,把上周修复的错误从快照里删掉,保证 baseline 只减不增;
- 需求并行:新 Pull Request 必须过当前目标级别,老文件改一行也要清掉该行触发的错误,用“童子军规则”防止回退。
第四步,三板斧落地:
- Rector 批量加类型:bin/rector process app --set=TYPE_DECLARATION,先加 scalar 类型,再加返回对象类型;
- 注解补洞:对 Hyperf 的 Redis 代理类写 .stub 文件,用 stubFiles 注入;对 JSON 字段写 /*
- @phpstan-type UserShape array{id: int, name: string}
- @phpstan-param UserShape $data */
- 单元测试兜底:Level 8 后开启 phpstan-strict-rules,任何 mixed 都报错,用 PHPUnit 覆盖 85% 以上,防止类型加错。
第五步,上线与回滚:
- 升级期间保持 PHP 7.4 运行,类型声明只做 in,不做 out,确保签名兼容;
- 每周灰度 10% 流量,Sentry 零新增异常即全量;
- 若异常上升,一键回滚 baseline 与代码到上周 tag,30 分钟内完成止血。
结果:6 周后 Level 8 零报错,baseline 从 1 800 条降到 0 条,接口平均响应时间无变化,P0 故障 0 起。面试官如果让我现场演示,我可以拿 Git 记录与 CI 日志当场复盘。”
拓展思考
-
如果公司代码量再大一个量级(500 万行)、日均发布 50 次,如何做到“不阻塞主干”?可以引入“目录级别”策略:在 phpstan.neon 用 parameters.scanDirectories 按业务域拆 10 个子配置,每个域独立 baseline,独立 Level,CI 并行跑,10 分钟以内出结果。
-
当团队里有人强烈反对加类型,认为“PHP 就是灵活”,如何用数据说服?可在升级前用 Psalm 的 --shepherd 生成“Bug 密度”徽章,升级后再跑一轮,把“潜在 NullReference 从 1 200 降到 45”贴到技术月报,用量化结果争取资源。
-
未来 PHPStan 可能把“运行时断言”也纳入静态分析,我们可以提前布局:在关键 Mapper 层写 /**
- @phpstan-assert InstanceOfFoo mixed): void,让静态与动态统一,提前适应 Level 9 的潜在规则。