Composer Monorepo 依赖本地 path 仓库实现,需严格对齐路径、包名、大小写及 repositories 顺序;改子包代码后 vendor 不更新,因 composer update 默认跳过 path 包,须用 --with-dependencies 或删 vendor 后重装。

先说一个核心事实:Composer 本身并不原生支持 Monorepo。我们常说的「单仓多包」模式,完全是依靠 repositories 配置项加上 path 类型仓库“拼凑”出来的。这种实现方式相当脆弱,路径、包名、大小写乃至配置顺序,但凡有一处对不上,你本地修改的代码就可能完全不会生效,而且 Composer 通常还不会报错,堪称静默的陷阱。
为什么改了子包代码,vendor里还是旧的
这大概是 Monorepo 实践中最常踩的坑了:你明明修改了 packages/my-sdk/src/Client.php,但执行完 composer dump-autoload 或 composer update 后,vendor/myorg/my-sdk 目录下的文件却纹丝不动。
composer dump-autoload这个命令,其职责仅仅是刷新类的自动加载映射,它不会触发任何包内容的同步。composer update命令在默认情况下,会主动跳过所有path类型的本地包,除非你显式指定该包名并加上--with-dependencies参数。- 最稳妥、一劳永逸的做法是:直接删除
vendor/myorg/my-sdk这个目录,然后重新运行一次composer install,强制 Composer 重建软链接。 - 如果你检查
vendor/myorg/my-sdk发现里面是完整的文件复制,而不是一个指向本地目录的符号链接,那就说明 symlink 功能没有启用。这时,需要在根项目composer.json的repositories配置里为对应仓库加上"options": {"symlink": true}。
repositories 怎么写才不被 packagist.org 覆盖
根项目 composer.json 中 repositories 数组的顺序,决定了 Composer 查找包的优先级,可谓一字千金。默认情况下,packagist.org 是一个隐式的、优先级很高的源。如果你没有禁用它,同时在 require 里写了诸如 "myorg/my-sdk": "^1.0" 这样的语义化版本约束,那么 Composer 会毫不犹豫地直奔远程仓库,去寻找已发布的 1.x 版本,从而完全忽略你本地的 packages/my-sdk 目录。
- 必须将
path类型的仓库声明放在repositories数组的最前面。 - 必须显式禁用默认的 packagist.org 源,配置为:
{"packagist.org": false}。 - 在
require中引用本地包时,版本必须使用"*@dev"、"dev-main"或简单的"*",绝不能写成"^1.0"这类标准的语义化版本。 - 每次修改了
repositories配置或子包自身的composer.json后,建议先运行composer clear-cache,否则 Composer 可能会读取旧的缓存数据。
子包 autoload-dev 不生效怎么办
在根项目运行 phpunitautoload-dev 配置。
- 对于
path类型的包,其autoload-dev配置默认是不启用的。即使子包自己的composer.json里写得再完整也无济于事。 - 解决方案是:必须在根项目的
composer.json的autoload-dev区块中,手动添加子包测试文件的路径映射。例如:"tests/MySdkTest.php": "packages/my-sdk/tests/"。 - 如果子包需要独立运行测试(例如在 CI 流水线中单独验证),那么它自身确实需要一份完整的
autoload-dev和require-dev配置。但即便如此,在根项目调用时,仍然需要上述那条显式的配置。
通配路径和 symlink 的实际限制
别对 "packages/*" 这样的通配符路径抱有过高期望——它仅在 Composer 2.2 及以上版本被支持,并且要求每个匹配的目录下都必须存在合法的 composer.json 文件。而符号链接(symlink)在 Windows 上默认会失败,在 Linux/macOS 上也可能会因为文件系统的挂载参数而失效。
"packages/*"不是递归匹配,它无法覆盖像packages/core/v2这样的嵌套层级。对于深层子目录,必须显式地写出完整路径。- Windows 用户必须以管理员身份运行终端,或者开启系统的“开发者模式”,否则创建符号链接(mklink)的操作会被系统拦截。
- Linux/macOS 用户如果发现软链接没有成功创建,首先应该检查
vendor/目录所在的文件系统是否挂载了noexec或nosymfollow这类限制性参数。 - 最省心的做法是:为所有
path类型的仓库都统一加上"options": {"symlink": true}配置,不依赖系统或 Composer 的默认行为。
说到底,用 Composer 管理 Monorepo,本质上是一系列需要精确到字符的配置对齐工作:路径、包名、大小写、版本写法、数组顺序、缓存状态——漏掉其中任何一环,其外在表现都是“看起来安装成功了,但实际上根本没连上”。这种近乎苛刻的脆弱性,恰恰也是它实现轻量可控的前提。这里没有黑魔法,只有对细节的绝对掌控。
