^1.2.3 表示 >=1.2.3 且 =0.2.3 且 =1.2.3 且 =1.2.0 且

composer.json 里 ^ 和 ~ 到底锁住哪些版本?
它们不是“大概更新”,而是有明确语义的版本范围规则,理解错就容易在 CI 或部署时突然拉到不兼容的版本。
^1.2.3 表示允许更新到 1.x.x 中所有 向后兼容 的版本,即 >=1.2.3 ;但注意:对 <code>0.x.y 版本,^0.2.3 只允许 >=0.2.3 (因为 0.x 被视为不稳定大版本)。
~1.2.3 更保守,等价于 >=1.2.3 ,只放开最小粒度的 patch 更新;而 <code>~1.2 等价于 >=1.2.0 —— 它按你写的位数截断,不是按实际发布版本位数推断。
-
^1.0.0≈ 允许升级到1.9.9,但绝不会到2.0.0 -
~1.0.0≈ 只允许升级到1.0.9,连1.1.0都被拦住 - 写
^0.5.0实际只放行0.5.x,和~0.5.0效果一样(因主版本为 0)
什么时候该用 * 或 dev-main?
* 不是“随便装最新”,而是让 composer 自由选择满足依赖图的任意版本 —— 这在本地开发可能省事,但在生产环境等于放弃版本控制权,CI 构建结果不可复现。
dev-main 是指向分支的“不稳定引用”,每次 composer update 都会重新拉取最新提交,适合内部协作调试,但绝不能出现在正式项目的 composer.json 中;一旦包作者重写 main 分支历史,你的构建就会失败或行为突变。
- CI 流水线中禁用
*,必须锁定具体版本或使用^/~显式约束 - 用
dev-main时,务必加#commit-hash后缀(如dev-main#abc123)来固定快照 -
composer require vendor/pkg:dev-main会自动写入composer.json,但下次update仍可能漂移
composer update 为什么有时跳过我的约束?
最常见原因是依赖传递:A 包要求 monolog/monolog:^2.0,B 包要求 monolog/monolog:^3.0,Composer 会尝试找一个同时满足两者的版本 —— 如果没交集,它可能降级或升级某个包来解冲突,甚至忽略你 composer.json 里写的约束。
另一个隐形干扰项是 config.platform:如果你配置了 "php": "8.1",但实际运行环境是 PHP 8.2,某些包可能因平台约束被跳过安装,导致最终解析出的版本和预期不符。
- 运行
composer update --dry-run查看将要变更的包和版本 - 用
composer prohibits vendor/package检查谁在强制拉高/拉低某个依赖 - 执行
composer update vendor/package --with-dependencies时,别漏掉--with-dependencies,否则子依赖可能卡在旧版
想彻底锁死所有依赖,只靠 composer.json 不够
composer.json 是“意向声明”,真正锁定版本的是 composer.lock 文件。只要它存在且没被删,composer install 就会严格还原记录的版本,无视 composer.json 里的 ^ 或 ~。
但这个锁文件本身有坑:git 提交时如果漏掉它,或者多人协作时有人手改了 composer.json 却没跑 update 生成新 lock,就会导致环境不一致。更隐蔽的是,lock 文件里记录的哈希值只校验 dist 包,如果包源从 dist 切到 source(比如 github repo),校验逻辑不同,可能绕过完整性检查。
- CI 中必须先
composer install,而不是update,否则 lock 文件失效 - 不要手动编辑
composer.lock,哪怕只改一行 —— 下次update会重写整个文件 - 用
composer install --no-dev时,lock 文件里require-dev部分依然存在,但不会安装;若之后运行composer update,这些 dev 包又会被拉进来
事情说清了就结束