composer create-project 本质是克隆远程仓库并执行 composer install,不运行脚手架或模板渲染;需目标包 type 为 project,通过 scripts 中 post-root-package-install 触发初始化脚本。

composer create-project 本质是克隆并安装,不是“生成”
它不运行脚手架代码,也不执行模板引擎渲染——只是把远程仓库(如 GitHub)的整个目录 git clone 下来,然后运行 composer install。所谓“自定义模板”,实际是维护一个已预设好结构、依赖和脚本的 Composer 包(通常为 type: project),由你托管在 Git 服务器上。
如何用 create-project 拉取私有/定制骨架
前提是目标仓库的 composer.json 中声明了 "type": "project",且根目录下有完整可运行的项目结构(如 public/、src/、index.php 等)。执行时指定包名和目标目录即可:
composer create-project vendor/my-skeleton my-app --stability=dev --no-interaction
-
vendor/my-skeleton必须已在 Packagist 注册,或已配置私有仓库源(如composer config repositories.mygit vcs https://git.example.com/my-skeleton) -
--no-interaction避免交互式提问(如是否删除已有目录),CI/CD 中必需 -
--stability=dev若骨架仅发布在dev-main分支,需显式允许不稳定版本 - 若想跳过
composer install(例如后续手动处理),加--no-install;但绝大多数情况不建议
create-project 后自动执行脚本的关键:scripts + post-root-package-install
真正实现“初始化逻辑”的地方是 composer.json 的 scripts 段。框架级骨架(如 Laravel、Symfony)靠这个完成环境文件复制、密钥生成等操作:
{
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate"
]
}
}
-
post-root-package-install是唯一在create-project流程中触发的事件(注意不是post-create-project-cmd—— 该事件只在composer create-project命令本身被调用时触发,但**仅当当前目录是空的且未安装任何包时才生效**,行为不可靠,官方已不推荐) - 脚本中调用
php或bin/console等二进制时,路径要写全或确保在$PATH中;相对路径以项目根目录为基准 - 避免在脚本里做耗时操作(如下载大文件),否则会卡住命令,用户无感知
常见失败原因和绕过方式
拉取失败往往不是模板问题,而是权限或配置链路断裂:
- 报错
Could not find package vendor/my-skeleton:确认composer show vendor/my-skeleton能查到;若为私有 Git,检查auth.json是否配置了 token,且仓库 URL 协议用https或ssh一致 - 报错
Failed to execute git clone ... fatal: could not read Username:Git 凭据未缓存,改用https://token:x-oauth-basic@github.com/...形式写入repositories,或配置git config --global credential.helper store - 脚本执行失败但命令返回 0:Composer 默认忽略脚本退出码;加
"script-action": "die"到config段可让失败中断流程(需 Composer 2.5+) - 想替换占位符(如
{{APP_NAME}})?Composer 不提供变量替换功能;必须自己写 PHP 脚本读取并替换文件内容,或改用专用工具如cookiecutter+composer create-project组合
真正难的不是拉代码,而是让骨架具备可维护性:版本分支对齐、脚本幂等、错误提示明确。很多人卡在第一次 CI 构建失败,其实只是忘了在 GitHub Actions 里 git config --global url."https://${{ secrets.TOKEN }}@github.com/".insteadOf "https://github.com/"。










