pre-commit钩子通过Git触发并调用Composer脚本检查代码质量,需确保脚本返回非0码以阻断问题提交;关键包括启用严格模式、路径校准、增量检查及CI与本地行为一致。

提交前用 Composer 脚本检查代码质量
Git 提交前(pre-commit)不是靠 Composer 自己触发的,而是靠 Git 钩子调用 composer run 或直接执行脚本命令。关键在于:Composer 脚本必须是可被外部 shell 正确调用的、有明确退出码的命令——成功返回 0,失败返回非 0(比如 php-cs-fixer 找到问题但加了 --dry-run,它默认仍会返回 0,这就拦不住提交)。
常见错误现象:git commit 顺利通过,但其实 phpstan 报了 error 或 psalm 发现了类型不匹配——因为脚本里没加 --no-interaction 或没设 fail-on-error 模式。
- 在
composer.json的"scripts"里定义检查任务,例如:"check": "phpstan analyse --no-interaction --level=max src/ && psalm --no-severity-highlighting --no-progress"
- 确保所有工具都启用严格模式:
php-cs-fixer fix --dry-run --using-cache=no(否则缓存可能掩盖新问题) - 避免在脚本中使用交互式参数(如
--interactive),Git 钩子环境下没有 TTY,会卡住或静默失败
pre-commit 钩子怎么写才可靠
别用全局钩子或手动复制 .git/hooks/pre-commit,推荐用 composer require --dev husky 或更轻量的 composer require --dev phing/phing + 自定义 shell 脚本。但最稳的方式是直接写一个可执行的 shell 文件,放在项目根目录(比如 bin/pre-commit),再软链进 .git/hooks/pre-commit。
容易踩的坑:git 钩子运行时的 $PWD 是 Git 工作区根目录,但某些 Composer 命令依赖 vendor/autoload.php,如果钩子脚本没先 cd 到项目根或没设好 COMPOSER_HOME,就会报 Class 'Composer\Autoload\ClassLoader' not found。
- 钩子脚本第一行必须是
#!/usr/bin/env bash,且文件要有执行权限:chmod +x bin/pre-commit - 开头加
cd "$(git rev-parse --show-toplevel)"确保路径正确 - 调用命令时显式指定 PHP 版本(尤其 CI 和本地不一致时):
php8.2 -d memory_limit=-1 composer run check - 捕获输出但不抑制退出码:
if ! composer run check 2>&1; then exit 1; fi
为什么 vendor/bin/xxx 直接调用有时失效
不是所有 vendor/bin/ 下的可执行文件都适合放进 pre-commit:有些工具(如 phpunit)默认只跑 tests/,而你改的是 src/;有些(如 phpcs)默认不检查 .php 以外的扩展名,但你的项目用了 .inc 或 .module。
仅从仓库的暂存文件生成 Git 提交消息。当用户要求 Codex 根据暂存更改、缓存差异、Git 索引或已通过 `git add` 添加的文件来编写、起草、创建或建议提交消息时使用;切勿代表用户执行提交操作。
性能影响明显:每次提交都全量跑 phpstan analyse 可能要 3–5 秒,如果团队习惯小步提交,体验会变差。这时候就得做增量检查。
- 用
git diff --cached --name-only '*.php'获取本次提交涉及的 PHP 文件,传给检查工具 -
phpstan支持按文件列表分析:phpstan analyse --no-interaction $(git diff --cached --name-only '*.php') -
php-cs-fixer必须加--allow-risky=yes才能处理部分规则(比如@PHP80Migration),否则静默跳过 - 注意 Windows 用户:Git Bash 下
$(...)语法可用,但 PowerShell 需改用$(git diff ...)或换用husky的 cross-platform 封装
CI 和本地 pre-commit 行为不一致怎么办
最常被忽略的一点:CI 环境通常用 composer install --no-dev,而 pre-commit 依赖 phpstan 这类 dev-only 工具。如果钩子脚本没做存在性校验,CI 上执行会直接报 command not found,导致整个流水线中断。
另一个隐形陷阱:composer.lock 更新后,不同 PHP 版本下 phpstan 的规则集可能变化(比如从 1.10 升到 1.11 后默认启用更严的 strict-rules),本地没更新 lock,CI 却跑了新版,结果 CI 报错而本地不报。
- 钩子脚本开头加判断:
if [ ! -f vendor/bin/phpstan ]; then echo "phpstan not installed, skipping"; exit 0; fi - CI 流水线里,提交前检查应复用同一套脚本,但用
composer install --with-all-dependencies确保 dev 工具就位 - 把 PHP 版本和工具版本锁死在
composer.json的"config.platform"和"require-dev"中,而不是依赖全局安装
真正麻烦的从来不是写钩子,而是让所有人提交前都走同一套检查逻辑——哪怕只漏掉一个队友的 pre-commit 没启用,问题代码就进了主干。

















