答案是运行 composer why-not vendor/package:version 定位阻塞链——它从根依赖逆推,逐层显示谁锁定了冲突版本,最上面一行通常是 composer.json 中过窄的手动约束或不兼容的中间包。

composer why-not 能直接定位阻断点,别等报错再查
报错信息里出现 don’t install xxx 或 found x packages that conflict 时,真正要盯的不是错误文字,而是谁在哪个环节卡住了目标版本。运行 composer why-not vendor/package:version(比如 composer why-not guzzlehttp/guzzle:^8.0)会输出一条从根 composer.json 往上倒推的链路,最后一行是你写的约束,往上每行的 (required by) 就是实际提互斥要求的地方。
常见误区:
- 用
composer depends查“谁依赖我”,它不带版本号,看不出约束冲突 - 只看
composer.json里的require,忽略require-dev—— 很多冲突来自phpunit/phpunit或infection/infection拉进来的老版sebastian/exporter - 写
composer why-not monolog/monolog:^2却返回空:必须用完整版本号,如monolog/monolog:2.0.0,否则匹配不到元数据
composer show --tree 揭露真实依赖结构,不是你“想装”的样子
composer show --tree 显示的是 composer.lock 里已解析出的快照,不是 composer.json 里写的愿望清单。它能暴露你根本没意识到的间接路径,比如某个包被两个不同分支的组件分别引入,版本却不一样。
实操建议:
- 查某包是否被锁死:
composer show --tree | grep "symfony/console",留意括号里标着的(locked to 5.4.42) - 聚焦关键路径:
composer show --tree monolog/monolog | grep -A5 -B5 "guzzlehttp/guzzle",快速定位跨包影响范围 - 看到
(replaced)或(provided)标记,得立刻去那个包自己的composer.json里确认它到底替换了什么、提供了哪些能力——运行时出错往往就在这里
composer update 必须带 --with-dependencies,否则子依赖不动
升级单个包却不更新它的直系依赖,等于装了个新壳配旧引擎。比如执行 composer update monolog/monolog --with-dependencies 才会同步调整它依赖的 psr/log 和 php 约束;而只写 composer update monolog/monolog,Composer 默认拒绝动其子依赖,哪怕新版本根本跑不起来。
几个关键细节:
- 错误写法:
composer update "monolog/monolog:^3"—— 引号 +^会让 Composer 自己找“最新兼容版”,不是你要的3.0.0 - 降级跨主版本(如从
guzzlehttp/guzzle:^8.0切回7.4.5),必须在composer.json里写死"guzzlehttp/guzzle": "7.4.5",不能只写"^7.0" - 执行后立刻
git diff composer.lock,确认只有目标包及其直系依赖被改,没波及其他——这是验证是否“定点更新”成功的唯一依据
循环依赖不是版本问题,得靠 composer depends --tree 破环
如果 composer install 卡在 Resolving dependencies、CPU 拉满、无网络请求,基本就是循环依赖,不是版本冲突。这时删 vendor 或 composer.lock 没用,因为问题不在版本号,而在包之间互相引用的逻辑闭环。
破环只能靠结构重构:
- 运行
composer depends --tree myorg/core,若输出中出现myorg/core ← myorg/api ← myorg/core这类箭头闭环,就是真实循环点 - 临时注释掉
composer.json中所有非核心的require-dev条目,再跑composer update --dry-run—— 如果不再卡住,问题就藏在 dev 包的 autoload 配置里(比如写了"../src/") - 破环只有三种有效方式:抽离契约包(建
myorg/contracts)、拆分单体包(core-api和core-domain分开)、或移除反向 autoload 映射——没有--force或--skip-cycle这种开关
依赖链混乱最麻烦的点在于:它常混着版本冲突和循环依赖一起发生,而两者排查路径完全不同。先用 why-not 排查版本阻断,再用 depends --tree 看有没有闭环,顺序错了就容易在原地打转。


















