插件不能声明“^1.0 || ^2.0”,必须指定单一 composer-plugin-api 版本;真兼容需运行时检测 class_exists() 分支适配,README 明确标注最低 Composer 版本及能力限制,并通过双环境 CI 验证。

插件声明里写 "^1.0 || ^2.0" 会直接失败
这种写法看似兼容,实则让 Composer 自身校验直接报错:Plugin X is not compatible with Composer 2 或安装时提示 composer-plugin-api 版本不满足。Composer v1 和 v2 的插件激活机制、事件类路径、activate() 方法签名完全不同,不是语义化版本能桥接的。
必须在 composer.json 的 require 中只写一个明确版本:
- 要支持 PHP 7.2 + Composer 1 环境 → 固定用
"composer-plugin-api": "^1.1.0" - 想用 v2 的
CommandProvider能力 → 只能升到"composer-plugin-api": "^2.2.0"
运行时检测 class_exists() 才是真兼容
所谓“兼容”,实际是你代码里主动判断当前环境,再走不同分支。不能在构造函数或静态块里直接 new v2 特有类,否则 v1 下直接 fatal error。
典型做法是在 activate() 入口做探测:
public function activate(Composer $composer, IOInterface $io)
{
if (class_exists('Composer\Script\Event')) {
// Composer 1:监听 pre-install-cmd 等旧事件
$composer->getEventDispatcher()->addListener('pre-install-cmd', [$this, 'onPreInstall']);
} else {
// Composer 2:注册 capability,如 CommandProvider
$composer->getPluginManager()->addCapability($this, new MyCommandProvider());
}
}
所有对 Composer\Package\Link、Package::getNamesWithStability() 等内部类/方法的调用,都得先 class_exists() 或 method_exists() 判断。
文档和 README 里必须写清运行时限制
用户不会去翻你的源码判断兼容性。README 里要直白说明:
- 最低支持 Composer 版本(如 “Requires Composer ≥ 2.2”)
- 是否支持降级运行(如 “可在 Composer 1 环境下加载但不执行命令注册”)
- 关键能力的环境依赖(如 “
CommandProvider仅在 Composer 2.2+ 生效”)
别写 “兼容 Composer 1 & 2”,而要写 “通过运行时检测适配不同 API 路径”。后者才是用户能验证、能 debug 的事实。
测试必须覆盖双环境 CI 流水线
光本地跑通没用。CI 配置里至少要跑两套:
- PHP 7.4 + Composer 1.x(用
composer self-update --1锁定) - PHP 8.1 + Composer 2.x(默认或
composer self-update --2)
每套都执行 composer install + 插件激活日志检查 + 关键功能断言。否则你写的 class_exists() 分支永远不知道有没有被真实触发过。
最容易被忽略的是:v1 环境下 CommandProvider 类不存在,但你又在插件类顶层 use 了它——这会导致解析失败,根本进不到 activate()。必须把 v2 相关类的引用全部移到条件分支内部,或者用字符串类名延迟加载。


















