Scripts 钩子
Composer 的脚本系统允许你在安装、更新、卸载等生命周期事件中自动执行自定义命令。通过预定义事件钩子(Pre/Post 事件)和自定义脚本命令,你可以将代码检查、测试运行、文件权限设置等常见任务自动化。本节将详细介绍 Composer 脚本的各种配置方式和实用场景。
前置知识
阅读本节前,建议先了解:
- Composer 基本命令(参见 Composer 基本命令)
- composer.json 配置结构(参见 composer.json 配置)
- Shell/Bash 脚本基础
基础概念
脚本运行时机
Composer 在执行命令的不同阶段会触发预定义事件。你可以注册回调来响应这些事件:
composer install
├── pre-install-cmd ← 安装前触发
├── post-install-cmd ← 安装后触发
└── pre-autoload-dump ← 自动加载生成前
└── post-autoload-dump ← 自动加载生成后
composer update
├── pre-update-cmd
└── post-update-cmd
composer require
├── pre-package-install ← 每个包安装前
└── post-package-install ← 每个包安装后内置事件列表
| 事件名 | 触发时机 | 典型用途 |
|---|---|---|
pre-install-cmd | install 命令执行前 | 环境检查 |
post-install-cmd | install 命令执行后 | 权限设置、缓存清理 |
pre-update-cmd | update 命令执行前 | 数据库备份 |
post-update-cmd | update 命令执行后 | 测试运行 |
pre-status-cmd | status 命令执行前 | |
post-status-cmd | status 命令执行后 | |
pre-package-install | 每个包安装前 | |
post-package-install | 每个包安装后 | |
pre-package-update | 每个包更新前 | |
post-package-update | 每个包更新后 | |
pre-package-uninstall | 每个包卸载前 | |
post-package-uninstall | 每个包卸载后 | |
pre-autoload-dump | 自动加载生成前 | |
post-autoload-dump | 自动加载生成后 | IDE helper 生成 |
post-root-package-install | 项目根包安装后 | |
post-create-project-cmd | 项目创建后 | 初始化脚本 |
语法与配置
基本配置
json
{
"scripts": {
"test": "phpunit",
"check": [
"@phpcs",
"@phpstan",
"@test"
],
"phpcs": "phpcs --standard=PSR12 src/",
"phpstan": "phpstan analyse src/",
"post-install-cmd": [
"chmod -R 775 storage bootstrap/cache"
],
"post-update-cmd": [
"@phpcs",
"@test"
]
}
}自定义脚本命令
json
{
"scripts": {
"test": "phpunit",
"test:unit": "phpunit --testsuite unit",
"test:integration": "phpunit --testsuite integration",
"test:all": [
"@test:unit",
"@test:integration"
],
"test:coverage": "phpunit --coverage-html coverage/",
"lint": "find src -name '*.php' -exec php -l {} \\;",
"fix": "php-cs-fixer fix",
"check": [
"@lint",
"@phpcs",
"@phpstan",
"@test"
],
"serve": "php -S localhost:8000 -t public/",
"build": [
"@lint",
"@test",
"composer dump-autoload --optimize --no-dev"
]
}
}脚本引用(@)
json
{
"scripts": {
"cs-check": "phpcs --standard=PSR12 src/",
"cs-fix": "php-cs-fixer fix",
"static-analysis": "phpstan analyse src/",
"test": "phpunit",
"check-all": [
"@cs-check",
"@static-analysis",
"@test"
],
"fix-all": [
"@cs-fix",
"@static-analysis"
],
"deploy": [
"@check-all",
"@build",
"@optimize"
]
}
}@ 符号的含义
@script-name 引用另一个自定义脚本命令。Composer 会递归解析引用,但不会循环引用。如果脚本名与内置命令相同,引用会指向自定义脚本。
详细说明
事件钩子详解
pre/post-install-cmd
json
{
"scripts": {
"pre-install-cmd": [
"echo '开始安装依赖...'",
"php -r \"if (!file_exists('.env')) copy('.env.example', '.env');\""
],
"post-install-cmd": [
"@post-autoload-dump",
"echo '安装完成'",
"php artisan key:generate --ansi"
]
}
}pre/post-autoload-dump
json
{
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
}
}post-autoload-dump 的特殊性
post-autoload-dump 在每次 install、update、require、remove 后都会触发。这是执行依赖包发现的理想时机。Laravel 利用此事件来自动发现服务提供者。
post-create-project-cmd
json
{
"scripts": {
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"echo '项目创建完成!运行 php artisan serve 启动开发服务器'"
]
}
}Composer 插件脚本
Composer 插件可以注册自定义事件。在 scripts 中直接引用 PHP 类的方法:
json
{
"scripts": {
"post-install-cmd": [
"MyNamespace\\Composer\\ScriptHandler::postInstall"
]
}
}php
<?php
declare(strict_types=1);
namespace MyNamespace\Composer;
use Composer\Script\Event;
class ScriptHandler
{
public static function postInstall(Event $event): void
{
$composer = $event->getComposer();
$io = $event->getIO();
$io->write('<info>正在执行 post-install 操作...</info>');
// 获取 extra 配置
$extra = $composer->getPackage()->getExtra();
$webDir = $extra['web-dir'] ?? 'public';
// 执行自定义逻辑
$dirs = ['logs', 'cache', 'uploads'];
foreach ($dirs as $dir) {
$path = $webDir . '/' . $dir;
if (!is_dir($path)) {
mkdir($path, 0755, true);
$io->write(sprintf(' 创建目录: %s', $path));
}
}
$io->write('<info>post-install 操作完成</info>');
}
public static function postUpdate(Event $event): void
{
$io = $event->getIO();
$io->write('<info>正在执行 post-update 操作...</info>');
self::clearCache();
self::runMigrations();
}
private static function clearCache(): void
{
$cacheDir = __DIR__ . '/../storage/cache';
if (is_dir($cacheDir)) {
$files = glob($cacheDir . '/*');
if ($files !== false) {
array_map('unlink', $files);
}
}
}
private static function runMigrations(): void
{
// 执行数据库迁移
}
}Script Event 对象
php
<?php
declare(strict_types=1);
use Composer\Script\Event;
function myScript(Event $event): void
{
// 获取 Composer 实例
$composer = $event->getComposer();
// 获取 IO 接口(用于输入输出)
$io = $event->getIO();
// 获取当前事件名称
$eventName = $event->getName();
// 判断是否为 dev 模式
$isDevMode = $event->isDevMode();
// 获取包信息
$package = $composer->getPackage();
$name = $package->getName();
$version = $package->getVersion();
// 获取 extra 配置
$extra = $package->getExtra();
// 获取依赖管理器
$repositoryManager = $composer->getRepositoryManager();
$localRepository = $repositoryManager->getLocalRepository();
// IO 操作示例
$io->write('<info>消息</info>');
$io->write('<comment>警告</comment>');
$io->write('<error>错误</error>');
$io->ask('请输入: ', '默认值');
$io->askConfirmation('确认执行? [y/N] ', false);
$io->isVerbose();
}实战示例
场景一:Laravel 项目的脚本配置
json
{
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
}
}场景二:代码质量检查流水线
json
{
"scripts": {
"lint": "php -d error_reporting=E_ALL -d display_errors=Off $(find src -name '*.php')",
"cs-check": "phpcs --standard=PSR12 --colors --report=full src/",
"cs-fix": "php-cs-fixer fix --verbose --diff src/",
"phpstan": "phpstan analyse --memory-limit=512M src/",
"test": "phpunit --colors=always",
"test:coverage": "XDEBUG_MODE=coverage phpunit --coverage-html coverage/",
"test:ci": "phpunit --colors=always --no-coverage --log-junit report.xml",
"check": [
"@lint",
"@cs-check",
"@phpstan",
"@test"
],
"fix-all": [
"@cs-fix",
"@phpstan"
]
}
}场景三:部署脚本
json
{
"scripts": {
"deploy:optimize": [
"composer dump-autoload --no-dev --classmap-authoritative --optimize",
"@php artisan config:cache",
"@php artisan route:cache",
"@php artisan view:cache",
"@php artisan event:cache"
],
"deploy:permissions": [
"chmod -R 755 storage bootstrap/cache",
"chown -R www-data:www-data storage bootstrap/cache"
],
"deploy:migrate": "@php artisan migrate --force",
"deploy:full": [
"@deploy:permissions",
"@deploy:migrate",
"@deploy:optimize"
]
}
}场景四:PHP 脚本处理器
php
<?php
declare(strict_types=1);
// 文件:scripts/check-environment.php
use Composer\Script\Event;
class EnvironmentChecker
{
public static function check(Event $event): void
{
$io = $event->getIO();
$errors = [];
// 检查 PHP 版本
if (version_compare(PHP_VERSION, '8.1.0', '<')) {
$errors[] = "PHP 版本需要 8.1+,当前: " . PHP_VERSION;
}
// 检查必需扩展
$requiredExtensions = ['pdo', 'mbstring', 'ctype', 'json', 'openssl', 'curl'];
foreach ($requiredExtensions as $ext) {
if (!extension_loaded($ext)) {
$errors[] = "缺少必需的 PHP 扩展: {$ext}";
}
}
// 检查目录权限
$writableDirs = ['storage', 'storage/logs', 'storage/framework', 'bootstrap/cache'];
foreach ($writableDirs as $dir) {
$path = realpath(__DIR__ . '/../' . $dir);
if ($path !== false && !is_writable($path)) {
$errors[] = "目录不可写: {$dir}";
}
}
if ($errors !== []) {
foreach ($errors as $error) {
$io->write("<error>{$error}</error>");
}
throw new \RuntimeException('环境检查未通过,请修复上述问题后再试');
}
$io->write('<info>环境检查通过 ✓</info>');
}
}json
{
"scripts": {
"pre-install-cmd": "EnvironmentChecker::check",
"pre-update-cmd": "EnvironmentChecker::check"
}
}注意事项
1. 跳过脚本执行
bash
# 跳过所有脚本(CI/CD 环境中常用)
composer install --no-scripts
# 仅跳过特定脚本
# Composer 不支持跳过单个脚本,可以通过环境变量控制
# 在 PHP 脚本中判断是否跳过
if (getenv('COMPOSER_SKIP_SCRIPTS') !== false) {
return;
}2. 脚本中的命令路径
json
{
"scripts": {
"test": "vendor/bin/phpunit",
"phpstan": "vendor/bin/phpstan analyse src/",
"phpcs": "vendor/bin/phpcs --standard=PSR12 src/"
}
}使用 vendor/bin/ 前缀
在脚本中调用 Composer 安装的二进制文件时,使用 vendor/bin/ 前缀(不带 /),Composer 会在执行时自动替换为正确的平台路径。在 Windows 上 vendor/bin/phpunit 会被正确解析为 vendor\bin\phpunit.bat。
3. 长时间运行的脚本
json
{
"scripts": {
"long-task": "php scripts/long-running-task.php"
}
}php
<?php
// scripts/long-running-task.php
// 设置足够长的超时时间
set_time_limit(0);
ini_set('memory_limit', '512M');4. 脚本执行顺序
json
{
"scripts": {
"pre-install-cmd": [
"echo 'Step 1'",
"echo 'Step 2'"
],
"post-install-cmd": [
"@post-autoload-dump",
"echo 'Step 3'",
"echo 'Step 4'"
]
}
}数组中的脚本按顺序执行,如果某个脚本失败(返回非零退出码),后续脚本不会执行。
最佳实践
1. 命名约定
| 前缀 | 含义 | 示例 |
|---|---|---|
| 无前缀 | 常用命令 | test, lint, fix |
test: | 测试相关 | test:unit, test:integration |
deploy: | 部署相关 | deploy:optimize, deploy:migrate |
db: | 数据库相关 | db:seed, db:migrate |
cs: | 代码风格 | cs:check, cs:fix |
2. 编写幂等脚本
php
<?php
declare(strict_types=1);
// 幂等脚本:多次执行结果相同
class IdempotentScript
{
public static function setup(Event $event): void
{
$io = $event->getIO();
// 好的写法:检查后再操作
if (!file_exists('.env')) {
copy('.env.example', '.env');
$io->write('<info>创建 .env 文件</info>');
} else {
$io->write('<comment>.env 文件已存在,跳过</comment>');
}
}
}3. 提供有用的反馈
json
{
"scripts": {
"build": [
"@echo 正在构建项目...",
"@lint",
"@test",
"composer dump-autoload -o",
"@echo 构建完成!"
]
}
}自定义 echo 命令
Composer 允许使用 @echo 语法输出消息,等价于 echo 命令。
4. CI/CD 友好的脚本
json
{
"scripts": {
"ci": [
"@composer install --no-interaction --prefer-dist",
"@lint",
"@phpstan",
"@test:ci"
],
"test:ci": "phpunit --no-coverage --log-junit report.xml"
}
}下一节
继续学习:性能优化 — 了解如何通过 Composer 优化策略提升项目的加载和运行性能。