在 composer.json 的 "scripts" 字段中定义自定义脚本,支持字符串、字符串数组或含 "type": "script" 的关联数组;推荐用数组形式显式声明类型,路径优先用 php vendor/bin/phpunit 而非 ./vendor/bin/phpunit,注意跨平台兼容性与参数透传需通过 exec 或 php 文件中转,并手动处理 exit code 以确保 ci 失败中断。

composer.json 里怎么定义自定义脚本
Composer 脚本本质是命令别名,写在 composer.json 的 "scripts" 字段里,运行时通过 composer run-script 或简写 composer run 触发。
常见错误是把脚本写成 shell 命令串但没考虑跨平台兼容性,比如用 rm -rf 在 Windows 上直接报错;或者漏写 "script" 类型声明,导致某些钩子(如 post-install-cmd)不触发。
- 脚本值可以是字符串(单条命令)、字符串数组(按顺序执行)、或关联数组(含
"cmd"、"event"、"description"等字段) - 推荐用数组形式,显式声明
"type": "script",避免被 Composer 当作事件钩子误判 - 路径尽量用
php而非./vendor/bin/phpunit,后者在不同项目结构下容易失效
"scripts": {
"test": {
"type": "script",
"description": "Run PHPUnit tests",
"cmd": "php vendor/bin/phpunit"
}
}什么时候该用 scripts 而不是 scripts-dev
"scripts" 和 "scripts-dev" 没有语法区别,后者只是约定俗成的键名——Composer 本身不识别 scripts-dev,它只是个普通字段。真正起作用的是脚本名是否匹配内置事件(如 post-autoload-dump)或是否被手动调用。
容易踩的坑是以为 scripts-dev 会自动只在开发环境运行,结果 CI 流水线里也执行了不该跑的清理脚本,删掉了生成文件。
- 区分环境靠逻辑判断,比如检查
$_ENV['COMPOSER_DEV_MODE']或读取composer.json的"config": {"dev-mode": true} - 敏感操作(如清缓存、删日志)建议加
--no-interaction标志并默认跳过,除非显式传-v或--force - CI 场景下优先用
composer install --no-dev,这样连scripts里的 dev-only 命令都不会加载
如何让自定义脚本支持参数传递
Composer 默认不透传参数给脚本命令,composer run test -- --filter=MyTest 这种写法里,--filter=MyTest 会被丢弃,除非脚本定义里明确启用 "interpret" 或用 exec 包裹。
根本原因是 Composer 对字符串脚本做的是简单替换,不是 shell 执行;数组脚本又默认不解析尾部参数。
- 最稳的方式:用
exec启动子进程,比如"cmd": "exec php vendor/bin/phpunit $@",然后调用时加-- --filter=... - 更干净的做法:写一个独立 PHP 文件(如
bin/run-test.php),在脚本里用$argv解析参数,再调用实际逻辑 - 注意 Windows 下
$@不生效,得用%*,所以跨平台必须走 PHP 中转层
脚本执行失败但 composer 仍返回 0 怎么办
这是 Composer 的默认行为:只要脚本进程启动成功,不管内部命令 exit code 是多少,Composer 都返回 0。CI 流程里这就意味着失败的测试不会中断构建。
核心问题在于 Composer 把脚本当“任务”而非“校验步骤”,没强制要求 exit code 透传。
- 在脚本命令末尾显式加
&& exit $?(Linux/macOS)或&& exit /b %errorlevel%(Windows) - 改用
php -r "system('vendor/bin/phpunit'); exit($?);"这类封装,确保子进程状态被捕获 - 检查 Composer 版本,2.5+ 对部分内置事件(如
post-update-cmd)已增强错误传播,但自定义脚本仍需手动处理
脚本里最容易被忽略的是信号转发和子进程超时控制——比如 PHPUnit 卡死,父进程不会自动 kill 它,得自己加 pcntl_signal 或用 proc_open 配合 timeout。










