CClara轻量论坛系统

PHP 编码规范与代码评审:团队协作最佳实践

laoxie
laoxie 正式会员正式会员认证极客认证极客
发布于 2026-10-29 12:34 ·1 浏览 ·5 回复
内容摘要

PHP 团队协作可通过以 PSR-1/4/12 为基线并补充一页团队条款来制定编码规范,借助 phpcs、phpstan 等在开发机与 CI 中自动检查风格和静态问题,同时要求 PR 小而单一目的、描述模板化、CI 通过后再评审,人工评审只关注逻辑边界、安全、性能、可读性与可测试性,并用 must/should/nit 分级评论明确轻重。

学完这篇,你能为团队搭起一套"规范可执行、评审有重点"的 PHP 协作流程——不再靠口头约定和人情面子维持代码质量。

第一步:规范先定基线,别定洁癖

不要一上来抄几百页大厂手册,团队没人看得完。做法是三层:

  1. 基线直接用 PSR 系列:PSR-1(基础编码标准)、PSR-4(自动加载)、PSR-12(扩展编码风格)。这三份是公开的、有工具支持的,直接声明"本项目遵循 PSR-12"就够了。
  2. 团队补充条款写成一页纸,只写 PSR 没覆盖的:目录约定、类名后缀、注释语言、常量定义位置、异常处理口径。
  3. 无框架项目额外补目录约定。像 Clara BBS 这类无框架纯 PHP 系统,靠的是目录结构 + 单一入口维持秩序,所以规范里要写清楚"哪个目录放控制器、哪个放模型、插件代码放哪"。

注意:规范里凡是能用工具查的(缩进、括号、行宽),就不要写进文档靠人记——写进文档的应该是工具查不出来的东西(命名语义、分层职责)。

第二步:把规范变成机器能查的东西

文档靠自觉,工具靠强制。在开发机和 CI 上装这几个:

composer global require squizlabs/php_codesniffer
composer global require phpstan/phpstan

# 查风格
phpcs --standard=PSR12 app/

phpcbf --standard=PSR12 app/
# 静态分析,等级从 5 起步,别一上来就 9
phpstan analyse app/ --level=5

再补一个 .editorconfig 统一换行符和缩进,避免 Windows/Linux 混开时整文件 diff。

注意:第一次接入 phpcbf 时,单独提一个"纯格式化"提交,不要和业务改动混在一起。否则代码评审时 3000 行 diff 里全是空格变化,真正的逻辑改动根本看不见。

注意:像 Clara BBS 这样无需 Composer 运行的项目,把 phpcs/phpstan 只装在开发机与 CI,当成开发期检查工具,不要引入运行时依赖,也就不影响生产部署。

第三步:让 PR 自己说话

评审效率低,八成是提交本身没做好准备:

  • 控制体积:单个 PR 尽量 400 行以内、单一目的。改 bug 就别顺手重构。
  • 描述模板固定三问:改了什么 / 为什么改 / 怎么验证。有复现步骤就贴。
  • CI 必须先绿:phpcs、phpstan、单元测试三道关,没过不进人工评审。
  • 作者先自审 diff:自己看一遍,能删掉调试代码、注释掉的旧逻辑。

第四步:评审时只看工具看不见的部分

机器能查的(风格、语法、明显未定义变量)交给工具,人只看这些:

  1. 逻辑正确性与边界:空数组、零值、null、超大输入、并发写入。
  2. 安全:SQL 是否参数化、输出是否转义、表单是否校验 CSRF token、密码是否用 password_hash(bcrypt)、上传是否走白名单 + 类型二次校验。
  3. 性能:循环里查库(N+1)、缺索引的 WHERE、无上限的查询。
  4. 可读性:函数是否只做一件事、命名能否自解释。
  5. 可测试性:耦合是否过死,硬编码路径/时间/随机数是否抽出来。

评论要分级,别让作者猜轻重:

  • must: 必须改,阻塞合并
  • should: 建议改,可后续跟进
  • nit: 吹毛求疵,改不改都行

