必须实现 PluginInterface,否则 Composer 静默跳过插件;需声明 "type": "composer-plugin" 并正确 use 接口,activate() 中仅注册事件、保存实例,不执行业务逻辑,extra 配置须空合并处理,Windows 下用 copy 替代 symlink。

必须实现 PluginInterface,否则 Composer 完全不会识别你的类为插件——连错误提示都不会有,只会静默跳过。
为什么 PluginInterface 是硬性门槛
Composer 不靠文件名、目录结构或注释识别插件,只认两件事:是否声明 "type": "composer-plugin",以及加载到的类是否真正实现了 ComposerPluginPluginInterface。漏掉 implements PluginInterface,或者只写了接口名但没加 use 语句导致命名空间解析失败,结果都一样:插件逻辑从不执行。
常见误判是以为“类放对位置 + autoload 配好 = 插件就活了”,其实 autoload 只解决类能找到,不解决“Composer 是否把它当插件用”。PluginInterface 是唯一开关。
- 接口定义在
composer-plugin-api包中,不是 Composer 自带的——必须显式require对应版本 -
activate()和deactivate()是强制方法,不能缺;后者可空实现,但签名必须完整 - 类不能是匿名类,也不能在
__construct()中抛异常(否则插件加载失败且无提示) - 如果用了
EventSubscriberInterface,getSubscribedEvents()返回的事件名必须拼写准确,比如ScriptEvents::POST_INSTALL_CMD,写成post-install-cmd字符串会失效
activate() 里该做什么、不该做什么
activate() 是插件唯一确定会被调用的入口,但它不是业务逻辑的终点,而是注册点。你在这里拿到 $composer 和 $io 实例,主要任务是绑定事件监听器、保存上下文引用,而不是直接写文件、调外部命令或读用户配置。
- ✅ 做:
$this->composer = $composer保存实例供回调使用;$dispatcher->addListener('post-autoload-dump', [$this, 'onAutoloadDump']) - ❌ 不做:在
activate()里调$composer->getPackage()->getExtra()并假设字段一定存在——用户项目可能根本没配extra,直接报Notice: Undefined index或更糟的Fatal error - ⚠️ 注意:
$io在非交互环境(如 CI)下可能不可写,避免在activate()里调$io->ask()这类阻塞操作 - 调试时可在开头加
file_put_contents('/tmp/activated', date('c')),确认是否真进来了
extra 配置读取和 fallback 的实际写法
用户项目通过 composer.json 的 extra 字段传参,这是插件获取定制化行为的唯一合法途径。但 getExtra() 返回的是原始数组,没有默认值、不做类型校验、也不保证键存在。
正确读取方式不是:
$config = $composer->getPackage()->getExtra()['myplugin'];
而是:
$extra = $composer->getPackage()->getExtra();<br>$config = $extra['myplugin'] ?? [];<br>$enabled = $config['enabled'] ?? true;<br>$outputDir = rtrim($config['output-dir'] ?? 'generated/', '/');
- 永远用
?? []防止getExtra()返回null或缺失键 - 字符串路径记得
rtrim(),避免后续拼接出现双斜杠 - 布尔开关别用
empty()判断——false、0、''都会误判,明确用=== true或filter_var($val, FILTER_VALIDATE_BOOLEAN) - 敏感信息(如 API token)绝不能放
extra,它会进composer.lock提交到仓库
Windows 下资源发布必须 fallback 到 copy
如果你的插件要“发布”文件(比如把模板复制到项目根目录),别默认用 symlink()。Windows 普通用户权限下无法创建符号链接,而 Composer 不会帮你捕获 ErrorException,直接中断命令。
- 先判断:
if (DIRECTORY_SEPARATOR === '\' || !function_exists('symlink')) - 再选择:
copy($source, $target)而不是symlink($source, $target) - 发布前务必
file_exists($target)检查,避免覆盖用户已修改的配置;可加--force-publish开关由用户决定是否覆盖 - 路径必须动态获取:
$vendorDir = $composer->getConfig()->get('vendor-dir'),硬写vendor/在自定义路径时必挂
最易被忽略的一点:事件监听器内部如果抛出未捕获异常,整个 composer install 就会退出。所有业务逻辑必须包在 try/catch 里,哪怕只是 catch (Exception $e) { $io->writeError('⚠️ ' . $e->getMessage()); }。











