conflicts 字段防运行时兼容问题,如类重写、事件监听器冲突等;需写具体版本约束才生效,如"conflicts": {"phpunit/phpunit": ">=10.0.0"}。

conflicts 字段到底防什么
conflicts 不阻止安装,只在 composer install 或 composer update 时检查已解析出的依赖图——如果某个被声明为冲突的包最终被拉进依赖树(无论直接还是间接),Composer 就会中止并报错:Your requirements could not be resolved to an installable set of packages.
它防的是「运行时才暴露的兼容问题」,比如两个包都重写了同一个类、都注册了同名的 Symfony 事件监听器、或都修改了全局 $GLOBALS。这类问题不会在 autoload 阶段报错,但上线后突然崩。
怎么写 conficts 才真正生效
必须写具体版本约束,不能只写包名。写成 "conflicts": {"monolog/monolog": "*"} 看似激进,但实际几乎没用——因为 Composer 默认允许 * 匹配任意版本,而冲突检测是精确比对已安装版本是否落在该约束范围内。
- ✅ 正确:只封掉已知不兼容的版本区间,例如
"conflicts": {"phpunit/phpunit": ">=10.0.0 - ✅ 正确:封死整个大版本(如果确认全不兼容),例如
"conflicts": {"guzzlehttp/guzzle": "^7.0"} - ❌ 错误:只写包名
"conflicts": {"laravel/framework": {}}—— 这会被忽略 - ❌ 错误:用
*当通配符封全部版本,"conflicts": {"symfony/console": "*"}—— Composer 不认这种写法,等价于没写
conflicts 和 replace、require 的关键区别
conflicts 是纯声明式防御,不改变依赖解析逻辑;replace 会从依赖图中“抹掉”被替换的包(常用于 fork 替代);require 是主动拉取。三者混用极易翻车。
- 如果你用
replace声明替代了some/package,再在conflicts里写它,Composer 会警告:「You cannot conflict with a package you replace」 - 如果你在
require里锁了"foo/bar": "1.2.3",又在conflicts里写"foo/bar": "^1.2",Composer 会直接拒绝解析——哪怕1.2.3明确落在冲突范围内 - 冲突检测发生在所有
require解析完成之后,所以它无法阻止你主动require一个自己声明冲突的包
CI 里必须跑 composer update --dry-run
conflicts 只在依赖解析阶段触发,本地开发时可能因缓存、旧 lock 文件或手动 require 绕过检测。CI 流程里光跑 composer install 不够。
必须加一步:
composer update --dry-run --no-interaction。这会让 Composer 强制重新解析整个依赖图,真实模拟新环境首次安装场景,把隐藏的冲突暴露出来。
容易被忽略的一点:如果项目用了 platform-check 或自定义 config.platform,--dry-run 仍会受其影响——得确保 CI 的 platform 配置和目标部署环境一致,否则冲突可能漏检。











