必须声明唯一版本,因Composer 1和2插件API不兼容:激活签名、事件类路径、能力模型均不同,校验失败即中断安装;双版本声明如"^1.0 || ^2.0"会被直接拒绝。

不能写 "composer-plugin-api": "^1.0 || ^2.0" —— 这种声明看似兼容,实则会让 Composer 2 直接拒绝安装,而 Composer 1 又无法解析 v2 的 capability 机制,根本走不通。
为什么 composer-plugin-api 版本声明必须唯一
Composer 1 和 2 的插件 API 是两套不兼容的体系:激活方法签名不同(activate(PluginInterface $plugin, Composer $composer) vs activate(Composer $composer, IOInterface $io))、事件类路径变了(Composer\Script\Event → Composer\Plugin\Capability\CommandProvider)、插件注册时机和能力模型也重构了。Composer 自身在加载插件前就会校验 composer-plugin-api 声明是否匹配当前运行版本,不匹配就中断,不会给你运行时兜底的机会。
常见错误现象包括:
Plugin X is not compatible with Composer 2-
Class 'Composer\Plugin\Capability\CommandProvider' not found(在 Composer 1 环境下) - 执行
composer install时直接报错退出,不提示具体原因
应该在 composer.json 中怎么声明
必须只选一个明确的版本范围,并与目标环境对齐:
- 若需支持 PHP 7.2+ 且项目仍用 Composer 1(如遗留 CI 或旧 Docker 镜像),就写
"composer-plugin-api": "^1.1.0" - 若明确要求 Composer 2.2+(比如要用
CommandProvider或新事件系统),就写"composer-plugin-api": "^2.2.0" - 绝不能写
"^1.0 || ^2.0"、"*"或留空——这些都会被 Composer 拒绝
这个字段只起“准入校验”作用,不是功能开关。真正的兼容逻辑必须下沉到 PHP 代码里做运行时判断。
运行时如何安全降级处理
在插件主类的 activate() 方法中,用 class_exists() 和 method_exists() 探测环境,再分路径初始化:
public function activate(Composer $composer, IOInterface $io)
{
if (class_exists('Composer\Script\Event')) {
// Composer 1:监听旧事件
$composer->getEventDispatcher()->addListener('pre-install-cmd', [$this, 'onPreInstall']);
} else {
// Composer 2:注册 capability
$composer->getPluginManager()->addCapability($this, new MyCommandProvider());
}
}
关键约束:
- 不要在
__construct()或静态块里 new v2 特有类(如CommandProvider),否则 Composer 1 下直接 fatal error - 避免调用已移除方法,例如
Package::getNames()(v2 改为getNamesWithStability()) - 所有对
Composer\Package\Link等内部类的引用,都得先class_exists()判断
allow-plugins 配置别漏掉
即使插件代码兼容双版本,用户安装时仍可能被 Composer 2.2+ 的安全策略拦截。必须确保项目 composer.json 的 config.allow-plugins 显式启用你的包名:
"config": {
"allow-plugins": {
"yourvendor/your-plugin": true
}
}
漏配会导致安装成功但插件完全不生效,且无任何错误提示——这是最隐蔽的坑。别依赖全局配置,每个项目单独声明更可控。
真正麻烦的从来不是写两套逻辑,而是忘记在 activate() 入口做 class 检查,或在 composer.json 里偷偷加了双版本声明还觉得“语义化版本理应兼容”。Composer 不会帮你圆场,它只认规则。


















