Composer 1 和 2 的插件无法统一兼容;声明 "composer-plugin-api": "^1.0 || ^2.0" 无效,因 v2 拒绝安装、v1 找不到新类而 fatal error,必须通过 class_exists() 和 method_exists() 运行时检测分路径加载,且 composer.json 中只能声明一个明确版本。

Composer 1 和 2 的插件根本不能“统一兼容”
直接说结论:composer-plugin-api 版本声明写 "^1.0 || ^2.0" 是无效的。Composer 2 会拒绝安装,Composer 1 会因找不到新类而 fatal error。这不是语义化版本能兜住的事——v1 的 activate(Composer $composer, IOInterface $io) 和 v2 的事件注册机制、Capability 接口、内部类路径(如 Composer\Plugin\Capability\CommandProvider)全都不一样。
必须用运行时检测分路径加载
所有真正跨版本工作的插件(比如 dealerdirect/phpcodesniffer-composer-installer 或 cweagans/composer-patches)都靠这个:在 activate() 入口里主动判断环境,而不是依赖 Composer 自动适配。
- 先检查
class_exists('Composer\Script\Event')—— 存在就是 Composer 1 - 再查
method_exists($composer, 'getPluginManager')—— 存在大概率是 Composer 2 - 所有对
Composer\Package\Link、Package::getNamesWithStability()这类 v2 新增/改名方法的调用,都得加class_exists()或method_exists()守卫 - 绝不在
__construct()里 new v2 特有类,否则 Composer 1 下直接报错退出
composer.json 里只能声明一个明确的 plugin-api 版本
你得在 require 里二选一:"composer-plugin-api": "^1.1.0" 或 "^2.2.0"。声明双版本范围会导致 Composer 自身校验失败,连 composer install 都跑不起来。
- 目标支持 PHP 7.2 + Composer 1 的老项目 → 锁死
^1.1.0 - 要用
post-autoload-dump以外的新事件(如pre-file-download)→ 必须升到^2.2.0 - 想让 CI 同时跑 v1/v2?别指望插件自动切换——得靠 CI 脚本分别装不同插件版本,或用条件 require(但 composer.json 不支持)
最容易被忽略的点:插件激活时机和 vendor 缓存
即使代码逻辑正确,也常因这两点失效:
- Composer 2.2+ 默认禁用未声明的插件,首次安装时会问
Do you trust "dealerdirect/phpcodesniffer-composer-installer" to execute code?—— 必须输y,否则插件类根本不加载 - 已存在
vendor/和composer.lock的项目,改完插件代码后必须rm -rf vendor composer.lock && composer install,否则旧缓存包跳过插件钩子











