Skip to content

Scripts 钩子

Composer 的脚本系统允许你在安装、更新、卸载等生命周期事件中自动执行自定义命令。通过预定义事件钩子(Pre/Post 事件)和自定义脚本命令,你可以将代码检查、测试运行、文件权限设置等常见任务自动化。本节将详细介绍 Composer 脚本的各种配置方式和实用场景。

前置知识

阅读本节前,建议先了解:

基础概念

脚本运行时机

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-cmdinstall 命令执行前环境检查
post-install-cmdinstall 命令执行后权限设置、缓存清理
pre-update-cmdupdate 命令执行前数据库备份
post-update-cmdupdate 命令执行后测试运行
pre-status-cmdstatus 命令执行前
post-status-cmdstatus 命令执行后
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 在每次 installupdaterequireremove 后都会触发。这是执行依赖包发现的理想时机。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 优化策略提升项目的加载和运行性能。

参考链接