composer why-not 是定位依赖冲突的起点,它倒序输出阻塞链,最后一行是 composer.json 根声明,往上每行末尾 (required by package/version) 指向封杀源头;composer show --tree 展示真实依赖结构,暴露间接路径;定点更新必须加 --with-dependencies,否则子依赖不更新易报错。

直接运行 composer why-not vendor/package:version,它会告诉你谁在阻止安装——这才是定位冲突的起点,不是删 vendor 或重生成 composer.lock。
用 composer why-not 挖出阻塞链最上面那行
报错里出现 don’t install monolog/monolog:2.9.0,别只盯着这个包本身;立刻执行 composer why-not monolog/monolog:2.9.0。输出是倒序的:最后一行是你 composer.json 里的根声明,往上每行末尾的 (required by package/version) 就是封杀源头。
- 如果第一行显示
Root package requires php: 7.4,说明你写死了 PHP 版本,而新包已放弃支持 —— 改composer.json中的"php"字段,或换 PHP 二进制执行 - 如果某行卡在
spatie/laravel-backup 7.2.0,去它的 Packagist 页面查是否已有支持 Laravel 11 的新版;别急着删它 - 输出为空?检查
require-dev—— 很多时候是phpunit/phpunit通过sebastian/exporter间接锁死了symfony/console
用 composer show --tree 看真实依赖结构,不是“愿望清单”
composer.json 是你写的愿望,composer.lock 和 vendor/ 才是实际装了什么。composer show --tree 输出的是后者的真实快照,能暴露你根本没意识到的间接路径。
- 查某个包被谁拉进来:
composer show --tree | grep "guzzlehttp/guzzle",注意看后面是否标着(locked to 7.4.5) - 过滤关键段更高效:
composer show --tree monolog/monolog | grep -A5 -B5 "psr/log",确认psr/log是否被不同版本同时引用 - 看到
(replaced)或(provided)标记?得去那个包自己的composer.json里查它到底提供了哪些虚拟包
定点更新必须带 --with-dependencies,否则大概率失败
composer update monolog/monolog 不加参数,Composer 默认拒绝更新其子依赖。哪怕 monolog/monolog v3 明确要求 psr/log ^3.0,它也不会动 psr/log —— 结果就是报错“无法满足要求”。
- 升版本正确操作:
composer update monolog/monolog --with-dependencies - 降主版本(比如从
guzzlehttp/guzzle:^8.0切回7.4.5):必须先在composer.json里写死"guzzlehttp/guzzle": "7.4.5",再跑composer update guzzlehttp/guzzle --with-dependencies - 永远加
--dry-run预览:composer update monolog/monolog --with-dependencies --dry-run,确认只有目标包及其直系依赖被改,没波及其他
别信 composer depends,它不体现版本限制
composer depends monolog/monolog 只回答“谁依赖我”,但不告诉你“谁要求哪个版本”。它对冲突诊断基本没用。
- 真正该用的是
composer why-not和composer show --tree -
composer prohibits monolog/monolog倒是有用,但它只列出明确声明"conflict"的包,覆盖场景有限 - 想验证某个版本是否真能解出:
composer require laravel/framework:11.0.0 --dry-run,比瞎猜靠谱得多
复杂点在于:同一个包可能被 require、require-dev、甚至 autoload 隐式引入三条不同路径,而每条路径都带着自己的版本约束。这时候只看 composer.json 就会漏掉关键封杀源。











