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 namesComposer 在执行过程中会按生命周期触发命名事件。所有脚本事件的字符串常量统一定义在 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-installcreate-project命令中根包安装完成之后但在其依赖安装之前post-create-project-cmdcreate-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事件触发时机initComposer 实例完成初始化之后commandCLI 上执行任意 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插件事件 - initComposer\EventDispatcher\Event插件事件 - commandComposer\Plugin\CommandEvent插件事件 - pre-file-downloadComposer\Plugin\PreFileDownloadEvent插件事件 - post-file-downloadComposer\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_TIMEOUT0要禁用单次脚本调用的超时必须使用run-script命令并指定--timeout参数php composer.phar run-script --timeout0 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 行并检测循环引用。如果引用了不存在的脚本会输出 warningYou 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 COMPOSERphpstan-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.readonly0 bin/compile, test: php simple-phpunit, phpstan: php vendor/bin/phpstan analyse --configurationphpstan/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_SCRIPTSpost-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),仅供参考