插件必须声明单一 composer-plugin-api 版本,兼容需在 activate() 中用 class_exists()/method_exists() 运行时判断环境并分支处理,构造函数和静态块严禁依赖版本特性。

composer-plugin-api 版本声明不能写 "^1.0 || ^2.0"
Composer 自身会直接拒绝安装声明了双版本 composer-plugin-api 的插件,不是“运行时报错”,而是根本装不上。错误信息通常是:Plugin X requires composer-plugin-api ^1.0, but you have ^2.2.0 或反过来——v1 环境下报 v2 类找不到。
必须在 composer.json 的 require 里只写一个明确版本:
- 要支持 Composer 1(如 PHP 7.2 项目),只能写
"composer-plugin-api": "^1.1.0" - 要用 Composer 2 的
activate(Composer $composer, IOInterface $io)签名或 Capability 机制,就得写"composer-plugin-api": "^2.2.0"
所谓“兼容”,是代码里自己判断环境,不是靠声明骗过 Composer 校验。
用 class_exists() 和 method_exists() 做运行时分支
在插件主类的 activate() 方法里,不能直接 new v2 特有类(比如 Composer\Plugin\Capability\CommandProvider),否则 Composer 1 下直接 fatal error;也不能调用 v2 已移除的方法(如 Package::getNames())。
正确做法是逐层探测能力:
- 先
class_exists('Composer\Script\Event')→ 存在则是 Composer 1 - 再
method_exists($composer, 'getPluginManager')→ 存在且上一条为 false,则大概率是 Composer 2 - 对
Composer\Package\Link这类内部类的引用,每次都要class_exists()包裹,不能假设它一定存在
示例逻辑片段:
public function activate(Composer $composer, IOInterface $io)
{
if (class_exists('Composer\Script\Event')) {
$composer->getEventDispatcher()->addListener('pre-install-cmd', [$this, 'onPreInstall']);
} elseif (method_exists($composer, 'getPluginManager')) {
$composer->getPluginManager()->addCapability($this, new MyCommandProvider());
}
}
测试用例必须覆盖两个 Composer 主版本环境
单元测试本身跑在 PHPUnit 里,不等于 Composer 环境。真要验证兼容性,得让测试在真实 Composer 1 和 Composer 2 下执行——不是 mock,是实打实调用。
推荐做法:
- CI 中用 matrix 分别启动
composer:1.10和composer:2.5容器(Docker 或 GitHub Actions 的setup-php+ 手动安装指定 Composer) - 每个环境都执行完整流程:
composer install --no-scripts→ 手动触发插件激活逻辑 → 验证生成文件(如src/PackageVersions/Versions.php)是否存在且内容合理 - 特别检查降级路径:比如把
composer.json里require的某个包删掉,看插件是否仍能 fallback 到空数组或默认值,而不是抛出Class not found
注意:不要只测“能跑”,要测“行为一致”。例如 Composer 1 下监听 pre-install-cmd,Composer 2 下注册命令,最终都应生成相同结构的版本映射数组。
避免在 __construct() 或静态块里触发版本敏感逻辑
很多插件会在构造函数里初始化事件监听器、读取配置或 new 内部类,这会导致 Composer 1 环境下加载类时就 crash——因为 __construct() 在插件被 Composer 实例化时立即执行,早于 activate() 的运行时判断。
安全边界只有一条:
- 所有依赖 Composer 版本特性的代码,必须收束到
activate()及其调用的子方法中 -
__construct()里只做无依赖的赋值(如$this->io = $io)或类型无关的参数校验 - 静态属性初始化禁止 new 任何
Composer\*类,禁止访问Composer\Package\*常量
这是最容易被忽略的点:本地开发时可能只用 Composer 2,测试全过;一放到老项目 CI 就挂,而错误堆栈根本看不到你写的业务逻辑,卡在 new Plugin() 这一行。











