Composer update卡死、CPU爆满是循环依赖的典型信号,本质为SAT求解器在闭环中无限回溯,表现为终端卡住、CPU持续95%+、内存每秒涨50MB+后突然退出;需用composer update --dry-run -v观察末尾反复路径,再以composer depends --tree实锤闭环链路。

composer update卡死、CPU爆满就是循环依赖的典型信号
Composer 遇到循环依赖不会报明确错误,而是 SAT 求解器在闭环里无限回溯:终端卡住几十秒、php 进程 CPU 占用持续 95%+、内存每秒涨 50MB+,最后突然退出。这不是性能问题,是逻辑矛盾——A 依赖 B,B 又依赖 A,求解器根本算不出合法安装路径。
用 composer update --dry-run -v 抓“临终遗言”
这个命令不改任何文件,但完整走一遍依赖解析流程。关键不是看开头,而是盯最后 5–10 行输出:
- 反复出现同一组路径(如
myorg/core → myorg/api → myorg/core)就是强信号 - 看到
Rejecting myorg/api because it requires myorg/core ^2.0, but current is myorg/core:1.9,说明版本约束在环内互相拉扯 - 某个包名连续出现 ≥3 次且父级不同(比如 A→C→B→A),大概率是多跳闭环
用 composer depends --tree 实锤闭环链路
这是唯一能确认循环的命令,但有两个硬前提必须满足:
- 项目已有有效的
composer.lock(否则先删vendor/和composer.lock,再跑composer update --dry-run -v看第一步失败点) - 必须带
--tree参数,否则只显示一级依赖,完全漏掉间接环 - 运行
composer depends myorg/core --tree,若输出含myorg/core ← myorg/api ← myorg/core(箭头方向是“被谁依赖”),自身出现两次即闭环成立
注意:composer depends myorg/core(无 --tree)只会返回直接上游,毫无诊断价值。
require-dev 是 80% 隐式循环的源头
最常被忽略的“循环”根本不在 require 字段里,而是测试工具通过自动加载把你的代码反向拉进了它的运行时上下文:
- 检查所有
require-dev包的composer.json,重点搜../、../../、src/、tests/——比如phpunit/phpunit的"autoload": {"psr-4": {"App\": "../src/"}}就会让它加载你的src/ - 确认你自己的
autoload-dev没把vendor/下的路径写进去,这等于告诉 Composer:“我依赖我自己” - 临时注释掉
phpunit/phpunit、infection/infection等喜欢跨目录 autoload 的 dev 工具,再试composer update --dry-run -v;如果不再反复跳转,问题就在这
真正难处理的不是 A→B→A 这种直白环,而是 A→C→D→B→A 这类多跳环,靠肉眼读 composer show --tree 很难定位,必须依赖 --tree 输出的层级缩进和重复出现模式来交叉验证。


















