如何编写自定义 Composer Plugin?

解读

在国内一线/二线互联网公司的 PHP 岗位面试中,Composer 已不仅是“包管理器”,更是 CI/CD、灰度发布、代码生成、自动加载治理的枢纽。面试官抛出“如何写自定义 Composer Plugin”这一题,核心想验证四点:

  1. 是否真正理解 Composer 事件机制(EventDispatcher)
  2. 能否把公司级需求(如统一补丁、自动注入 SDK、私有源镜像回源)沉淀为可复用插件
  3. 是否掌握插件的打包、发布、版本约束与灰度策略
  4. 是否具备排查“插件未生效”“循环依赖”等线上故障的能力

答题时,切忌只贴代码,而要体现“工程落地 + 性能 + 稳定”三板斧。

知识点

  • Composer 插件接口:Composer\Plugin\PluginInterface 与 Capable 系列接口
  • 事件体系:ScriptEvent、PackageEvent、InstallerEvent 的触发时机与性能差异
  • 插件加载顺序:composer-plugin-api 版本约束、composer.json 中 type = composer-plugin 声明
  • 国内镜像场景:阿里云、腾讯云、华为云镜像缓存穿透与回源策略
  • 私有源鉴权:satis / nexus 的 basic-auth、token、ldap 三种模式
  • 热加载与 OPcache:插件代码更新后如何避免“重启 php-fpm”
  • 调试技巧:composer dump -vvv、COMPOSER_DISABLE_XDEBUG_WARN、composer show -p
  • 安全合规:插件代码需过 SonarQube、Snyk 扫描,禁止动态执行 eval
  • 版本灰度:使用 branch-alias + stability=dev 实现“金丝雀”发布

答案

以下示例实现一个“自动为 Laravel 项目注入公司统一追踪 SDK”的 Composer Plugin,兼顾国内私有源与性能。

  1. 创建项目骨架
mkdir laravel-trace-plugin && cd $_
composer init --name=mycomp/laravel-trace-plugin --type=composer-plugin
  1. 声明依赖与插件入口
    composer.json(关键片段)
"require": {
    "php": ">=7.4",
    "composer-plugin-api": "^2.0"
},
"extra": {
    "class": "MyComp\\Composer\\TracePlugin"
},
"type": "composer-plugin"
  1. 实现插件主类
    src/TracePlugin.php
namespace MyComp\Composer;

use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Composer\Plugin\Capable;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;

class TracePlugin implements PluginInterface, Capable, EventSubscriberInterface
{
    private $composer;
    private $io;

    public function activate(Composer $composer, IOInterface $io)
    {
        $this->composer = $composer;
        $this->io       = $io;
    }

    public function deactivate(Composer $composer, IOInterface $io)
    {
        // 清理临时文件,防止 inode 耗尽
    }

    public function uninstall(Composer $composer, IOInterface $io)
    {
        // 可选:回滚注入的 SDK 配置
    }

    public function getCapabilities()
    {
        return [
            'Composer\Plugin\Capability\CommandProvider' => 'MyComp\Composer\TraceCommandProvider',
        ];
    }

    public static function getSubscribedEvents()
    {
        return [
            ScriptEvents::POST_AUTOLOAD_DUMP => 'injectTraceSdk',
        ];
    }

    public function injectTraceSdk(Event $event)
    {
        $vendorDir = $this->composer->getConfig()->get('vendor-dir');
        $target    = $vendorDir . '/../bootstrap/app.php';

        if (!is_file($target)) {
            $this->io->write('<info>非 Laravel 项目,跳过 SDK 注入</info>');
            return;
        }

        $code = <<<'PHP'
// 由 composer-plugin 自动注入,禁止手工修改
if (env('TRACE_ENABLE', true)) {
    \MyComp\Trace\Agent::init(config('trace'));
}
PHP;

        $content = file_get_contents($target);
        if (strpos($content, 'MyComp\Trace\Agent') !== false) {
            $this->io->write('<info>SDK 已注入,跳过</info>');
            return;
        }

        // 在 return $app 之前注入
        $content = preg_replace(
            '/(return \$app;)/',
            $code . "\n\n" . '$1',
            $content
        );

        file_put_contents($target, $content);
        $this->io->write('<info>追踪 SDK 注入完成</info>');
    }
}
  1. 打包与发布到私有 Satis
# satis.json
{
  "name": "MyComp Private Repository",
  "homepage": "https://packages.mycomp.cn",
  "repositories": [
    {"type": "vcs", "url": "ssh://git@git.mycomp.cn/php/laravel-trace-plugin.git"}
  ],
  "require-all": true
}

执行

satis build satis.json public/
rsync -avz public/ root@packages.mycomp.cn:/data/packages/
  1. 业务项目使用
    在项目 composer.json 添加
"repositories": [
    {"type": "composer", "url": "https://packages.mycomp.cn"}
],
"require": {
    "mycomp/laravel-trace-plugin": "^1.0"
},
"config": {
    "allow-plugins": ["mycomp/laravel-trace-plugin"]
}

执行

composer install --no-dev -vvv

可在输出日志中看到“追踪 SDK 注入完成”,证明插件生效。

  1. 灰度与回滚
    通过 branch-alias 把 1.x-dev 指向主干,CI 中指定
composer require mycomp/laravel-trace-plugin:1.x-dev --no-dev

若线上异常,立即

composer require mycomp/laravel-trace-plugin:^1.0 --no-dev

即可回滚到最新稳定 tag,整个过程无需重启 php-fpm。

拓展思考

  1. 性能优化:插件代码常驻内存,需避免在 activate 阶段做 IO 密集操作;可延迟到首次事件触发再初始化。
  2. 横向扩展:如果公司采用容器化部署,可把插件注入逻辑改为“写入 Dockerfile 层”,避免每次 composer install 都重复 patch。
  3. 安全审计:在插件中禁止执行远程下载或动态 eval;建议接入内部 SCA 平台,对 composer.lock 进行每日漏洞扫描。
  4. 多版本并行:利用 composer-plugin-api ^2.0 与 ^1.0 的差异,维护两条分支,确保老项目平滑升级。
  5. 故障演练:定期在测试集群模拟“私有源 502”场景,验证插件的降级策略(如缓存本地副本、跳过注入)。