composer update --dry-run -v 是唯一能实锤循环依赖的命令,卡在 resolving dependencies 超30秒、cpu拉满即表明sat求解器陷入闭环;重点观察末尾是否重复出现同一路径(如 vendor/a → vendor/b → vendor/c → vendor/a),出现两次即确认闭环。

composer update --dry-run -v 是唯一能实锤循环依赖的命令
卡在 Resolving dependencies 超过 30 秒、CPU 拉满、终端无响应,不是网络慢,是 SAT 求解器在闭环里打转。这时候别等,直接运行:composer update --dry-run -v,重点看最后几行是否反复出现同一路径,比如:vendor/a → vendor/b → vendor/c → vendor/a——出现两次就是闭环实锤。
注意:composer show --tree 默认包含 require-dev,容易掩盖真实生产链;composer validate 只校验 JSON 格式,对依赖逻辑完全无效。不加 -v 就只显示“正在解析”,毫无诊断价值。
常见干扰项:
- Windows cmd 可能截断输出,建议用 PowerShell 或加
| more分页查看 - 输出里出现
Package not found,说明该包根本没进解析阶段——先composer clear-cache,再删vendor/和composer.lock,重试composer update --dry-run
composer depends --tree 必须满足两个前提才有效
这个命令是定位闭环路径的唯一有效入口,但它有两个硬前提:项目必须已成功执行过 composer install(保证 composer.lock 有完整快照),且必须带 --tree 参数。不加就只显示一级依赖,根本看不到间接环。
查可疑包时,别只盯着 require,重点检查 require-dev 引入的包是否悄悄污染了主依赖图。例如:phpunit/phpunit 在自己的 composer.json 里写了 "autoload": {"psr-4": {"App\": "../src/"}},就把你的 src/ 加进了它的自动加载路径;而你的代码又用了它的测试基类,运行时就形成隐式闭环。
验证方法:
- 临时注释掉
require-dev下所有非核心工具(如phpunit/phpunit、phpstan/phpstan),再试composer update;如果恢复成功,问题就在这里 - 所有
require-dev包的composer.json都要搜../、../../、src/这类路径 - 检查你自己的
autoload-dev是否误把vendor/下的路径写进去了——这等于告诉 Composer:“我依赖我自己”
破环只有三种真实可行的方式,没有“跳过”选项
所有靠谱解法都指向一个目标:让依赖方向变成单向。强行删 composer.lock 或加 --ignore-platform-reqs 不是解决,是埋雷。
抽离契约包:
- 把双方共用的接口、DTO、异常类拎出来,建新包
myorg/contracts -
myorg/core和myorg/api都只require "myorg/contracts": "^1.0",删掉彼此的require
运行时解耦:
- A 不再
new B()或use BSomeClass,而是定义interface ServiceInterface(放契约包里),B 实现它,A 通过容器或工厂获取实例 - 此时 A 的
composer.json里彻底删掉对 B 的require
降级为可选依赖:
- 如果 B 只在 A 的测试里用(比如 Mock 数据),就移到
require-dev - 如果是增强功能(如导出 Excel),改用
suggest字段提示用户手动安装
autoload 和 autoload-dev 的越界引用最容易被忽略
很多“循环”根本没写在 require 字段里,而是靠自动加载悄悄打通的。例如:"psr-4": {"App\": "src/"},而 src/ 里用了 phpunit/phpunit 的 TestCase;或者 autoload-dev 错误地把 vendor/ 下的路径也写进去了。
这种隐式循环不会出现在 composer depends --tree 输出里,但会在类加载时触发死锁或 Class not found。验证方式很直接:临时清空 autoload-dev 配置,再跑一次 composer update --dry-run -v,如果不再回溯闭环,问题就定位到了。
真正难处理的,从来不是显式的 A → B → A,而是 A → B → C → A 这种三层以上嵌套,或者由 autoload 规则反向引入的跨目录引用——它们不会报错,直到第一次运行时才崩。











