滚动更新时 composer update 是高危操作,因其默认忽略 composer.lock 重算整个依赖树,导致多实例间版本混搭、序列化不兼容及静默行为变更。

滚动更新中不能用 composer update 直接全量重算依赖树,必须锁定变更范围、分批验证、避免跨版本混搭。
为什么滚动更新时 composer update 是高危操作
滚动更新要求服务持续可用、配置渐进生效,而 composer update 默认忽略 composer.lock,重新协商整个依赖图——它可能把 symfony/http-foundation 从 6.4.0 升到 6.4.7,同时把共用的 psr/cache 从 3.0.0 升到 3.0.1,看似安全,实则触发底层行为变更:缓存键生成逻辑微调、HTTP 头解析顺序变化。这些不会报错,但会在某次请求中静默失效。
- CI 流水线里跑通 ≠ 线上流量下稳定,因为
composer.lock的哈希值变了,部署镜像实际加载的是新解析结果 - 多实例滚动时,旧实例还在用老
vendor/,新实例加载新依赖,若两者通过消息队列或共享缓存交互,极易出现序列化不兼容或类型断言失败 - 某些包(如
monolog/monolog)在补丁版里悄悄改了日志上下文字段名,旧代码读不到context只看到extra,监控告警就漏掉
composer update 必须带明确包名 + --with-all-dependencies 限定影响面
滚动更新只允许升级你主动声明要变的组件及其直接依赖链,不能让 Composer 自行决定要不要动 symfony/polyfill 或 doctrine/instantiator。默认行为太宽,--with-all-dependencies 反而是更可控的选择。
- 只升框架核心:
composer update laravel/framework --with-all-dependencies,它会更新illuminate/*全家桶,但不会碰phpunit/phpunit或guzzlehttp/guzzle - 若需连带升级 HTTP 客户端:
composer update guzzlehttp/guzzle laravel/framework --with-all-dependencies,Composer 会合并两棵树,只更新交集部分(比如共用的psr/http-message) - 绝对不要写
composer update --with-all-dependencies不带包名——这等于全量更新,和composer update无异 - 每次执行前加
--dry-run,重点看输出里有没有downgrading或removing,这类操作在滚动中极易引发兼容断裂
锁文件必须按环境拆分,禁止跨环境复用 composer.lock
开发机 PHP 8.2、预发机 PHP 8.3、生产机 PHP 8.3(但启用了 opcache.enable_cli=1)——同一份 composer.lock 在三个环境里解析出的实际包版本可能不同。Composer 会按当前平台 PHP 版本过滤候选包,而 lock 文件只记录“当时算出来的结果”,不记录“当时用的平台条件”。
- CI 构建阶段用
composer install --no-interaction --prefer-dist --optimize-autoloader,且该命令必须运行在目标 PHP 版本容器内 - 禁止把本地
composer.lock提交后直接用于生产部署;应由 CI 在对应 PHP 镜像中生成专属 lock 文件 - 若需临时绕过平台检查(例如测试 PHP 8.4 兼容性),用
composer update --ignore-platform-reqs,但仅限本地验证,绝不可提交到仓库 - 检查
composer.json中是否设了"config": {"platform": {"php": "8.3"}},这个配置会让所有环境强制按 PHP 8.3 解析,适合统一基线,但要注意它会屏蔽真实环境差异
滚动发布后第一件事不是测功能,而是验证 vendor/autoload.php 加载一致性
新实例启动后,即使所有类文件都在 vendor/ 里,也可能因自动加载映射未刷新导致 Class not found。这不是包没装对,是 PSR-4 映射没重建。
- 部署脚本末尾必须加
composer dump-autoload -o,-o强制生成优化版映射,否则某些嵌套命名空间(如App\Jobs\Notification\SendEmailJob)可能漏注册 - 若项目用了 Laravel,还要清配置缓存:
php artisan config:clear && php artisan cache:clear,否则AppServiceProvider的绑定可能从旧缓存读取,跳过新版本里的初始化逻辑 - 检查
composer show --installed | grep -E "(monolog|symfony|laravel)"输出的版本号,确认和composer.lock里记录的一致;不一致说明install阶段被干扰(比如--ignore-platform-reqs残留)
真正难处理的从来不是版本数字怎么变,而是两个实例之间共享状态时,一个用新包序列化数据、另一个用旧包反序列化——这种问题不会出现在 composer outdated 里,也不会在 --dry-run 日志中暴露,只能靠接口契约文档 + 跨版本序列化测试兜底。











