Composer symlink 本身不会导致死循环,真正原因是依赖图中的循环 require 或 autoload-dev 的跨目录映射;需检查本地包 composer.json 的 "options": {"symlink": true"} 配置及 dev 包的 PSR-4 映射是否指向父级目录。

Composer symlink 本身不会导致死循环
软链接(symlink)只是文件系统层面的指针,composer install 创建 symlink 的过程是单向、静态的,不会触发任何运行时调用或递归解析。所谓“死循环”,实际是依赖图中存在 require 或 autoload-dev 层面的循环引用,和 symlink 是否启用完全无关。
但 symlink 会让问题更隐蔽:你改了本地包代码,却因 fallback 到 copy 模式而误以为“改了没生效”;或者 dev 工具通过 symlink 跨目录加载你的源码,意外引入反向依赖链。
- 真正触发 Composer 拒绝解析的是循环
require关系,比如arequireb,brequirea -
autoload-dev中的 PSR-4 映射若指向父级目录(如"App\": "../src/"),会让 phpunit 等工具把你的项目代码载入它自己的上下文,形成隐式反向依赖 - symlink 只是让这类跨目录加载“看起来更自然”,但它不是源头
如何确认是不是 symlink 引发的加载错乱
先验证 symlink 是否真实生效,再排查是否被误用:
- Linux/macOS:运行
ls -la vendor/vendor/name,输出含->才是链接;若显示普通目录,说明已 fallback 到 copy 模式 - Windows:运行
dir vendorendor ame,看到JUNCTION或SYMLINKD才是成功;否则就是静态副本 - 关键配置不在主项目
composer.json,而在**本地包自己的composer.json** 中必须有:"options": {"symlink": true} - Windows 用户必须以管理员身份运行终端,否则创建 symlink 静默失败
- Docker 场景下,
-v $(pwd)/../my-pkg:/workspace/my-pkg这种挂载方式不透传 symlink 权限,需改用绑定挂载 +--cap-add=SYS_ADMIN
autoload-dev 映射是 80% 隐式循环的真正推手
很多“类加载后出现循环依赖”的报错,根源是 require-dev 包在 autoload-dev 里偷偷把你项目的 src 映射进去了——它不是依赖你,而是直接加载你。
- 检查所有
require-dev包的composer.json,搜索../、../../、"src/"、"tests/" - 典型高危配置:
"autoload": {"psr-4": {"App\": "../src/"}}出现在phpunit/phpunit或infection/infection的 composer.json 中 - 临时注释掉这些 dev 包,再跑
composer update --dry-run -v;如果末尾不再反复跳转同一组路径,问题就定位到了 - 你自己的
autoload-dev里也严禁写vendor/下的路径,这等于告诉 Composer:“我依赖我自己”
死循环的实锤与破环路径
Composer 不会“尝试解开”循环,它会在 SAT 求解阶段直接拒绝,表现为 CPU 持续 95%+、内存飙升、命令卡住几十秒后退出。此时靠猜没用,得用对命令:
- 运行
composer update --dry-run -v,盯最后 5–10 行:反复出现同一组包(如myorg/core → myorg/api → myorg/core)就是闭环铁证 - 运行
composer depends myorg/core --tree(注意必须带--tree),若输出含myorg/core ← myorg/api ← myorg/core,箭头方向表示“被谁依赖”,自身出现两次即闭环成立 - 破环只有三条路:抽离公共契约到独立包、把强依赖改为运行时注入(如接口 + DI 容器)、降级为可选依赖(
"suggest"或require-dev)
别指望 composer dump-autoload 能解决循环问题——它只管映射,不管依赖逻辑。真遇到死循环,vendor/ 和 composer.lock 往往已经处于半损坏状态,最稳做法是删干净再重来。


