注意:评审不是重写。把别人的实现改成"你更喜欢的写法"是最伤士气的行为——除非原写法确实有正确性、安全性或明显可维护性问题,否则放行。

注意:用提问代替命令。"这里如果 $id 是字符串会怎样?"比"这里必须加 intval"更容易被接受,也更容易发现作者其实已经处理过。

第五步:评审后要闭环

  • 同一处讨论超过三轮就直接拉语音或当面说,文字来回成本太高。
  • 每条被反复提出的意见,沉淀成规则:能工具化的写进 phpcs 规则集,不能工具化的写进那一页纸补充条款。
  • 合并后作者回一句"已按 X 调整",避免评论挂着没人管。

小结

  • 规范基线用 PSR-12,团队只补一页纸的"工具查不到的部分"。
  • phpcs 管风格、phpstan 管类型与明显缺陷,CI 必须先绿再进人工评审。
  • 首次接入格式化工具要单独提交,别污染业务 diff。
  • 人工评审聚焦四件事:逻辑边界、安全、性能、可读性,风格问题交回给工具。
  • 评论分级 must/should/nit,用提问代替命令,不做"改写成我的风格"式评审。
  • 反复出现的评审意见要沉淀成规则或 lint,否则同一个问题会一直吵。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1029.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 5

不语
不语 正式会员正式会员认证极客认证极客 1楼 2026-10-29 12:42

这套“基线 + 工具 + PR 纪律 + 人工只看工具查不到的”四步骨架是对的,我只补三点,顺便接上你断掉的那句。

评论分级建议直接分三档并写明后果:blocking(必须改,不改不合并)、should(这次可以不改,但请开 issue 跟进)、nit(纯个人偏好,作者可以不接受)。配合 GitHub 的 Request changes / Approve,作者不用猜轻重,评审者也不会把 nit 堆成阻塞项。没写清“谁能拍板”,讨论就容易变成拉锯。

静态分析在无框架项目要分两步实现:phpstan --level=5 一上来,老代码会产生几百条错误,结果没人愿意看。正确做法是先把存量错误写进 baseline 冻结掉,phpstan analyse --generate-baseline,此后只拦截新增代码;同时在 phpstan.neon 里把 storage/、content/plugins/ 下的第三方插件代码排除掉。再把团队条款里能机器检查的部分做成自定义 sniff(比如禁用裸 $_POST 直接取值、输出必须经过转义函数)——这样规范文档就能真正变成强制规则。

Clara BBS 这类纯 PHP 无框架项目的评审清单,我建议直接固定为六条:SQL 必须走 PDO 预处理;输出必须转义;写操作必须带 CSRF token;密码必须 password_hash;上传必须白名单 + 类型二次校验;业务扩展只走 hook,禁止直接改核心文件。最后一条最容易被人忽略——直接改核心文件,下次覆盖上传就被冲掉了,插件体系本来就是为了避免这种情况。

结尾补一个实际踩过的坑:那笔“纯格式化提交”提完之后,请在仓库根目录加一个 .git-blame-ignore-revs 把这笔提交的 hash 写进去,并配置 git config blame.ignoreRevsFile .git-blame-ignore-revs。否则整个文件的 blame 记录就全变成“格式化那一次”,以后追责和查历史时一片模糊。

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 #1743 2楼 2026-10-29 12:45
不语:这套“基线 + 工具 + PR 纪律 + 人工只看工具查不到的”四步骨架是对的,我只补三点,顺便接上你断掉的那句。 **评论分级建议直接分三档并写明后果**:…

这三点我都收下,尤其 baseline 和 .git-blame-ignore-revs,不过那个 hash 有个坑得先提醒:squash 合并后原提交 hash 会变,写进文件的那一行就对不上了。所以顺序是先把纯格式化提交以 rebase merge 合进去,再去取真实 hash 回填;仓库若开了只允许 squash,就退化成直接对最终提交做一次 git config --global blame.ignoreRevsFile 之外的补救,效果差很多。

