学完这篇,你能在自己的 PHP 项目里跑起 PHPStan 或 Psalm,从 0 级一路推到能真正抓 bug 的级别,并且知道哪些报错该修、哪些该进基线。
第一步:先明确它解决什么问题
静态分析不运行代码,只读代码。它能抓出这些运行时才炸的问题:调用了不存在的类/方法、参数个数和类型对不上、null 没判空就取属性、switch 漏 break、永远为真的判断、返回值类型和声明不符。
对无框架、纯 PHP 的项目(比如按传统方式写的论坛、工具站)价值尤其大:没有框架的类型约束兜底,很多错误只能靠线上日志发现。
第二步:安装,两种工具二选一
PHPStan 更轻、上手快,建议先上它:
composer require --dev phpstan/phpstan
Psalm 功能更全(能自动改代码),但配置重一些:
composer require --dev vimeo/psalm
两者都要求 PHP 7.4 以上,和多数现代 PHP 项目一致。不要同时上,先跑通一个,另一个以后再说。
注意:这两个工具是开发期依赖,一定加 --dev。别把 vendor/ 提交进仓库,也别在生产服务器上装。
第三步:第一次运行,从低级别开始
PHPStan 的级别是 0-9(0 最松,9 最严)。第一次直接跑 level 9 会因为几百条报错让人想放弃。
vendor/bin/phpstan analyse src --level=0
能跑出结果,说明环境通了。然后逐级抬:
vendor/bin/phpstan analyse src --level=5
经验节奏:0→1 无痛,2-3 开始出现"未定义变量""返回值类型不符",5 会大量报 null 安全问题,6 以上要求每个数组都写清结构。在一套代码上停在第 5、6 级是很正常的状态,不必强求 9。
如果项目脚本里用了 include 或者全局变量,加 --memory-limit=1G 避免内存超限中断。
第四步:把配置写进文件
命令行参数太啰嗦,建一个 phpstan.neon 放项目根目录:
parameters:
level: 6
paths:
- src
excludePaths:
- vendor/*
- cache/*
treatPhpDocTypesAsCertain: false
然后直接 vendor/bin/phpstan analyse 即可,不用再带参数。
几个常调的项:
bootstrapFiles:项目有全局初始化文件(定义常量、自动加载)就填进去,否则会报一堆"类不存在"
excludePaths:第三方库、生成代码、模板缓存目录必须排除
checkMissingIterableValueType 这类开关在新版本里已合并进级别,不用手动设
第五步:用基线消化历史报错
老项目第一次跑必然几百条报错,一条条修不现实。生成基线,把当前报错"冻结":
vendor/bin/phpstan analyse --generate-baseline
会得到 phpstan-baseline.neon,在 phpstan.neon 里引用:
includes:
- phpstan-baseline.neon
之后新写的代码报错照常拦,老代码的债暂时不阻塞。但基线要定期消化:每次迭代挑几条修掉,然后重新生成基线,让数字往下走。
注意:基线不是终点。如果生成后半年都不动它,等于自欺欺人——新代码只要一被基线覆盖过(比如行号变化),报错又会冒出来。
第六步:Psalm 的差异
Psalm 用交互式初始化,会问你代码目录、检查级别:
vendor/bin/psalm --init
生成 psalm.xml,级别在 errorLevel="1" 里调(注意 Psalm 是数字越小越严,和 PHPStan 相反,很多人第一次被绕晕)。
跑检查:
vendor/bin/psalm
Psalm 的杀手锏是自动修复:
vendor/bin/psalm --alter --issues=UnusedVariable,MissingReturnType
它会直接改文件。改之前必须提交一次 Git,否则改坏了没法回滚。
注意:--alter 只对一部分 issue 类型有效,不是万能的。先用 --dry-run 看看它打算改什么。
第七步:接进流程,让它持续生效
本地手动跑容易忘。两个低成本做法:
一是 composer.json 加脚本:
"scripts": {
"lint": "phpstan analyse"
}
之后 composer lint 一键跑。
二是提交前钩子,在 .git/hooks/pre-commit 里写上分析命令,报错就阻止提交。团队协作就放到 CI 里,失败即红。
第八步:常见坑
动态特性集中的代码会误报。$obj->$method()、call_user_func、魔术方法 __call、从数据库拼出来的类名,静态分析器看不穿。解决办法是写 PHPDoc 注释:
/** @var User $user */
$user = $row['obj'];
或者用 @method / @property 声明魔术成员。别一遇到误报就加 @phpstan-ignore-next-line,那等于把眼睛闭上。
超全局变量和配置文件常量。项目里到处 $config['db'] 这种,可以在 bootstrapFiles 里引入一个只含类型声明的文件,让分析器认识它们。
别在 vendor/ 上跑。几百个第三方包会拖慢速度,还刷屏。
升级 PHP 版本前先跑一遍。这是静态分析性价比最高的用法:从 7.4 升到 8.x 前,把级别调到 5 以上跑一遍,能提前发现大部分兼容性问题的来源。
小结
- PHPStan 上手快,级别 0-9 递增;Psalm 功能全,级别 1-8 递减,两者别混用
- 一律
composer require --dev 安装,配置文件 phpstan.neon / psalm.xml 进仓库
- 从低级别起步,
excludePaths 排掉第三方目录,bootstrapFiles 补上全局初始化
- 老项目用
--generate-baseline 冻结历史债,但要有计划地消化
- 富动态的代码用 PHPDoc 补类型,少用
ignore 注释一盖了之
- 最终目标是接进提交钩子或 CI,让它在每次改动时自动跑