composer install 仅在无 composer.lock 时生成它:先解析 composer.json 求解依赖,再写入精确版本、哈希及 URL;存在 lock 则严格按其安装,不解析、不更新、不重写。

composer install 为什么有时会生成 composer.lock?
因为 composer install 的行为完全取决于当前是否存在 composer.lock 文件:
- 有
composer.lock→ 严格按它记录的版本、哈希、URL 安装,不联网、不解析、不改 lock - 没有
composer.lock→ 退化为首次求解:读composer.json,运行依赖求解器,下载满足约束的最新兼容版本,并生成新composer.lock
所以新人克隆项目后第一次跑 composer install 看到 lock 文件“突然出现”,不是 bug,是预期行为。但要注意:这个首次生成的结果,和直接跑 composer update 不一定相同——后者强制走完整求解流程,对 dev-main、^3.0@dev 这类不稳定约束的处理更激进。
composer update 默认会更新所有包,不只是你改过的
很多人误以为改了 composer.json 里一行 "monolog/monolog": "^3.0",composer update 就只动 monolog。实际不是:
- 默认行为是重新计算整个依赖图,包括
phpunit、symfony/polyfill等所有间接依赖 - 哪怕其他包的约束没变,只要它们有新版本满足全部约束,就会被一并升级
- 想只更新某个包?必须显式指定:
composer update monolog/monolog - 想连带更新它的子依赖?加
--with-dependencies;否则可能卡在“已满足”状态,看似没动,实则留了隐患
漏掉这个细节,开发环境顺手 upgrade 了整个生态,结果测试没覆盖到某个 polyfill 对 DateTimeZone 的行为变更,上线后时区解析就出错。
生产环境死守 composer install --no-dev,不是为了省时间
关键不在快慢,而在语义和确定性:
-
composer install --no-dev是“复现”:跳过 dev 包,且完全跳过依赖解析,只按 lock 里写死的哈希和 URL 下载归档 -
composer update --no-dev仍是“重算”:它仍会联网请求 Packagist API、校验 PHP 版本/扩展/子依赖约束,本质是 NP-hard 计算,50 个包可能卡两分钟 - update 必然重写
composer.lock,如果没 commit 就被覆盖,下次部署就再也回不去旧版本 - CI/CD 流水线或 Docker 构建中出现
composer update,等于把版本控制权交给网络延迟和镜像同步状态
凌晨告警经常就来自这儿:某次 deploy 脚本里多敲了一个 update,而那个时刻 Packagist 正在同步一个带 break change 的 patch。
lock 文件不提交 = 放弃可重现性
composer.lock 不是临时文件,它是 Composer 保证可重现性的唯一凭证:
- Git 忽略它,等于每个人跑
composer install得到的是不同时间点的依赖快照 - 团队里有人用 PHP 8.2,有人用 8.3,若没 lock,
install可能拉到不同版本的symfony/console(因平台约束触发不同分支选择) - 安全补丁落地必须靠
composer update+ 提交新 lock,而不是靠“大家记得手动升一下”
最常被忽略的一点:lock 文件里不仅存版本号,还存了每个包的完整下载 URL 和 SHA256 哈希。这意味着即使 packagist.org 下线,只要你有 lock,就能从本地缓存或镜像还原整个 vendor 目录——前提是它被提交了。











