Composer 不会自动初始化或更新 Git submodule。它仅负责 PHP 依赖的下载与安装,完全不感知 .gitmodules,也不执行 git submodule 命令;需通过 post-install-cmd/post-update-cmd 脚本手动触发 git submodule update --init --recursive,或改用 path 类型仓库配合本地 submodule 管理。

Composer 本身不碰 git submodule,别指望它自动初始化
Composer 只负责下载和解压包,哪怕你用 git clone 方式拉取一个含 submodule 的仓库,它也不会执行 git submodule update --init --recursive。你看到的空目录、Class not found、或 fatal: No url found for submodule path,基本都源于此。
验证方式很简单:进到 vendor/xxx/package 目录下,运行 git submodule status。如果没输出,或者报错,说明 submodule 根本没加载。
- 不要在 CI 环境里依赖 “本地开发能跑通” —— CI 往往是 clean 环境,没有手动执行过 submodule 命令
- 别把 submodule 初始化逻辑塞进
post-autoload-dump—— 它不保证 vendor 目录已就位 - 若 submodule 指向私有 Git 地址(如
git@gitlab.internal:xxx/sub.git),确保运行环境已配置好 SSH key 或 token 访问权限
用 post-install-cmd 和 post-update-cmd 自动触发 submodule 更新
这是最直接、可控的补救方式。在项目根目录的 composer.json 中添加脚本钩子,让 Composer 在安装/更新后主动调用 Git 命令:
"scripts": {
"post-install-cmd": [
"git submodule update --init --recursive"
],
"post-update-cmd": [
"git submodule update --init --recursive"
]
}
注意几个实操细节:
- 该命令作用于整个项目根目录,所以只适用于 submodule 在项目自身 repo 里的场景(比如你把 SDK、config、或 shared-assets 作为 submodule 放在主项目里)
- 如果 submodule 是某个第三方包自带的(比如
vendor/symfony/console里有 submodule),上面写法会失败——因为git submodule需要先cd进对应包目录 - 更稳妥的做法是加路径判断:
@php -r "if (is_dir('./vendor/xxx/package/.git')) { chdir('./vendor/xxx/package'); system('git submodule update --init --recursive'); }"
遇到第三方包带 submodule,优先切 dist 模式绕开
如果你只是想装上依赖跑起来,而不是参与那个包的开发,--prefer-dist 是最快解法。它会让 Composer 放弃 git clone,改从 Packagist 下载 zip 包,自然跳过所有 submodule 相关操作。
两种启用方式:
- 临时:运行
composer install --prefer-dist或composer update --prefer-dist - 永久:在
composer.json里加"preferred-install": {"*": "dist"}
但要注意:不是所有包都有 dist 归档。如果某私有包只托管在 GitLab 上且没打 tag,--prefer-dist 会直接失败,报 Could not find package xxx at version yyy。这时就得回到上一节的手动钩子方案,并确保目标包仓库本身 submodule 已正确提交且可访问。
混合架构下 submodule 和 path 类型仓库别混用
当项目同时用到 path 仓库(比如本地开发中的 SDK)和 submodule(比如共享的 config repo),容易出现路径冲突或加载顺序错乱。典型现象是:composer install 成功,但运行时提示 Cannot declare class Xxx, because the name is already in use。
根本原因是:path 仓库会被软链接进 vendor,而 submodule 是硬检出在项目根目录;如果两者都包含同名命名空间或类文件,autoload.php 会按 PSR-4 规则重复注册。
- 解决方案:明确职责边界 —— submodule 只放纯静态资源(如 YAML 配置、SQL 模板、图标集),不放 PHP 类;PHP 逻辑全部走
path或vcs仓库 - 上线前必须删掉
repositories.type: "path"配置,否则 CI 构建时找不到本地路径,直接中断 - 若 submodule 确实含 PHP 代码,考虑把它单独打包成一个 Composer 包,再通过
vcs引入,而不是直接挂 submodule
真正麻烦的从来不是“怎么让它跑起来”,而是“怎么让它在 dev、test、prod 三个环境里行为一致”。submodule 的状态不会被 composer.lock 锁定,它的 commit hash 只存在于 .gitmodules 文件里 —— 这个细节,90% 的人上线前才想起来检查。


