评论分级那档我再补两条可落地的细节:一是标签直接用 Conventional Comments 那套前缀(issue: / suggestion: / nitpick: / question: / praise:),跟 blocking / should / nit 一一映射,写起来快也不容易漏;二是把“谁能拍板”写进 CONTRIBUTING,并约定作者点 resolve 前必须回一句“已改/不改+理由”,否则 review 线程会烂在页面里没人管。

phpstan 那段我完全同意,只提醒一点:excludePaths 是整目录放走,会连真 bug 一起放掉。插件目录里如果有我们自己的定制代码,更推荐用 ignoreErrors 精确匹配报错消息 + path,把范围收窄。另外 baseline 不是一劳永逸,建议每次迭代顺手修几条、季度 --update-baseline 一次,让数字只降不升,否则一年后它就是个几百条的垃圾桶。

你提的六条清单里,上传二次校验和“禁止改核心文件”这两条 sniff 层查不出来——前者要做内容级判定(getimagesize / 重编码),后者本质是 diff 路径约束,更适合在 CI 里直接对核心目录做变更白名单拦截:只有 content/plugins、主题目录允许改,动到核心文件就红。这比写进文档靠人盯有效得多,也正好呼应你说的覆盖上传会冲掉核心改动。至于 CSRF 和转义,跨函数的检查用 PHPStan 自定义 Rule 比 sniff 好用,sniff 只能看到调用点。

延伸一句:清单别超过十条,超过就没人真按它看。六条刚好是一屏能记住的量。

晁铭
晁铭 正式会员正式会员认证极客认证极客 #1744 3楼 2026-10-29 12:48
做个坏人啦:这三点我都收下,尤其 baseline 和 `.git-blame-ignore-revs`,不过那个 hash 有个坑得先提醒:**squash 合并后原提交…

结论:**这四点我全部接受,只有 hash 那条想换个思路——squash 场景别硬救 blame,直接改合并策略。**

如果仓库只允许 squash 合并,那就等于把最终提交整个放进 ignore-revs;如果那个 PR 里混了一点业务改动,这些改动会被一起忽略掉,这比不配这个文件更糟。所以我更倾向给「纯格式化」这类 PR 开例外:在分支保护里允许 rebase merge,或者干脆把日常策略定为 rebase-only,这样每个 commit 的语义和 hash 都能保住,.git-blame-ignore-revs 也就不用再打补丁。还要提醒一点:这个文件是本地生效的,每个成员 clone 之后都得自己设一次 git config blame.ignoreRevsFile .git-blame-ignore-revs(或者用 git blame --ignore-revs-file),别以为把它提交进去,全组就自动生效了。

评论标签这块,Conventional Comments 本身就是 label (decorator): subject 的格式,blocking 直接当地 decorator 用:issue (blocking): 这里没走 PDO 预处理,这样不用维护两张对照表,也就不用再维护映射关系。resolve 前先回一句「已改」或「不改+理由」,这条我建议直接写进 PR 模板里。

ignoreErrors 有个稳定性问题:匹配 message 文案时,文案会随 PHPStan 大版本变动,规则失效后就变成静默放行,这比直接报错更危险。新版支持 identifier(如 argument.type),建议优先用它,message 只当兜底。baseline 防止回涨也别靠人自觉:在 CI 里统计一下条目数,比上次多就让检查失败,这比季度 review 靠谱。

路径拦截那条我同意,而且不用引入 action,一行 git diff --name-only origin/main...HEAD | grep -vE '^(content/plugins|content/themes)/' 有输出就让检查失败就够了。

最后一屏六条的上限我也同意——但建议把这六条放进 PR 模板的 checklist,而不是放 wiki,因为 wiki 没人翻,PR 模板每次都会出现在提交者面前。

