直接写死版本号才是真锁定,如"monolog/monolog": "2.11.0";改完必须执行composer update monolog/monolog更新lock文件,并提交lock到Git,CI/CD用install --locked校验,否则仍可能升级。

直接写死版本号才是真锁定
想让 monolog/monolog 永远不升级,不能写 "^2.8" 或 "~2.8.0",这些符号都会在下次 composer update 时放行新版本。必须用纯数字、无任何前缀后缀的写法:"monolog/monolog": "2.11.0"。
常见错误包括:
-
"2.11"—— 在某些 Composer 版本中会被解析为"2.11.0",但不保证一致,别赌 -
"2.11.*"—— 这是通配符,允许安装2.11.999,不是锁定 -
"dev-main as 2.11.0"——as只影响 autoload 和版本别名,不阻止dev-main被更新到新提交
改完 composer.json 后,必须立刻执行 composer update monolog/monolog,否则 composer.lock 里记录的还是旧版本或范围值。
为什么只靠 composer.lock 不够安全
composer.lock 确实能锁住当前安装状态,但它只是快照,不是契约。一旦被删、被覆盖、或 CI 流水线没拉到它(比如 GitHub Actions 默认只 checkout 代码),composer install 就会退化成按 composer.json 重新解析——这时如果里面写的还是 "^2.8",就可能装上 2.12.0。
所以关键动作有三个:
- 把
composer.lock提交进 Git,且不能加进.gitignore - CI/CD 构建时用
composer install --no-dev --no-interaction,绝不用update - 上线前加一层校验:
composer install --locked,它会严格比对composer.lock中每个包是否仍满足composer.json的约束,不满足直接失败
临时冻结某个包但保留其他更新
日常维护中,你可能只想升级 phpunit,但坚决不动 guzzlehttp/guzzle。这时候别去改 composer.json,用命令更灵活:
- 只更新指定包:
composer update phpunit/phpunit(漏掉guzzlehttp/guzzle就不会动它) - 忽略特定包(Composer 2.2+):
composer update --ignore=guzzlehttp/guzzle,但注意:如果其他包依赖更高版 Guzzle,仍会触发冲突报错 - 预览再动手:
composer update --dry-run先看哪些包会被升,确认无误再执行
别信 composer config --no-updates 这类伪配置,它不生效;也别用 require-dev “假装” require 来锁间接依赖——那会污染 autoloading,还可能引入 dev-only 类。
间接依赖和 conflict 字段的误用风险
你想锁住 guzzlehttp/guzzle 的间接依赖?别试图在 require 里重复声明它。正确做法是在根级加 conflict:
"conflict": {
"guzzlehttp/guzzle": ">=8.0.0"
}
但这只在 composer update 或 install 时起作用,用于拒绝不兼容版本,**不会回滚已安装的版本**。而且它不解决“为什么这个包被拉进来”的问题——得用 composer why guzzlehttp/guzzle 查源头。
容易踩的坑:
-
conflict写错范围,比如"guzzlehttp/guzzle": "7.*",这反而会禁止所有7.x,导致无法安装 - 以为
conflict能替代require中的精确版本——不能,两者职责不同:一个防冲突,一个定主版本 - 在私有包或未打 tag 的分支上用
conflict,效果不可控,因为 Packagist 上没对应元数据
最稳的路径始终是:根 require 里写死 + composer.lock 提交 + install --locked 校验。其他都是补丁,不是根基。


















