composer why-not 是唯一能定位阻塞链的命令,它不依赖 composer.lock 或缓存,直接从元数据反向推导谁阻止安装某版本,输出从下往上读,最后一行是根声明,以上每行标注 (required by ...) 的包即冲突来源。

composer why-not 是唯一能定位阻塞链的命令
它不依赖 composer.lock 或缓存,直接从元数据中反向推导「谁在阻止你装某个版本」。报错里出现 don’t install laravel/framework v11.0.0,就立刻跑:
composer why-not laravel/framework:11.0.0
输出从下往上读:最后一行是你 composer.json 里的根声明,往上每行的 (required by ...) 就是施加冲突约束的包。常见陷阱:
- 输出为空?不是没冲突,而是你没在
composer.json里显式 require 它,或某conflict规则已全局拒绝该包 - 写成
laravel/framework:^11或11可能匹配不到——必须用完整版本号,如11.0.0 - 如果项目有
require-dev,比如phpunit/phpunit拖着老版sebastian/exporter,它可能间接锁死symfony/console,但why-not默认不查 dev 区,得手动加--dev参数
composer show --tree 看真实依赖结构,别信 composer.json
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 "laravel/framework" - 看到
(replaced)或(provided)?说明该包已被替代,得去它自己的composer.json里确认replace字段是否真能覆盖你的使用场景
composer update 要带 --with-dependencies,否则白更新
只写 composer update monolog/monolog,Composer 默认不动它的子依赖,哪怕新版本根本跑不起来。正确姿势:
composer update monolog/monolog --with-dependencies
这会连带更新 monolog/monolog 的直系 require,比如 psr/log 或 php 版本约束。注意:
- 别加引号和
^:错误写法composer update "monolog/monolog:^3",这会让 Composer 自己找“最新兼容版”,不是你要的3.0.0 - 跨主版本降级(如从 Guzzle 8 切回 7.4.5),必须在
composer.json里写死"guzzlehttp/guzzle": "7.4.5",不能只写"^7.0" - 执行后立刻
git diff composer.lock,确认只有目标包及其直系依赖变动,没波及其他
composer update --dry-run --verbose 查 SAT 求解卡点
当终端停在 Resolving dependencies、CPU 占满、无网络请求时,不是网络问题,是本地 SAT 求解器在穷举。加这两个参数:
composer update --dry-run --verbose
它会实时打印正在尝试的约束组合。重点关注:
- 反复出现同一组包(如
monolog/monolog 2.x → spatie/laravel-backup 7.0 → laravel/framework 10.x),但下一秒又退回尝试monolog 3.x—— 这大概率是某处conflict字段在硬性拦截 - 大量
php >=7.4组合被试,说明平台约束太宽;收紧"php": "^8.1"可减少 80%+ 求解时间 - 频繁出现
dev-main或dev-develop?检查私有包是否悄悄升级了require,比如把"guzzlehttp/guzzle": "^7.8"改成了"^8.0"
真正难处理的从来不是报错文字,而是那些没出现在 composer.json 里、却通过 conflict、replace、autoload 或 require-dev 悄悄生效的约束。