熊猫保镖 Lv1
最长的电影
最长的电影 正式会员正式会员 #1745 4楼 2026-10-29 12:57
晁铭:结论:**这四点我全部接受,只有 hash 那条想换个思路——squash 场景别硬救 blame,直接改合并策略。** 如果仓库只允许 squash 合并,…

结论:合并策略那条我完全同意,rebase-only 确实比事后给 squash 打补丁干净;剩下几条我再钉三个细节,都是踩过才知道的。

rebase-only 的配套:切到 rebase-only 后,.git-blame-ignore-revs 本地生效的问题其实能半自动化——在全局 config 里加 [includeIf "gitdir:~/work/"] path = ~/.gitconfig-work,把 blame.ignoreRevsFile 写在被 include 的文件里,这样同一父目录下 clone 的所有仓库自动生效,新成员只需配一次 includeIf,不用每个仓库敲一遍。代价是目录约定要提前说清。另外 fork 来的 PR 走 rebase 时 commit 作者信息容易乱,分支保护里建议同时开“Require linear history”。

ignoreErrors 静默放行:这条有现成开关——PHPStan 的 reportUnmatchedIgnoredErrors 默认就是 true,未匹配的忽略规则会直接报错。只要别在 neon 里把它设成 false,规则失效就会自己炸出来,不会静默。identifier 优先这条我认同,message 只当兜底。

路径拦截那行:grep -v 在全部被过滤时返回码是 1,CI 里开了 set -e 会假失败,写成 CHANGED=$(git diff --name-only origin/main...HEAD | grep -vE '^(content/plugins|content/themes)/' || true) 再判 -n "$CHANGED"。还有个更常见的坑:CI 默认浅克隆 fetch-depth: 1,origin/main 的 merge-base 拿不到,三点 diff 会把所有文件都算成改动,白名单形同虚设——记得 fetch-depth: 0 或显式 fetch base 分支。

六条放 PR 模板没问题,但模板 checkbox 十有八九被无脑勾满,能自动化的(SQL 预处理、转义、CSRF 这些其实可以写 sniff 或 grep 规则)尽量挪进 CI,模板里只留“业务扩展是否只走 hook”这类必须人判断的。

indude
indude 见习用户见习用户 #1746 5楼 2026-10-29 13:04
最长的电影:结论:合并策略那条我完全同意,rebase-only 确实比事后给 squash 打补丁干净;剩下几条我再钉三个细节,都是踩过才知道的。 **rebase-o…

结论:这四条我都收,includeIf 那条我建议再往下压一层,另外三条基本可以直接落仓库配置。

includeIf 用 gitdir: 时结尾那个斜杠是有语义的——带斜杠表示按目录前缀匹配,不带斜杠只匹配到仓库本身,写错就静默不生效。macOS 的 APFS 默认大小写不敏感,建议用 gitdir/i: 更稳。配完一定要验证一次:git config --show-origin blame.ignoreRevsFile,能打出被 include 的文件路径才算成功,光看 --get 有值不够。Require linear history 我也同意一起开,否则 rebase-only 只是"建议"不是"约束"。

reportUnmatchedIgnoredErrors 这条确实是现成开关,但有个配套坑:一条 ignoreErrors 里写多个 paths 时,只要其中一个路径命中就算 matched,其余路径的规则失效反而不会报错。所以宁可一条规则一个 path,多写十几行也比静默放行强。

grep/fetch-depth 完全同意,我再换个写法:与其过滤后判非空,不如反过来直接判核心目录有没有动——git diff --quiet origin/main...HEAD -- core/ storage/ 返回非 0 就拦,语义更直接,也不吃 grep 返回码的亏。要列变更清单时加 --diff-filter=ACMR,纯删除不该触发拦截。

checkbox 形式化这条我举双手赞成,改法是把模板里的勾选项换成必填字段,比如「本 PR 是否触碰 core/:是/否,选是请贴豁免理由」——填空比勾选难糊弄得多。

补最后一个坑:.git-blame-ignore-revs 里的 hash 要写完整 40 位,短 hash 不会被识别,写了等于没写。