composer本身不支持monorepo,所有本地包实时联动效果均依赖根composer.json中repositories配置的路径、包名、版本标识(如"@dev")和软链开关("options":{"symlink":true})四者严丝合缝,缺一即静默回退至packagist加载远程包。

composer 本身不支持 Monorepo,所有“本地包实时联动”效果,都依赖根 composer.json 中 repositories 配置的四个要素严丝合缝:路径、包名、版本标识、软链开关。错一个,就静默 fallback 到 Packagist。
为什么 composer require acme/utils 装的是远程旧版,不是本地代码?
根本原因:Composer 根本没看到你本地的 packages/utils 目录——它只认根 composer.json 里 repositories 声明的路径。
-
repositories必须写在根目录的composer.json中,子包自己的composer.json里加无效 -
url是相对于根composer.json的路径,比如子包在packages/utils,就得写"packages/utils",不能写./packages/utils或绝对路径 - 改完
repositories后,必须先运行composer clear-cache,否则缓存可能让新路径不生效 - 子包
composer.json的name字段必须非空且完全一致("acme/utils"≠"Acme/Utils"),大小写和连字符都敏感
vendor/acme/utils 是复制出来的,不是符号链接?
这是开发中最隐蔽的失效点:你以为改了子包代码主项目立刻可见,结果却在用一份静态副本。
- 默认不启用软链,必须在
repositories条目中显式加"options": {"symlink": true} - Windows 用户需以管理员身份运行终端,或开启“开发者模式”,否则
mklink被禁用 - Linux/macOS 上若
vendor/所在文件系统挂载了noexec或nosymfollow,软链也会失败 - 别信
composer dump-autoload—— 它不重建软链;最稳妥方式是删掉vendor/acme/utils,再跑一次composer install
require 里写 "^1.0" 还是 "*@dev"?
写 "^1.0" 就等于放弃本地路径,强制走 Packagist 版本匹配逻辑。
- 本地
path包不参与语义化版本解析,"acme/utils": "^1.0"会让 Composer 直接跳过你的本地目录,去找已发布的 1.x 版本 - 唯一安全的写法是
"*@dev"、"@dev"或"dev-main"(后者要求子包分支名确实是main) - 子包自身
composer.json中的version字段可省略,Composer 不读它;真正起作用的是name和路径一致性 - 子包之间互相依赖时,被依赖方也必须落在
repositories的url覆盖范围内,否则依赖树会断在远程源
Monorepo 下 composer install 为什么越来越慢?
不是 Composer 变慢了,而是它在重复扫描几十个子包的 composer.json 并尝试软链,I/O 和逻辑开销叠加导致耗时从几秒升到数分钟。
- 临时删掉
repositories中的path条目,对比耗时,就能确认是否由 Monorepo 引起 - 避免在根
require中用模糊约束(如"*"或"^1.0")引用内部包——这会让 Composer 拒绝复用已有软链,强制重解 - 日常开发建议进子包目录执行:
cd packages/utils && composer install --no-install --no-autoloader,只生成 autoload 映射,不触发冗余 symlink - 根
composer.json的config字段里关掉无用插件:"fxp-asset": false(除非真用 bower/npm asset)
options.symlink 默认为 false,以及 clear-cache 这步操作常被跳过。











