直接写死版本号最可靠但禁自动升级;^允许主版本内向后兼容更新(如2.85.0→2.100.0),~仅限补丁级更新(如2.85.0→2.85.9),真正生效依赖composer.lock而非约束符本身。

直接写死版本号最可靠,但得清楚它会彻底禁掉自动升级;想留弹性就用 ^ 或 ~,但必须配合 composer.lock 和定期检查。
怎么用版本约束符控制升级边界
Composer 不靠“开关”控制升级,靠的是你在 composer.json 的 require 里写的版本字符串本身。不同写法语义差异极大:
-
"monolog/monolog": "2.85.0":纯数字 = 精确锁定,composer update monolog/monolog也不会动它 -
"monolog/monolog": "^2.85.0":允许 2.x 全系列更新(如升到 2.100.0),但绝不会到 3.0 -
"monolog/monolog": "~2.85.0":只允许补丁级更新(即 2.85.x),2.86.0 就不被接受 "monolog/monolog": ">=2.85.0 :显式范围,和 <code>^2.85.0效果一致,但更易读、少歧义
注意:^ 对主版本为 0 的包(如 0.5.0)行为不同——它只允许 0.5.x,不是 0.x。这点常被忽略,导致本地能跑、CI 报错。
为什么 composer outdated 显示有更新,但 update 却不升级
这是预期行为,不是 bug。常见于你用了精确版本号或 prefer-stable: true + minimum-stability: stable 的组合:
-
composer outdated是按当前composer.json的约束去查“理论上可升到哪”,不管 lock 文件里锁的是啥 -
composer update是按 lock 文件 + json 约束重新求解,如果当前已装版本就在允许范围内,就不会动 - 比如
"guzzlehttp/guzzle": "^7.5",而你已装 7.9.0,outdated可能显示 7.10.0 可用,但update不执行——因为 7.9.0 已满足^7.5
要强制拉最新兼容版,得加 --with-all-dependencies 或删掉 composer.lock 后重装(不推荐生产环境)。
如何防止间接依赖偷偷升级破坏兼容性
你没在 require 里写 psr/log,但它还是从 1.x 升到了 2.x?问题出在上游包的约束太松,而 Composer 默认让“最宽的那个约束胜出”。conflict 是目前最干净的解法:
- 在
composer.json根级加:"conflict": { "psr/log": ">=2.0.0" } - 它不是“安装一个假包”,而是在依赖解析阶段就拒绝任何匹配的版本,连安装都过不去
- 比用
require-dev“假装需要旧版”安全得多——后者会污染 autoloader、引入无用类 - 多个限制就并列写:
"symfony/console": ">=6.0", "phpunit/phpunit": ">=10.0"
这个字段只影响下一次 install 或 update,对已装好的包无感,也不改 lock 文件结构。
composer.lock 是契约,不是缓存
很多人把 composer.lock 当成可删可重生成的中间产物,这是最大隐患。它记录了:content-hash、每个包的 exact commit hash、PHP 扩展要求、甚至平台配置(如 config.platform.php)。一旦被误删或多人混用不同 Composer 版本生成,composer install 就可能失败或装出不一致结果。
真实协作中必须:
– 每次 composer update 后提交 composer.lock
– CI 流水线只运行 composer install,绝不跑 update
– 本地改完 composer.json 后,先 composer update --lock 生成新 lock,再测试,再提交
复杂点在于:同一个 composer.json,用 Composer 1.x 和 2.5+ 生成的 lock 文件内容结构不同,哈希也不同。老项目升级工具链前,务必验证 lock 文件是否仍被团队所有环境接受。


















