Composer直接拒绝解析循环依赖,报错卡在Resolving dependencies阶段;须用composer depends --tree定位闭环,再通过抽离契约包、运行时解耦或降级为可选依赖实现单向依赖。

Composer 不管理循环依赖——它直接拒绝解析。所谓“加载顺序”在循环存在时根本不会进入,报错就卡在 Resolving dependencies 阶段,不是顺序问题,是逻辑不可能。
composer update 卡在 resolving dependencies 是循环依赖的典型信号
这不是网络慢、缓存脏或 PHP 版本不匹配,而是 SAT 求解器已确认:不存在一组版本能让所有约束同时成立。常见表现包括:
- 命令长时间无响应(几十秒后才报错),输出停在
Resolving dependencies... - 最终错误类似
Root package cannot be installed或Package a depends on b, which depends on a - 本地能装、CI 失败——大概率是 CI 缓存了旧版
composer.lock或元数据,导致依赖图不一致
composer depends --tree 是唯一可靠的闭环定位方式
别靠猜,也别只看 composer.json 里写了啥。真实依赖链可能藏在 require-dev、replace、分支别名甚至私有仓库同步延迟里。实操要点:
- 必须在已成功
composer install的项目中运行,否则依赖图不完整 - 一定要加
--tree参数,否则composer depends myorg/core只显示一级引用,看不到闭环 - 看到输出里出现
myorg/core ← myorg/api ← myorg/core这种自指路径,就是实锤 - 若提示
Package not found,先composer clear-cache,再确认该包是否被早期解析拦截
autoload + require-dev 是最常被忽略的隐式循环来源
你以为只是测试用的包,结果它通过自动加载把你的代码“拉进”了生产依赖图。典型陷阱:
-
"autoload": {"psr-4": {"App\": "src/"}},而src/里用了phpunit/phpunit的类(比如TestCase) - 某个
require-dev包(如infection/infection)在它的composer.json中声明了"autoload": {"psr-4": {"MyOrg\": "../src/"}} -
autoload-dev里误写了"vendor/some-tool/src",让 Composer 认为“我依赖我自己”
抽离契约包是最稳健的破环手段
不是删掉 require,也不是加 --ignore-platform-reqs,而是让双方都依赖一个更轻、更稳定的第三方。关键细节:
- 新契约包(如
myorg/contracts)的composer.json中"require": {}必须为空,最多只留"php": "^8.1" - 接口、DTO、异常类全放进去,但不能有任何业务逻辑或具体实现
- A 和 B 都改用
"myorg/contracts": "^1.0",版本号语义化,升级节奏解耦 - 别在契约包里写
"replace": {"myorg/a": "*"}——这会和 A 的自身声明冲突,直接闭环
真正难的不是找到闭环,而是说服团队接受“拆包”带来的协作成本;最容易被跳过的,是清理 autoload 配置和验证 require-dev 包的自动加载范围。











