composer 拒绝安装循环依赖而非绕过——通过 composer show -t --no-dev 看真实依赖树,用 --dry-run -v 查求解器“临终遗言”,重点排查自引、autoload-dev 路径及 require-dev 逻辑闭环。

Composer 不会“处理”循环依赖,它只检测并拒绝安装——一旦发现无法收敛的依赖图,就直接报错或卡住。 你看到的 Root package cannot be installed 或 Dependency resolution failed 不是警告,而是求解器明确告诉你:没有一组版本能满足所有约束。问题不在“怎么绕过”,而在“哪条路径在自指”。
用 composer show -t --no-dev --ignore-platform-reqs 看真实依赖树
这个命令输出的是最接近 composer install 实际行为的依赖结构。默认的 composer show --tree 会混入 require-dev 和跳过平台检查,掩盖生产环境的真实闭环。
- 重点扫描重复出现的包名组合,比如
package-a → package-b → package-a这种嵌套三层以上的展开 - 如果某个包在树中被多次引入(如出现 3 次以上),且每次父级都不同,大概率是间接循环的信号
- Windows 下 cmd 可能截断长输出,建议用 PowerShell 或加
| more
运行 composer update --dry-run -v 抓求解器最后的挣扎
加 -v 后,Composer 会在失败前打印它尝试过的最后一个可行组合和立即 reject 的原因。这不是日志,是求解器的“临终遗言”。
- 留意最后 5–10 行反复出现的包名,比如
package-b被选中又因requires package-a ^2.0被丢弃,而当前项目锁的是package-a:1.9 - 检查
composer.json中是否误把本项目自身写进require,例如"myvendor/myproject": "dev-main"—— 这是最隐蔽的显式循环 -
--dry-run不改任何文件,但会完整走一遍 SAT 求解流程,比install更早暴露死锁
查 composer depends 和 composer why-not 定位隐式闭环
显式互引好找,难的是 autoload + require-dev 构成的逻辑循环:测试工具加载了你的代码,你的代码又用了它的类。
- 运行
composer depends package-a查谁依赖package-a;再对那个包运行composer show package-x,看它的autoload是否包含你项目的src/或tests/ - 用
composer why-not package-b:2.0看谁在阻止升级——常是某个require-dev包的composer.json里写了"package-a": "dev-main" - 临时注释掉
phpunit/phpunit、infection/infection等喜欢跨目录 autoload 的 dev 工具,再试update
别信 conflict 能自动修复,它只是提前报错
conflict 不会卸载已有包,也不会重排依赖顺序,它只让 Composer 在解析早期就放弃尝试——省得卡住或报模糊错误。
- 写
"conflict": { "old-sdk": ">=1.0.0" }前,必须先composer remove old-sdk,否则冲突检测不生效 -
replace是替代声明,适用于你 fork 并重实现了某个包,但要注意:它不会自动加载你 fork 后的代码,autoload 还得自己配 - 私有包之间硬性循环时,
conflict是最快速的刹车,不是解药;真正要动的是架构——把共用逻辑抽成独立包shared-utils,让双方都require它
最容易被忽略的点:循环未必发生在 require 字段,而藏在 autoload-dev 里写了 "../vendor/some-tool/src" 这种路径,或者某个 dev 包的 psr-4 映射把你项目的目录当成了它自己的命名空间根。这类问题不会出现在 show --tree 里,只能靠人工翻 vendor/*/composer.json。











