先说说 Composer 依赖版本锁定的核心机制:这事儿的真正功臣是 composer.lock 文件,可不是 composer.json 里写的那些版本号。后者只是划了个范围,说白了就是个约束条件,真正决定你最后装到哪个具体版本的,还得看 lock 文件。

为什么 composer.lock 必须提交到 Git
不提交 lock 文件,环境一致性基本就是一句空话。同一份 composer.json,在不同机器上跑 composer install,很可能装出完全不同的依赖树——尤其当某个包发布了新的 patch 版本时,问题就来了。
具体来说,有这么几个典型场景:
- CI/CD 流水线拉完代码执行
composer install,如果没有 lock 文件,它会自动触发composer update的行为,也就是去解析最新的兼容版本,结果完全不可控。 - 生产服务器部署时,
composer install找不到 lock 文件,就会退化成update操作,可能一不小心就把没经过测试的变更带上线了。 - 团队协作中更常见:A 同学在本地
update了某个包,但忘了提交 lock 文件,B 同学再install时拿到的还是旧版,两人本地的 vendor 目录实际并不一致。
composer install 和 composer update 的行为差异
这两个命令,还真不是字面上“安装”和“升级”那么简单。它们的触发逻辑,完全不一样。
composer install:只认composer.lock,严格按照里面记录的精确版本、commit hash、dist url 来安装。只有在 lock 文件不存在时,才会 fallback 到update行为。composer update:完全忽略 lock 文件,重新解析composer.json里的所有版本约束,计算出当前最新且满足条件的版本组合,并重写 lock 文件。- 日常开发中,新增一个包时用
composer require foo/bar,它默认执行的就是update。但上线前,务必确保 lock 文件已经提交,而且生产环境只跑install。
稳定性标记怎么影响 lock 文件内容
当你在命令行里敲下 @beta 或者 --stability=dev 去装一个不稳定版本时,这个具体版本(比如 v3.2.0-beta2 或者 dev-main#abc123)会被写进 lock 文件。下次 install 时,就会严格装这个 commit,不会自动升级到 dev-main#def456。
这里有几个关键点需要留意:
minimum-stability是一个全局的兜底策略,只在没有显式指定稳定性时才起作用。一旦你在require里写了"foo/bar": "dev-main"或"foo/bar": "^2.0@rc",它就立刻生效,lock 文件记下的就是那个具体的快照。prefer-stable: true并不会阻止你装dev-main——只要你明确写了,它照装不误。它的作用只是:在其他包同时有 stable 和 rc 两个版本可选时,优先挑 stable 的。- 最危险的操作是什么?改了
minimum-stability之后直接跑composer update,lock 文件里可能悄悄混进一堆dev-分支,而你根本没注意到。
生产环境部署必须加的参数
光会跑 composer install 还不够,得加上几个关键参数才行:
--no-dev:跳过require-dev里的包,比如 phpunit、phpstan 这些测试工具,别让它们混进生产镜像。--optimize-autoloader:生成 classmap 来加速自动加载,减少文件 stat 的开销,对 PSR-4 包尤其有效。--no-interaction:防止因为缺少交互输入(比如 GitHub token 提示)导致 CI 卡住。- 还有一条铁律:不要在生成环境机器上跑
composer update,哪怕加了--no-dev——它仍然会重算依赖、改写 lock 文件,破坏可重现性。
说到底,真正难的技术债往往不是写对一条命令,而是让整个流程里的所有人——包括 CI 脚本、运维脚本、新来的同事——都默认信任 lock 文件,而不是习惯性地去敲 update。一旦有人绕过了 lock,整体稳定性就从根上松动了。