setup:di:compile 报 Circular dependency,开发模式下页面可能还能打开。循环依赖是对象图问题:A 构造时需要 B,而 B 直接或经过数层又需要 A,Object Manager 无法决定先实例化谁。
完整异常链比最后两个类更重要
保存编译输出中所有类名,画出 A→B→C→A。检查最近改动的 constructor、di.xml preference、virtualType 和 plugin。最终显示核心类不代表核心有错,自定义 preference 可能把实现替换成循环版本。
不要用 ObjectManager 隐藏循环
在方法里直接 getInstance 只是把编译期错误推迟到运行期,并破坏可测试性。先判断职责是否混在一起:把共同逻辑抽到更小服务、通过事件/接口反转依赖,通常比延迟加载更清晰。
Proxy 的适用条件
如果 A 只在某个方法路径偶尔使用重服务 B,可注入 B 的 Proxy,让真实对象在调用时创建,从而打破构造阶段循环。但若 A 方法调用 B、B 同一调用又立即回 A,Proxy 只会推迟无限递归,不能解决设计循环。
<argument name="service" xsi:type="object">
VendorModuleModelHeavyServiceProxy
</argument>
Plugin 也会制造隐形依赖
对 A 的 plugin 构造函数注入 A 或注入依赖 A 的服务,插件列表生成时就可能循环。Plugin 应依赖更窄的接口,避免调用 subject 的公共方法重新进入同一拦截链。
修复后删除 generated/code、在干净构建中 compile,并运行实际业务路径与单元测试。成功编译只是第一步,还要确认没有递归调用和意外提前实例化。
工厂与 Service Locator 不是结构修复
Factory 每次 create 虽然延迟对象生成,但如果创建路径仍回到原服务,运行时依旧循环。只有业务流程天然需要新实例时使用 Factory;共享无状态服务应通过接口注入。把所有依赖都改成 Proxy/Factory 会让图更难理解。
Observer/Command 分离
两个服务互相调用常说明它们既做状态变更又做通知。可把核心写操作抽成 command,把后续反应发布事件或队列;事件处理器不得再次触发同一命令而无幂等保护。这样同时缩短事务并打破依赖。
查生成代码不是改生成代码
generated 中的 Interceptor/Factory 能帮助看最终类型,但不能直接编辑。修复源 di.xml/PHP 后清理生成目录重新编译,比较依赖图。部署产物必须由同一 composer.lock 和模块配置构建。
用测试锁住依赖方向
为拆分后的服务写构造测试和业务测试,保证接口依赖从高层业务指向稳定抽象。静态分析或架构测试可以禁止某些命名空间反向引用,避免下次改动再次形成 A→B→A。编译错误解决后,也要测异常路径与事务回滚。
Area 配置差异也要检查
frontend、adminhtml、webapi、graphql 可合并不同 di.xml,循环可能只在某个 area 的 preference/plugin 组合出现。编译会扫描更完整对象图,所以开发页偶尔正常并不能排除问题。分别运行受影响入口的集成测试,确认各 area 最终类型一致。

