Composer插件无法通过composer-plugin-api版本声明实现v1/v2兼容,必须在activate()中用class_exists()等运行时探测分路径加载逻辑。

Composer 插件 API 版本声明根本不能“兼容” v1 和 v2
声明 "composer-plugin-api": "^1.0 || ^2.0" 在 composer.json 里,只会让 Composer 自身拒绝安装——v1 不认识 ^2.0 的语义,v2 更会直接报错 Plugin is not compatible with Composer 2。这不是配置写法问题,是插件机制底层断裂导致的硬性限制。
真正可行的路径只有一条:代码里做运行时探测,而不是靠 JSON 声明“假装兼容”。必须在 activate() 入口处,用 class_exists() 和 method_exists() 判断当前环境能力,再分路径加载逻辑。
- 不要在
__construct()或静态块中 new v2 特有类(如Composer\Plugin\Capability\CommandProvider),否则 v1 下直接 fatal error - 避免调用已移除方法,例如
Package::getNames()(v2 改为getNamesWithStability()) - 所有对
Composer\Package\Link等内部类的引用,都得先class_exists()判断
为什么 require 中只能写一个明确的 composer-plugin-api 版本
Composer 在加载插件前会校验 composer-plugin-api 版本是否匹配当前运行时。它不走语义化版本的宽松解析逻辑,而是严格比对 major 版本号。v1 只认 ^1.1.0,v2 只认 ^2.2.0(或更高 v2.x 小版本),中间没有交集。
这意味着:目标支持老项目(PHP 7.2 + Composer 1)?require 里只能写 "composer-plugin-api": "^1.1.0";想用 v2 的 capability 注册、新事件系统?就必须升到 "composer-plugin-api": "^2.2.0",且放弃 v1 用户。
所谓“兼容”,实际是你自己承担适配成本,不是 Composer 替你兜底。
常见错误现象与对应排查点
遇到 Class 'Composer\Plugin\Capability\CommandProvider' not found,基本可断定是 v1 环境下执行了 v2 专属代码——没做 class 存在性判断就直接引用。
看到 Plugin X is not compatible with Composer 2,先检查 composer.json 的 require 字段:是不是写了双版本范围?是不是用了 v1 的事件类路径(如 Composer\Script\Event)却在 v2 下运行?
- 错误日志里出现
activate() expects 2 parameters, 1 given→ v1 的activate(Composer $composer, IOInterface $io)被 v2 调用,说明插件被误装进 v2 环境但未适配 - 插件注册的命令在 v2 下不出现 → 没走 capability 注册路径,仍试图用 v1 的事件监听方式绑定命令
- v2 下
$composer->getEventDispatcher()返回 null → v2 已废弃该接口,必须改用 plugin manager + capability
实际开发中容易被忽略的细节
最隐蔽的问题不是逻辑分支写错,而是 autoloader 加载顺序和类名解析时机。v1 和 v2 的插件类自动加载路径可能不同,尤其当插件同时声明了 PSR-4 和 classmap 时。
另一个高频坑:在 v2 环境下测试通过,但上线后 v1 用户报错,往往是因为 CI/CD 流水线默认用 v2 安装插件,而本地开发机或客户服务器仍是 v1 —— 没有强制在测试矩阵里跑双版本环境。
别依赖文档里的“向后兼容”描述。Composer 官方文档从没承诺 v1/v2 插件 API 兼容,所有“兼容方案”都是社区倒推出来的运行时降级模式,稳定性取决于你写的探测逻辑够不够细、够不够早。











