最稳的本地模块拆分方式是直接用path类型仓库,需满足子模块composer.json的name唯一、type为library、autoload映射精准,主项目repositories用相对路径配置,require写dev-分支名,上线前必须移除path配置并改用真实版本号。

直接用 path 类型仓库是本地模块拆分最稳的路径,跳过发布、不用 CI 等待、改代码立刻生效——但前提是每个子目录都配对了合法的 composer.json,且主项目 repositories 和 require 严格匹配。
每个子模块必须有完整、独立的 composer.json
不是“有就行”,而是必须满足三项硬性条件:
-
name字段全局唯一(如"myorg/auth"),不能带版本号或分支名 -
"type": "library",不能是project或空值 -
autoload.psr-4映射必须精准:命名空间末尾不加反斜杠,路径末尾也不加斜杠(例如"MyOrg\Auth\": "src/",写成"src/"或"src"都会导致文件加载失败)
漏掉任意一条,composer install 会静默跳过该模块,或在运行时报 Class not found ——它不会报错,只报“找不到类”。
主项目 repositories 必须用 type: path + 相对路径
在根目录 composer.json 的 repositories 段里,只能这样写:
{
"repositories": [
{"type": "path", "url": "./packages/auth"},
{"type": "path", "url": "./packages/order"}
]
}
常见错误包括:
- 用绝对路径(
/home/user/project/packages/auth)——CI 构建时路径不存在 - 漏掉
./前缀,导致 Composer 当作远程 URL 解析,报Could not parse version constraint - 把多个模块写进一个
url(如"./packages/*")——Composer 不支持 glob
执行 composer install 后,vendor/myorg/auth 是指向 packages/auth 的软链接,不是复制。这点必须确认:ls -la vendor/myorg/auth 应显示 -> ../../packages/auth。
require 版本必须写 dev- 分支名,不能写 * 或 latest
require 段不是写 "myorg/auth": "*",而是明确指定开发分支:
"require": {
"myorg/auth": "dev-main",
"myorg/order": "dev-develop"
}
原因很实际:
-
*会让 Composer 尝试匹配 Packagist 上已发布的版本,而你本地还没发包 -
dev-main中的dev-前缀是 Composer 识别 path 仓库的开关,缺了就当远程包处理 - 分支名必须和子模块 Git 仓库当前 HEAD 所在分支完全一致(大小写敏感)
如果子模块用的是 main 分支,但你写了 "dev-master",composer update 会成功但不软链,最终还是复制一份到 vendor 里,改代码不生效。
上线前必须清理 path 仓库配置
本地能跑通 ≠ 上线能用。CI 流水线中没有 packages/ 目录,path 仓库会直接失效,报 Could not find a matching version。
上线前务必做两件事:
- 从
repositories中删掉所有type: path条目 - 把
require中的dev-main改为真实语义化版本(如"1.2.0"),并确保该版本已推送到私有源(如 Satis 或 Private Packagist)
最容易被忽略的是:改完后必须重新执行 composer update myorg/auth 并提交更新后的 composer.lock。否则线上仍会尝试加载本地路径,构建必然失败。











