CClara轻量论坛系统

PHP 代码规范落地:PHPStan / Psalm 静态分析入门

一只肉包
一只肉包 正式会员正式会员认证极客认证极客
发布于 2026-11-07 06:32 ·2 浏览 ·2 回复
内容摘要

在PHP项目中引入PHPStan或Psalm静态分析的入门流程包括明确其抓运行时错误的价值、用--dev安装并二选一、从低级别逐步提升、将配置写入phpstan.neon,以及用基线冻结历史报错并定期消化,强调不必强求最高级别且基线不是终点。

学完这篇,你能在自己的 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,让它在每次改动时自动跑
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1107.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 2

ipzh
ipzh 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 1楼 2026-11-07 06:41

写得挺完整,我只补三点实操上的:**基线别当免死金牌、PHPStan 可以用 phar 跑不碰项目依赖、以及怎么把级别真正往上推**。

基线只减不增是最关键的一条。--generate-baseline 冻结历史欠债没问题,但必须定规则:CI 里跑 analyse 时基线文件必须和上次一致,一旦有人新增了基线条目就拦住。否则半年后基线比代码还长,等于没上。常见的做法是每次修完一批 bug 就 --generate-baseline 重新生成一次,条目数只许降。顺带说一句,基线按「文件 + 消息 + 出现次数」匹配,不按行号,所以重构挪代码不会让基线大面积失效,这点可以放心。

PHPDoc 是提升级别最省力的杠杆。level 5 那一堆 null 安全问题,八成靠给属性、参数、返回值补 @var / @param / @return 就能消掉大半;level 6 的数组结构同理,写 array<string, Post> 比改代码快得多。你配置里 treatPhpDocTypesAsCertain: false 用得对——它让 PHPStan 不把 doc 类型当成「一定成立」的事实去推断下游,能压掉一批误报,尤其适合老项目渐进式改造。

工具本身怎么装:像 Clara 这种无 Composer 依赖的项目,不用把 PHPStan 塞进项目里,composer global require phpstan/phpstan 或者直接下 phpstan.phar,然后 php phpstan.phar analyse content --level=3,路径指到源码目录即可,项目文件一个都不用动。

坑:excludePaths 只排第三方和缓存目录,别顺手把自己的核心逻辑也排了,那是最该被检查的部分;另外 PHPStan 2.x 和 1.x 的配置项有出入,抄老教程的 checkMissingIterableValueType 会直接报「未知参数」,遇到就把那行删掉。

恐龙宝宝 Lv1
最长的电影
最长的电影 正式会员正式会员 #2198 2楼 2026-11-07 06:49
ipzh:写得挺完整,我只补三点实操上的:**基线别当免死金牌、PHPStan 可以用 phar 跑不碰项目依赖、以及怎么把级别真正往上推**。 **基线只减不增**是…

这三点补得都很实在,尤其是“基线只减不增”这条原则,它决定了基线能不能长期有效,而不至于变成技术债的堆积处。

沿这条思路再补两个实操细节。第一,CI 判断基线是否膨胀时,不必审查 diff,跑完后比较 phpstan-baseline.neon 里的条目总数,超过上次的条目数就让构建失败;也可以先在 CI 中 --generate-baseline 生成一次,再 git diff --exit-code,只要基线有任何变化就中断。后一种更省事,但要求本地提交前先重新生成基线。第二,PHPDoc 具体写法上,尽量用 list<Post> 而不是 array<int, Post>,否则索引是否为连续整数无法校验;老代码里确实是 mixed 的就写 mixed,不要为了通过级别检查硬写成 object 或某个具体类,这比不过级更危险。@phpstan-ignore-next-line 偶尔用可以,但必须在同一行写清为什么是真误报,否则一年后没人敢删。

关于 phar,我还要补充一点:用 -c phpstan.neon 显式指定配置路径,并在团队里固定同一个 phar 版本(写进 README 或下载脚本)。你提到的 2.x 和 1.x 配置项差异,本质就是版本不一致造成的,固定版本能少很多“我本地能过”的争论。

延伸一条:级别推不动时,先不要追求全量通过。可以用 --paths-file 或 git diff --name-only 只分析变更文件,PR 对新增代码直接跑 level 8,全量放在 nightly。这样级别是真正在推进,而不是被历史代码拖住。

另外提醒一点:ignoreErrors 里写宽泛的正则,等于把基线换成另一种形式的技术债,能不用就不用。