Git Subtree 不能替代 Composer 的 autoloader 机制,Subtree 仅嵌入代码而不参与自动加载;必须在主项目 composer.json 中显式声明依赖并配置 autoload 映射,或通过 path repository 将 subtree 目录注册为本地包源,否则 Class not found。

Git Subtree 不能替代 Composer 的 autoloader 机制
Subtree 只是把另一个仓库的代码以子目录形式“嵌入”当前项目,它不参与 PHP 自动加载流程。即使你用 git subtree add 把一个 Composer 包的源码拉进 vendor/xxx,composer autoload 也不会识别它——因为 composer.json 里没声明这个包,autoload 配置也没覆盖对应命名空间。
常见错误现象:Class not found 即使文件物理存在,PHP 运行时找不到类;或者 composer dump-autoload 后毫无反应。
- 必须在主项目的
composer.json中显式声明该子项目为依赖(哪怕它实际通过 subtree 拉取) - 如果子项目本身有
autoload配置(如"psr-4": {"Foo\": "src/"}),主项目需在自己的autoload或autoload-dev中补全映射,或使用repositories+path类型指向 subtree 目录 - 避免直接修改
vendor/下的 subtree 目录:Composer 可能下次install时覆盖或清空它
用 path repository 让 Composer “认出” subtree 目录
这是实现“无痕发布”的关键一步:让 Composer 把本地 subtree 目录当作一个可安装的包源,而不是手动塞进 vendor 的静态副本。
假设你把子项目 my-org/utils 用 git subtree add --prefix=libs/utils 拉到了 libs/utils 目录下,且该目录含完整 composer.json(含 name、autoload 等):
{
"repositories": [
{
"type": "path",
"url": "./libs/utils"
}
],
"require": {
"my-org/utils": "*"
}
}
注意:my-org/utils 必须与 subtree 项目 composer.json 中的 name 完全一致;"*" 会匹配本地路径下任意 commit,但不会自动更新——composer update my-org/utils 仅刷新 autoload 映射,不拉 Git 提交。
- 每次 subtree 更新后(
git subtree pull),需手动运行composer update my-org/utils触发 autoload 重建 -
path类型 repository 不支持版本约束(如^1.2),只能用*或dev-main等分支别名 - CI 构建时若禁用
vendor/提交,需确保libs/utils被纳入构建上下文,否则path源不可达
发布时如何隐藏 subtree 细节
所谓“无痕”,是指最终发布的包(如打包成 tar.gz 或推送到 Packagist)不暴露内部用了 subtree,下游用户只当它是普通 Composer 包使用。
核心原则:发布产物只包含 composer.json 和实际需要的代码,不带 .git、不带 libs/utils/.git、也不带任何 subtree 元数据。
- 用
git archive或composer archive打包前,确认libs/utils目录已“去 Git 化”——即删掉其下的.git子目录(subtree add 后会生成) - 若使用
composer publish类工具,需在archive前执行清理脚本:find libs -name '.git' -type d -exec rm -rf {} + - 不要把
git subtree命令写进composer.json的scripts,这会让下游用户误以为必须装 Git 才能安装你的包
多个子项目共用同一份依赖源时的冲突风险
当 A、B、C 三个子项目都 subtree 同一个底层库(比如 shared-contract),且各自用 path 方式接入时,它们的 composer.json 会分别指向本地不同路径(../shared/shared-contract、../../shared/shared-contract 等),但 Composer 不允许同一 name 出现在多个 path 源中——会报错 Package my-org/contract is already present。
解决方式不是绕开,而是统一收口:
- 所有子项目都从一个**中央 workspace 目录**引用
shared-contract,例如全部设为"url": "../shared-contract" - 用
composer create-project或 monorepo 工具(如symfony/flex的插件模式)管理跨项目路径一致性 - 真正要发布时,每个子项目应独立
composer install --no-dev --optimize-autoloader,并确保shared-contract的 autoload 映射被合并进最终vendor/autoload.php,而非靠 runtime require
最容易被忽略的是:subtree 的 commit hash 在各子项目中可能不同步,导致功能行为不一致——发布前务必校验所有 subtree 目录的 git rev-parse HEAD 是否一致。


















