Composer依赖闭环的诊断核心是识别重复出现的包名组合:通过composer update --dry-run -v末尾输出、composer depends --tree中同一包名两次出现、composer show -t --no-dev --ignore-platform-reqs中循环路径,以及检查require-dev中autoload导致的隐式循环。

composer update --dry-run -v 的最后10行就是求解器“临终遗言”
Composer 不会卡住再重试,它一旦确认无法收敛,就直接退出——但退出前会打印最后一次尝试的路径和立即 reject 的原因。这不是日志,是 SAT 求解器的决策快照。
- 重点盯
composer update --dry-run -v输出末尾反复出现的包名组合,比如:package-a v1.2.0 requires package-b ^2.0→package-b v2.3.0 requires package-a ^1.0→ 又回到package-a - 看到同一组包(如
myorg/core、myorg/api)在嵌套层级中连续出现 ≥3 次,基本可判定为闭环入口 - 若输出含
Rejecting package-b because it requires package-a ^2.0,而锁文件里package-a是1.9,说明版本约束在环内互相拉扯,不是单纯“找不到包”
composer depends --tree 输出中出现自身两次即实锤闭环
composer depends --tree 是目前唯一能直接暴露依赖方向的命令,但必须满足两个硬前提:项目已有有效 composer.lock,且明确带 --tree 参数。
- 运行
composer depends myorg/core --tree,若输出含myorg/core ← myorg/api ← myorg/core,箭头表示“被谁依赖”,同一包名出现两次即闭环成立 - 若报
Package not found,说明该包根本没进composer.lock—— 先删vendor/和composer.lock,再跑composer update --dry-run -v看第一步失败点 - 不加
--tree的composer depends只显示一级上游,完全漏掉A → C → B → A这类多跳环
require-dev 中的 autoload 路径是 80% 隐式循环的源头
最常被忽略的“循环”根本不在 require 字段里,而是测试工具通过自动加载把你的代码反向拉进了它的运行时上下文。
- 检查所有
require-dev包的composer.json,重点搜../、../../、src/、tests/—— 比如phpunit/phpunit的"autoload": {"psr-4": {"App\": "../src/"}}就会让它加载你的src/ - 确认你自己的
autoload-dev没把vendor/下的路径写进去,否则 Composer 会认为“我依赖我自己” - 临时注释掉非核心
require-dev条目(尤其是infection/infection、phpstan/phpstan),再试composer update;很多“死循环”只是测试包在作祟
composer show -t --no-dev --ignore-platform-reqs 才接近真实安装路径
composer show -t 默认混入 require-dev 和跳过平台检查,容易掩盖生产环境的真实闭环。死锁往往藏在“实际会加载的子图”里。
立即学习“PHP免费学习笔记(深入)”;
- 执行
composer show -t --no-dev --ignore-platform-reqs,人工扫描输出中重复展开的包名组合,例如:vendor/a/package-a v1.3.0→vendor/b/package-b ^2.1→vendor/b/package-b v2.1.0→vendor/a/package-a ^1.2 - Windows 下
cmd可能截断长输出,建议用 PowerShell 或加| more -
composer validate完全无用,它只校验 JSON 格式;composer show不加--no-dev会引入干扰项
require,而是某个 require-dev 包的 autoload 把你的 src/ 拉进了它的加载路径,而你的代码又用了它的类——这种隐式闭环不会出现在依赖声明里,只会在运行时暴露。



















