Composer 遇到循环引用直接拒绝生成依赖图而非尝试解开;可通过 --dry-run -v 观察重复路径、Rejecting 日志及 composer depends --tree 实锤闭环,同时排查 require-dev autoload 和 path 包 symlink 失效问题。

Composer 遇到循环引用不会“尝试解开”,它直接拒绝生成依赖图——所谓“深度递归调用”其实是 SAT 求解器在闭环里无限回溯,表现为 CPU 持续 95%+、内存每秒涨 50MB+、终端卡住几十秒后突然退出。
如何确认是循环依赖而非其他错误
别急着删 vendor 或重写 composer.json,先验证是不是真循环:
- 运行
composer update --dry-run -v,不改任何文件但走完完整解析流程;盯最后 5–10 行输出,反复出现同一组路径(如vendor/a → vendor/b → vendor/a)就是强信号 - 看到类似
Rejecting vendor/b because it requires vendor/a ^2.0, while currently locked to vendor/a:1.9,说明版本约束在环内互相拉扯 - 某个包名(如
myorg/core)在末尾日志中连续出现 ≥3 次且父级不同,大概率是 A→C→B→A 这类多跳环
用 composer depends --tree 实锤闭环链路
这是唯一能定位真实循环路径的命令,但有两个硬前提必须满足:
- 项目已有有效的
composer.lock(如果报Package not found,说明该包根本没进锁文件——先删vendor/和composer.lock,再跑composer update --dry-run -v看第一步失败点) - 命令必须带
--tree参数,否则只显示一级依赖,完全漏掉间接环 - 运行
composer depends myorg/core --tree,若输出含myorg/core ← myorg/api ← myorg/core,箭头方向是“被谁依赖”,自身出现两次即闭环成立
require-dev 中的 autoload 是 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;如果不再反复跳转,问题就在这
path 类型本地包的 symlink 失效也会伪装成循环
当你看到 Class not found 或 require_once(): Failed opening required,但 vendor/acme/utils 目录明明存在——大概率是 Composer 没走 symlink,而是复制了一份静态副本:
- 关键点不在主项目,而在本地包自己的
composer.json:必须加"options": {"symlink": true} - Windows 用户必须用管理员权限运行终端,否则创建 junction 失败且无提示
- 验证是否真链接成功:
ls -la vendor/acme/utils(macOS/Linux)应显示->;Windows 上dir vendorcmeutils应显示JUNCTION
真正棘手的不是报错本身,而是循环可能藏在 require-dev 的 autoload 里、或由 path 包的 symlink 失效触发——这两类问题都不会在 composer.json 的 require 字段里留下痕迹,却足以让整个依赖解析流程崩溃。











