Composer 脚本机制(Scripts)完全指南:事件、回调、自定义命令与进程控制
【免费下载链接】composerDependency Manager for PHP项目地址: https://gitcode.com/gh_mirrors/co/composer
导读
Composer 脚本(Scripts)是挂在composer.json中、由 Composer 在安装/更新/归档等执行过程中自动触发的回调机制:它可以是一个 PHP 静态方法回调,也可以是一条命令行可执行命令,还可以是 Symfony Console 的Command类。本文以 doc/articles/scripts.md 为骨架,结合本仓库(composer/composer)的 EventDispatcher 源码与 ScriptEvents 常量定义,系统讲解事件名称与触发时机、脚本定义方式、事件对象 API、run-script手动执行、自定义命令、进程超时管理、@引用语法、环境变量与描述/别名配置,帮你把 Composer 脚本从"能用"提升到"用得明白、用得稳"。
什么是脚本
在 Composer 的语境中,一个脚本可以是:
- PHP 回调:定义为类中静态方法(static method)的可调用体;
- 命令行可执行命令:任意可在 shell 中执行的程序;
- Symfony Console Command 类(Composer 2.5 起支持):方便你定义参数与选项,但不推荐用于处理事件。
脚本的典型用途是在 Composer 的执行流程中运行某个包的自定义代码、或执行与该包绑定的特定命令(例如安装后刷新缓存、复制配置文件、运行测试等)。
注意:只有根包(root package)的
composer.json中定义的脚本才会被执行。如果某个依赖包在它自己的composer.json中声明了脚本,Composer 不会执行这些脚本。
这一点在源码中也有印证:EventDispatcher::getScriptListeners() 只读取$package->getScripts(),而这里的$package是$this->composer->getPackage(),即根包。
事件名称(Event names)
Composer 在执行过程中会按生命周期触发命名事件。所有脚本事件的字符串常量统一定义在 ScriptEvents 中,插件相关的常量则分布在 InstallerEvents、PackageEvents、PluginEvents 中。
命令事件(Command Events)
| 事件 | 触发时机 |
|---|---|
pre-install-cmd | 在存在 lock 文件的前提下执行install命令之前 |
post-install-cmd | 在存在 lock 文件的前提下执行install命令之后 |
pre-update-cmd | 执行update命令之前,或不存在 lock 文件时执行install命令之前 |
post-update-cmd | 执行update命令之后,或不存在 lock 文件时执行install命令之后 |
pre-status-cmd | 执行status命令之前 |
post-status-cmd | 执行status命令之后 |
pre-archive-cmd | 执行archive命令之前 |
post-archive-cmd | 执行archive命令之后 |
pre-autoload-dump | 转储(dump)自动加载器之前(发生在install/update过程中,或通过dump-autoload命令触发) |
post-autoload-dump | 自动加载器转储之后(同上触发路径) |
post-root-package-install | create-project命令中根包安装完成之后(但在其依赖安装之前) |
post-create-project-cmd | create-project命令执行完毕之后 |
安装器事件(Installer Events)
| 事件 | 触发时机 |
|---|---|
pre-operations-exec | 在安装 lock 文件并即将执行 install/upgrade 等操作之前。需要挂钩该事件的插件必须以全局方式安装才可用,否则在项目全新安装时插件尚未被加载 |
包事件(Package Events)
| 事件 | 触发时机 |
|---|---|
pre-package-install | 某个包安装之前 |
post-package-install | 某个包安装之后 |
pre-package-update | 某个包更新之前 |
post-package-update | 某个包更新之后 |
pre-package-uninstall | 某个包卸载之前 |
post-package-uninstall | 某个包卸载之后 |
插件事件(Plugin Events)
| 事件 | 触发时机 |
|---|---|
init | Composer 实例完成初始化之后 |
command | CLI 上执行任意 Composer 命令之前,可访问程序的 input 与 output 对象 |
pre-file-download | 文件下载之前,允许你根据待下载 URL 提前操作HttpDownloader对象 |
post-file-download | 包 dist 文件下载完成之后,允许对文件做额外检查 |
pre-command-run | 命令执行之前,允许你修改InputInterface对象的选项与参数以调整命令行为 |
pre-pool-create | 包 Pool 创建之前,可过滤将进入 Solver 的包列表 |
重要提醒:Composer 对
install/update之前依赖的状态不作任何假设。因此不要在pre-update-cmd或pre-install-cmd钩子中书写依赖 Composer 所管理依赖的脚本。如果你需要在install/update之前执行脚本,请确保它们自包含在根包内。
定义脚本
根composer.json中的 JSON 对象应包含名为"scripts"的属性,其值为"事件名 → 脚本"的映射。一个事件的脚本可以是字符串(仅单个脚本),也可以是数组(单个或多个脚本)。
对任意事件而言:
- 事件触发时,脚本按定义顺序执行;
- 绑定到同一事件的脚本数组可同时混用 PHP 回调与命令行可执行命令;
- 包含回调的 PHP 类与命令必须能通过 Composer 的自动加载功能加载;
- 回调只能自动加载 psr-0、psr-4 与 classmap 中定义的类。如果回调依赖类之外定义的函数,回调自身负责加载包含这些函数的文件。
完整示例:
{ "scripts": { "post-update-cmd": "MyVendor\\MyClass::postUpdate", "post-package-install": [ "MyVendor\\MyClass::postPackageInstall" ], "post-install-cmd": [ "MyVendor\\MyClass::warmCache", "phpunit -c app/" ], "post-autoload-dump": [ "MyVendor\\MyClass::postAutoloadDump" ], "post-create-project-cmd": [ "php -r \"copy('config/local-example.php', 'config/local.php');\"" ] } }配合上面的定义,下面是一个可用的回调类MyVendor\MyClass:
<?php namespace MyVendor; use Composer\Script\Event; use Composer\Installer\PackageEvent; class MyClass { public static function postUpdate(Event $event) { $composer = $event->getComposer(); // do stuff } public static function postAutoloadDump(Event $event) { $vendorDir = $event->getComposer()->getConfig()->get('vendor-dir'); require $vendorDir . '/autoload.php'; some_function_from_an_autoloaded_file(); } public static function postPackageInstall(PackageEvent $event) { $installedPackage = $event->getOperation()->getPackage(); // do stuff } public static function warmCache(Event $event) { // make cache toasty } }关于
COMPOSER_DEV_MODE:在install或update命令运行期间,环境变量COMPOSER_DEV_MODE会被注入环境。若命令带有--no-dev标志,该变量为0,否则为1。该变量在dump-autoload运行时同样可用,取值与最近一次install/update相同。在源码中,这一信息通过 Script\Event 的isDevMode()与getDevMode()传递,并在 EventDispatcher::makeAutoloader() 中用于决定自动加载器是否包含 dev 依赖。
事件类(Event classes)
事件被触发时,你的 PHP 回调收到的第一个参数是一个Composer\EventDispatcher\Event对象,它提供了getName()方法用于获取事件名。
根据脚本类型的不同,你会得到不同的事件子类,它们带有各种 getter 和关联对象:
| 事件类型 | 事件类 |
|---|---|
| 基类 | Composer\EventDispatcher\Event |
| 命令事件 | Composer\Script\Event |
| 安装器事件 | Composer\Installer\InstallerEvent |
| 包事件 | Composer\Installer\PackageEvent |
| 插件事件 - init | Composer\EventDispatcher\Event |
| 插件事件 - command | Composer\Plugin\CommandEvent |
| 插件事件 - pre-file-download | Composer\Plugin\PreFileDownloadEvent |
| 插件事件 - post-file-download | Composer\Plugin\PostFileDownloadEvent |
以 Script\Event 为例,它额外提供了:
getComposer():返回 Composer 实例,可进一步读取配置、仓库管理器等;getIO():返回 IO 接口(用于输出提示/错误);isDevMode():返回当前是否为 dev 模式;getArguments():返回用户传入的额外参数数组;getOriginatingEvent()/setOriginatingEvent():用于@引用脚本时追踪调用链上的最顶层事件。
包事件 PackageEvent 则能通过getOperation()->getPackage()拿到当前被安装/更新/卸载的具体包对象。
手动运行脚本
如果你想手动触发某个事件的脚本,语法为:
php composer.phar run-script [--dev] [--no-dev] script例如composer run-script post-install-cmd会运行所有已定义的post-install-cmd脚本以及 插件 注册的监听器。
你还可以通过--向脚本处理器追加参数,例如:
composer run-script post-install-cmd -- --check会把--check传给脚本处理器:CLI 处理器会将其作为命令行参数接收,PHP 处理器则可通过$event->getArguments()以数组形式读取。
对应的命令实现位于 RunScriptCommand,它同时支持--timeout参数(见下文"进程超时")。
编写自定义命令
如果你添加的自定义脚本不属于上面预定义的事件名,你可以:
- 用
run-script运行它; - 或将它们当作 Composer 原生命令运行。
例如,下面定义的处理器可以直接通过composer test执行:
{ "scripts": { "test": "phpunit", "do-something": "MyVendor\\MyClass::doSomething", "my-cmd": "MyVendor\\MyCommand" } }与run-script类似,你可以给脚本追加参数,例如composer test -- --filter <pattern>会把--filter <pattern>传给phpunit。
- 通过 PHP 方法执行
composer do-something arg,会调用static function doSomething(\Composer\Script\Event $event),arg可以在$event->getArguments()中获取。但这种方式不方便以--flags形式传递自定义选项。 - 使用 symfony/console 的
Command类,你可以更轻松地描述脚本、定义和访问参数与选项。
用 Symfony Console Command 类定义脚本
以下面的命令为例,你可以直接运行composer my-cmd --arbitrary-flag,甚至无需--分隔符。要被识别为 symfony/console 命令,类名必须以Command结尾并继承 Symfony 的Command类。同时注意:这会使用 Composer 内置的 symfony/console 版本,可能与你在项目中 require 的版本不一致,且会随 Composer 次版本更新而变化。如果需要更强的版本保障,建议使用你自己的二进制文件,在独立进程中运行你自己的 symfony/console 版本。
脚本名称与描述(定义在Command类内)会覆盖composer.json中的配置:scripts中的键(作为传给run-script的命令名)会被$defaultName或setName()的值替换,scripts-descriptions中对应脚本类的描述也会被类内定义替换。
<?php namespace MyVendor; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class MyCommand extends Command { protected function configure(): void { $this // ->setName('custom-cmd') //if this gets included, it would execute with `composer custom-cmd` instead ->setDescription('Custom description for this command') ->setDefinition([ new InputOption('arbitrary-flag', null, InputOption::VALUE_NONE, 'Example flag'), new InputArgument('foo', InputArgument::OPTIONAL, 'Optional arg'), ]) ->setHelp( "Here you can define a long description for your command\n". "This would be visible with composer my-cmd --help" ); } public function execute(InputInterface $input, OutputInterface $output): int { if ($input->getOption('arbitrary-flag')) { $output->writeln('The flag was used'); } return 0; } }从源码层面看,EventDispatcher::doDispatch() 会对字符串回调做如下判定:
isCommandClass()(第 643 行):包含\、不含空格且以Command结尾 → 当作 Command 类处理,并校验它确实是Symfony\Component\Console\Command\Command的子类(第 303 行),同时禁止把保留事件名(如pre-install-cmd)绑定到 Command 类上(第 307-310 行);isPhpScript()(第 635 行):不含空格且包含::→ 当作Class::method静态方法调用;isComposerScript()(第 651 行):以@开头(且不是@php与@putenv)→ 当作对另一个脚本的引用。
关于 PATH:在执行脚本之前,Composer 会把 bin-dir 临时推到
PATH环境变量的最前面(见 ensureBinDirIsInPath()),因此依赖的二进制文件可以直接被找到。在上面的例子中,无论phpunit实际位于vendor/bin/phpunit还是bin/phpunit,都能被找到并执行。
另外,pushEvent() 会在事件栈中检测循环调用:如果同一事件被重复触发,会抛出Circular call to script handler ... detected异常,防止脚本无限递归。
管理进程超时
虽然 Composer 并不打算管理 PHP 项目中的长时运行进程,但有时在自定义命令上禁用进程超时确实很方便。该超时默认为 300 秒(见 Config.php 的默认配置),可以通过以下几种方式覆盖:
- 全局禁用(所有命令):使用配置键
process-timeout; - 当前及后续调用禁用:使用环境变量
COMPOSER_PROCESS_TIMEOUT; - 单次调用禁用:使用
run-script命令的--timeout标志; - 针对特定脚本禁用:使用静态辅助方法。
针对特定脚本禁用超时,直接在composer.json中引入辅助方法:
{ "scripts": { "test": [ "Composer\\Config::disableProcessTimeout", "phpunit" ] } }针对整个项目禁用所有脚本的超时,使用composer.json配置:
{ "config": { "process-timeout": 0 } }也可以设置全局环境变量,在当前终端环境中禁用之后所有脚本的超时:
export COMPOSER_PROCESS_TIMEOUT=0要禁用单次脚本调用的超时,必须使用run-script命令并指定--timeout参数:
php composer.phar run-script --timeout=0 test源码佐证:process-timeout的默认值 300 秒定义在 Config.php#L39,其类型校验(数字、转为 int)在 ConfigCommand.php#L336,而 BaseIO 会在 IO 初始化时把该配置应用为ProcessExecutor的超时。Composer\Config::disableProcessTimeout()静态方法则定义在 Config.php#L751 附近。
引用其他脚本
为了复用脚本、避免重复定义,你可以用@前缀在脚本中调用另一个脚本:
{ "scripts": { "test": [ "@clearCache", "phpunit" ], "clearCache": "rm -rf cache/*" } }还可以引用脚本并给它传新参数:
{ "scripts": { "tests": "phpunit", "testsVerbose": "@tests -vvv" } }从实现上看,@引用会通过 isComposerScript() 识别,然后递归分发:目标脚本会收到一个带有originatingEvent链的新ScriptEvent(第 260-272 行),并检测循环引用。如果引用了不存在的脚本,会输出 warning:You made a reference to a non-existent script。
调用 Composer 命令
调用 Composer 命令可以使用@composer,它会自动解析为当前正在使用的 composer.phar:
{ "scripts": { "test": [ "@composer install", "phpunit" ] } }一个限制是:你不能像@composer install && @composer foo这样在一行内连续调用多个 composer 命令,必须拆分成 JSON 数组中的多个命令条目。
源码中,@composer开头的脚本会走专门的执行路径(第 252-258 行),使用当前进程的 PHP 可执行文件与COMPOSER_BINARY环境变量来重新拉起 composer 进程。
执行 PHP 脚本
执行 PHP 脚本可以使用@php,它会自动解析为当前正在使用的 php 进程:
{ "scripts": { "test": [ "@php script.php", "phpunit" ] } }同样的限制:不能在一行内写@php install && @php foo,需拆成 JSON 数组。
你也可以调用 shell/bash 脚本,在其中通过PHP_BINARY环境变量拿到 PHP 可执行文件的路径。实现上,第 388-412 行 会把@php前缀替换为完整的 PHP 命令(包含allow_url_fopen、disable_functions、memory_limit等与当前进程一致的 ini 设置,见getPhpExecCommand()),并在 Windows 下做路径转义与扩展名处理。
控制附加参数
自 Composer 2.8 起,你可以控制额外参数如何传给脚本命令。
当运行composer script-name arg arg2或composer script-name -- --option时,Composer 默认会把arg、arg2与--option追加到脚本命令的末尾。
- 如果某条命令不想接收这些参数,可以在命令中任意位置放入
@no_additional_args:它会移除默认追加行为,并在真正执行前被删除; - 如果希望参数追加到其他位置(而非最末尾),可以用
@additional_args精确指定参数插入点。
例如,运行composer run-commands ARG,配合下面的配置:
{ "scripts": { "run-commands": [ "echo hello @no_additional_args", "command-with-args @additional_args && do-something-without-args --here" ] } }最终会执行:
echo hello command-with-args ARG && do-something-without-args --here实现细节见 EventDispatcher::doDispatch()(@no_additional_args的剥离)与 第 240-245 行 / 第 354-358 行(@additional_args的插入逻辑)。
设置环境变量
要以跨平台的方式设置环境变量,可以使用@putenv:
{ "scripts": { "install-phpstan": [ "@putenv COMPOSER=phpstan-composer.json", "@composer install --prefer-dist" ] } }@putenv由 EventDispatcher 在 第 378-387 行 特殊处理:带=时设置变量(Platform::putEnv),不带=时清除该环境变量(Platform::clearEnv)。正因为如此,@putenv本身不接收脚本追加的参数。
自定义描述(scripts-descriptions)
你可以在composer.json中为自定义脚本设置描述:
{ "scripts-descriptions": { "test": "Run all tests!" } }这些描述会在composer list或composer run -l命令中显示,用于说明脚本的用途。
注意:只能为自定义命令设置描述(预定义事件名不可设置)。
本仓库自身就是一个范例——composer.json#L100-L109 中定义了compile、test、phpstan三个脚本及其描述:
"scripts": { "compile": "@php -dphar.readonly=0 bin/compile", "test": "@php simple-phpunit", "phpstan": "@php vendor/bin/phpstan analyse --configuration=phpstan/config.neon" }, "scripts-descriptions": { "compile": "Compile composer.phar", "test": "Run all tests", "phpstan": "Runs PHPStan" }自定义别名(scripts-aliases)
自 Composer 2.7 起,你可以为自定义脚本设置别名:
{ "scripts-aliases": { "phpstan": ["stan", "analyze"] } }别名提供了替代的命令名(例如用composer stan或composer analyze代替composer phpstan)。
注意:只能为自定义命令设置别名。
补充:跳过脚本(COMPOSER_SKIP_SCRIPTS)
除文档正文外,源码还揭示了一个实用的环境变量:EventDispatcher 构造函数 会读取COMPOSER_SKIP_SCRIPTS,将其按逗号拆分成待跳过的脚本名列表;getScriptListeners() 会据此在分发时直接跳过对应事件的全部脚本,非常适合在 CI 或临时调试场景中禁用特定钩子:
export COMPOSER_SKIP_SCRIPTS=post-install-cmd,post-autoload-dump小结与建议
- 脚本按事件驱动:命令事件、安装器事件、包事件、插件事件分别在 Composer 生命周期的不同节点触发,事件字符串常量可参考 ScriptEvents.php;
- 脚本可以是
Class::staticMethod回调、命令行命令,或 Symfony ConsoleCommand类(仅 Composer 2.5+,且不建议用于事件); - 所有脚本事件的核心分发逻辑集中在 EventDispatcher,其监听器合并、
@引用解析、@php/@putenv/@composer特化处理、bin-dir 注入 PATH、循环调用检测等行为都有清晰的源码实现可查; - 涉及长时间任务的脚本,可按需通过
process-timeout配置、COMPOSER_PROCESS_TIMEOUT环境变量、run-script --timeout或Composer\Config::disableProcessTimeout四种方式管理超时; - 使用
scripts-descriptions与scripts-aliases可以让自定义脚本在composer list/composer run -l中更可读、更易用。
相关测试用例可参考 tests/Composer/Test/EventDispatcher/EventDispatcherTest.php,其中覆盖了脚本分发、@引用、参数透传、超时与 Command 类脚本等行为,是深入理解本文各特性的第一手实证材料。
【免费下载链接】composerDependency Manager for PHP项目地址: https://gitcode.com/gh_mirrors/co/composer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考