conflict 字段仅在 install/update 时校验版本冲突,不清理已安装包;它非运行时防护,也不控制卸载。仅当依赖解析结果与 conflict 规则重叠时才报错,且必须使用语义化版本语法,分支名、* 等无效;require 与 conflict 共存时交集为空则直接失败;platform 配置可能掩盖真实环境冲突。

conflict 字段只在 install/update 时校验,不清理已安装包
它不是运行时防护,也不是卸载开关。你写了 "conflict": {"guzzlehttp/guzzle": "^7.5"},但项目里已经装了 guzzlehttp/guzzle 的 7.4.5 版本?完全没事,不会报错,也不会自动删掉。
只有当你执行 composer require some/package,而这个操作最终导致 Composer 解析出 guzzlehttp/guzzle 的 7.5.0 或更高版本时,才会中断并报错。
- 已锁在
composer.lock中的旧版本 → 不触发校验 - 手动
composer require guzzlehttp/guzzle:7.4.5成功 → 不校验 - 执行
composer update后解析出7.5.2→ 立刻失败
必须用标准语义化版本语法,分支名和 * 都无效
"conflict": {"laravel/framework": "dev-main"} 这种写法等于没写——Composer 根本不认分支名、dev- 前缀、main、stable 这类标识,只匹配真实版本号(如 10.25.0、9.51.0)。
常见错误写法和修正:
-
"monolog/monolog": "*"→ 拦所有版本,太暴力;真要禁用,明确写"monolog/monolog": "" -
"php": "8.0"→ 不严谨,应写"php": "8.0.*"或"php": ">=8.0.0, -
"ext-json": ">=1.0"→ 合法,平台扩展也支持同样语法
conflict 和 require 共存时,交集为空就直接失败
Composer 把 require 和 conflict 一起喂给 SAT 求解器做逻辑判断。不是“谁优先”,而是“能不能同时满足”。
例如:
"require": {"symfony/console": "^6.2"},
"conflict": {"symfony/console": ">=6.3.0"}
这组配置必然失败,因为 ^6.2 包含 6.3.0 及以上版本,和 conflict 范围重叠。
- Composer 不会自动降级到
6.2.12,也不提示替代方案 - 也不会绕开冲突去选另一个依赖路径——它只做判断,不做策略选择
- 解决办法只有两个:放宽
require(比如改成"^6.2.0"),或删掉这条conflict
platform 冲突受 config.platform 影响,本地环境可能被掩盖
你写了 "conflict": {"php": ">=8.2.0"},但 composer.json 里同时有:
"config": {
"platform": {
"php": "8.1.0"
}
}
那这条冲突就失效了——Composer 会假装运行在 PHP 8.1,不再检查真实 CLI 版本是否为 8.3。
这意味着你在本地测试通过,CI 环境却可能因真实 PHP 版本触发冲突。验证时务必确认:config.platform 是否干扰了你的意图。
真正容易被忽略的是:conflict 规则本身不带上下文,它只响应“当前解析路径下某个包的最终版本号”,而不是“这个包有没有被用到”或者“代码里有没有调用它的某段 API”。写得太宽,会拦住不该拦的;写得太窄,又起不到作用。











