答案是运行composer why-not vendor/package:version定位阻塞链——输出倒序显示依赖层级,最后一行是根require,往上每行“(required by)”即封杀源头。

composer why-not 能直接定位冲突源头
报错里出现 don't install xxx 时,别靠猜,composer why-not 是唯一能直接告诉你“谁在拦路”的命令。它不是静态分析,而是让 Composer 模拟安装失败路径,输出一条反向依赖链——最后一行是你 composer.json 里的声明,往上逐行是哪个包(及其版本约束)提出了互斥要求。
必须带完整版本号,比如:composer why-not guzzlehttp/guzzle:^8.0.0。写 ^8.0 或 8 很可能匹配不到元数据,返回空或报错。
常见误判点:
- 输出为空?先检查
require-dev—— 很多冲突来自phpunit/phpunit或mockery/mockery拖着老版sebastian/exporter,间接锁死symfony/console - 命令执行前没更新
composer.json?它只分析当前已声明但无法满足的约束,不是预测未来 - 看到
[InvalidArgumentException] Package not found?说明该包根本没在composer.json或composer.lock里注册过,why-not对它无意义
composer show --tree 揭露真实依赖结构
composer.json 是你写的愿望清单,composer show --tree 才是 vendor/ 和 composer.lock 里的真实快照。它能暴露你根本没意识到的间接路径,比如某个测试工具悄悄引入了不兼容的 psr/log 版本。
关键信息全在括号里:
-
(locked to 5.4.42)→ 这个版本已被composer.lock固定,删 lock 文件再update才可能松动 -
(replaced)或(provided)→ 得去那个包自己的composer.json里确认它到底替代了什么、是否真能覆盖 - 过滤查看:比如
composer show --tree monolog/monolog | grep -A5 -B5 "guzzlehttp/guzzle",快速定位交叉点
composer prohibits 查谁在阻止特定版本
当你已经知道想装哪个版本(比如 monolog/monolog:2.9.0),但不确定谁在卡住,composer prohibits 比 why-not 更聚焦。它列出所有明确声明与该版本冲突的上游组件,包括 require、conflict 和 replace 字段。
用法简单但参数不能少:
-
composer prohibits monolog/monolog:2.9.0→ 查所有禁止该版本的包 -
composer prohibits -r monolog/monolog:2.9.0→ 加-r递归查 transitive dependencies,否则只显示直接 require 的包 - 注意看输出里带
requires的行才是真正的约束来源;provides或replaces不构成阻塞
--dry-run 是验证改动是否真能解出路径的唯一方式
任何升级或降级操作前,必须加 --dry-run。它不改任何文件,只让 Composer 跑一遍 SAT 求解器,提前告诉你能不能解出路径、会动哪些包、有没有新冲突。
典型场景:
-
composer require laravel/framework:11.0.0 --dry-run→ 看是否真能拉进 Laravel 11 -
composer update monolog/monolog --with-dependencies --dry-run→ 验证加了--with-dependencies后是否还卡住 - 如果
--dry-run失败,说明约束本身矛盾,得回退到why-not或prohibits再挖一层
真正容易被忽略的是:很多团队跳过这步,直接 update,结果在 CI 上失败才发现要回滚——而 --dry-run 在本地 2 秒就能给出确定答案。


















