PHP 8.5 升级生存指南:避免凌晨两点回滚的检查清单

PHP 8.5 升级生存指南:避免凌晨两点回滚的检查清单

升级 PHP 不难,被坑才难

一月初是做那种"你永远不想赶工"的工作的好时机:运行时升级。

大多数 PHP 8.x 小版本升级很顺利,但"顺利"不等于"零风险"。真正的问题通常来自:

  • 隐藏的平台约束(扩展、系统库、SAPI),
  • 能编译但行为不同的依赖,
  • 被忽略多年的警告/弃用突然淹没日志,
  • 假设回滚"很简单"的上线计划(实际上很少简单)。

这篇文章侧重实操。我不会重新讲 PHP 8.5 的新特性,比如管道操作符或 URI 处理。新特性只会作为兼容性检查点简单提及。目标是像成年人一样发布 PHP 8.5:有计划、有防护、有监控、有真正能用的回滚路径。

PHP 8.5 的稳定版于 2025 年 11 月 20 日发布,遵循正常的 PHP 支持周期。

原文 PHP 8.5 升级生存指南:避免凌晨两点回滚的检查清单

确定目标版本,定义内部支持策略

在动 CI 或 Composer 之前,先回答一个问题:

在你的组织里,这次升级"完成"意味着什么?

确定目标和截止日期

PHP 分支有两年的活跃支持,然后是两年的安全修复。

官方支持表:

  • PHP 8.5:2025 年 11 月 20 日初始发布
  • 活跃支持至 2027 年 12 月 31 日
  • 安全支持至 2029 年 12 月 31 日

支持窗口很宽裕------但你的内部截止日期通常由以下因素驱动:

  • 合规要求,
  • 托管镜像 / 基础容器,
  • 框架支持窗口,
  • 停留在"仅安全修复"阶段的成本。

定义范围(升级常常在这里无声失败)

写下什么包含、什么不包含:

包含:

  • 运行时版本升级(FPM/CLI),
  • Composer 依赖调整,
  • CI 矩阵更新,
  • 生产环境上线策略,
  • 升级后验证。

不包含(除非你明确加进去):

  • 重构代码以使用 PHP 8.5 新特性,
  • 与 PHP 8.5 无关的主要框架升级,
  • "顺手改"的重写。

如果不定义范围,"升级"会变成"重写",然后就会停滞。

提前决定兼容性策略

如果代码库需要在旧版 PHP 和 8.5 上同时运行一段时间:

  • 用 CI 强制双版本兼容,
  • 在生产环境切到 8.5 之前,不要合并 8.5 专属语法(比如 |>)。

如果做硬切换:

  • 确保上线/回滚方案万无一失,
  • 接受功能分支可能更早开始使用 8.5 专属语法。

初始审计:当前 PHP、扩展和依赖约束

大多数"PHP 升级"bug 不是语言 bug,而是环境不匹配。

快照实际运行的运行时

在生产环境运行这些命令,不是你的笔记本:

bash 复制代码
php -v
php -m
php --ini
php -i | head -n 50

如果用 PHP-FPM,还要捕获:

  • FPM pool 配置,
  • ini 覆盖,
  • 传给 FPM 的环境变量,
  • opcache/JIT 设置(这些在不同环境可能不同)。

创建可复用的"平台快照"脚本

把这样一个脚本放到 tools/php-platform-snapshot.php,在每个环境(开发/预发/生产)运行。提交脚本本身,不要让它成为口口相传的知识。

php 复制代码
<?php
declare(strict_types=1);
function ext_version(string $ext): ?string {
    $v = phpversion($ext);
    return $v === false ? null : $v;
}
$snapshot = [
    'php_version' => PHP_VERSION,
    'php_version_id' => PHP_VERSION_ID,
    'sapi' => PHP_SAPI,
    'os' => PHP_OS_FAMILY . ' ' . php_uname('r'),
    'ini_loaded' => php_ini_loaded_file(),
    'ini_scanned' => php_ini_scanned_files(),
    'extensions' => [],
];
$exts = get_loaded_extensions();
sort($exts);
foreach ($exts as $ext) {
    $snapshot['extensions'][$ext] = ext_version($ext);
}
echo json_encode($snapshot, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;

这能给你两个有用的东西:

  • 测试 8.5 时可以 diff 的"什么变了"记录,
  • 快速发现"预发能跑生产不能跑"问题的方法------往往是生产多了一个扩展或 ini 不同。

盘点 Composer 平台要求

Composer 把你的 PHP 运行时和扩展当作"平台包",依赖可以 require 它们。

有用的命令:

bash 复制代码
composer show --platform
composer check-platform-reqs
  • composer show --platform 列出 Composer 看到的平台包。
  • composer check-platform-reqs 验证你的真实服务器是否满足已安装包的 PHP/ext 要求(它会故意忽略 config.platform)。

这是在部署前发现"生产缺 ext-intl"最快的方法。

构建 CI 矩阵:在旧版 PHP 和 8.5 上运行测试(把警告当信号)

CI 是让升级变得可预测的地方。

规则:在生产稳定运行 8.5 之前,保留旧运行时在 CI 中

即使你计划硬切换,短暂的重叠期也能帮你避免"我们在生产准备好之前合并了 8.5 专属代码"。

GitHub Actions 矩阵示例(旧版 PHP + PHP 8.5)

如果用 GitHub Actions,shivammathur/setup-php 支持直接指定 '8.5'

yaml 复制代码
name: CI
on:
  push:
  pull_request:
jobs:
  tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.3', '8.5'] # 调整为你的当前版本 + 目标版本
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          tools: composer:v2
          coverage: none
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Run tests
        run: vendor/bin/phpunit

区分"错误"、"警告"和"弃用"

不要立刻把所有东西都当失败。先分类:

  • 解析错误 / 致命错误:阻止升级。
  • 行为变更:通常需要测试 + 代码修复。
  • 弃用:不一定阻止升级,但它们预示未来会坏------而且可能很快淹没日志。

PHPUnit 本身就区分 outcomes(failed/errored)和 issues(warnings、risky tests 等)。

一个实用的升级策略:

  • 即使存在弃用,也让测试在 8.5 上通过,
  • 但加一个单独的 CI job 来统计弃用数量并强制执行预算(比如"弃用不能超过 N 条")。

这通常比一夜之间打开"任何弃用都失败"更现实。

弃用和行为变更:像处理故障一样分类,而非当重构

PHP 8.5 新增了一批弃用特性和几个可能让人意外的不兼容变更。官方迁移指南对此有详细说明。

一个有效的分类工作流

  1. 暴露:在 CI 和预发环境启用 E_ALL。

  2. 分组:按消息签名聚类(相同文件/行/消息)。

  3. 排序:按风险优先:

    • 安全相关,
    • 运行时正确性,
    • 日志量 / 运维噪音,
    • 未来破坏可能性。
  4. 修复:最小的安全变更。

  5. 防止回归:添加针对性测试或静态规则。

到处都会冒出来的常见弃用

这些是让团队说"等等,这以前居然允许?"的问题:

非规范的类型转换名称已弃用(boolean)(integer)(double)(binary) → 用 (bool)(int)(float)(string)

快速搜索:

  • grep 搜 \(integer\) / \(boolean\) 等,
  • 应用 codemod(或 Rector------后面会讲)。

case 语句用分号结尾已弃用 (用 :)。

这常出现在"一直能跑"的老 switch 块里。

用 null 作为数组偏移(或在 array_key_exists 中)已弃用;PHP 建议用空字符串代替。

这通常指向输入规范化 bug------修根本原因,不只是修警告。

递增非数字字符串已弃用 ;用 str_increment() 代替。

如果你有代码用 $s++ 做字母递增,会看到这个。

反引号操作符已弃用(它是 shell_exec 的别名)。

即使你接受安全风险,也不想让弃用警告风暴淹没生产日志。

__sleep()__wakeup() 软弃用 ;优先用 __serialize() / __unserialize()(或在过渡期同时支持两者)。

运维相关的弃用(扩展、ini、"空操作"函数)

有些弃用是"你不再需要这个了",但仍可能让你措手不及:

  • curl_close()curl_share_close() 已弃用,因为句柄会自动释放。
  • imagedestroy() 已弃用,因为 GdImage 会自动释放。
  • finfo_close() 已弃用,因为 finfo 对象会自动释放。
  • MHASH_* 常量已弃用。
  • PDO "uri:" DSN scheme 因安全原因已弃用。

如果你运维大型应用,这些很重要,因为它们可能:

  • 日志爆炸(噪音),
  • 掩盖真正的错误,
  • 或指示值得审查的安全敏感行为(远程 URI 的 PDO DSN)。

值得尽早检查的不兼容变更

来自官方列表:

  • class_alias() 不能再用 "array" 或 "callable" 作为别名。
  • 对象到布尔的松散比较被统一为与 (bool)$object 行为一致。
  • Trait 在父类之前绑定(微妙的行为变更)。
  • 某些 attribute 目标验证错误现在在编译期发生(有选项可以延迟)。

你可能永远不会遇到这些。但如果遇到,你希望它们在 CI 立刻失败------而不是在生产环境。

Composer 策略:lock 文件、平台要求和分阶段更新

依赖管理是升级出问题的地方。

理解 Composer 在检查什么

Composer 把当前 PHP 版本建模为平台包(php),并据此检查你的依赖。

所以你有两个相关的问题:

  1. "我的依赖图能在 PHP 8.5 上解析吗?"
  2. "我的生产环境真的满足解析出的依赖图吗?"

分阶段更新依赖(不要"一次性搞定")

一个实用的分阶段方法:

阶段 A:不改变运行时,解析依赖

  • 在当前 PHP 版本上更新依赖(如果可能)。
  • 目标:减少同时出现的未知数。

阶段 B:假设在 PHP 8.5 上解析依赖

  • composer.json 中用 config.platform.php 模拟 PHP 8.5 进行依赖解析(临时的,在分支上)。
  • 然后可控地运行 composer update

阶段 C:在真实 PHP 8.5 运行时上安装

  • 在 CI/预发环境的真实 8.5 上运行完整测试套件。

正确使用 Composer 的平台检查

  • platform-check 控制在 Composer 的 autoloader bootstrap 中生成 platform_check.php
  • composer check-platform-reqs 验证真实运行时是否匹配已安装包的要求。

换句话说:

  • config.platform 来模拟和解析。
  • check-platform-reqs 来确保生产实际兼容。

升级期间会反复使用的安全更新命令

常见模式:

bash 复制代码
# 保守地更新依赖
composer update --no-interaction --with-all-dependencies

# 严格按 lock 文件安装
composer install --no-interaction --prefer-dist

如果需要追查一个有问题的包:

  • 显式更新它(加依赖),
  • 在 CI 的两个运行时上重新运行。

避免把"忽略平台要求"当成解决方案。它能让你本地脱困,但不是迁移策略。

部署前添加可观测性(这样你能证明升级是健康的)

如果不能度量,就会争论。

基线化"健康"的含义

在部署 PHP 8.5 之前,捕获:

  • 错误率(HTTP 5xx + 未捕获异常),
  • 慢请求率(p95/p99),
  • 内存使用趋势(FPM worker),
  • 队列延迟(如果你跑 worker),
  • 日志量(尤其是警告/弃用)。

临时增加日志信号(但不要让日志变成垃圾场)

在升级窗口期间,加一个短期的"升级视角"是合理的:

  • 在进程启动时记录 PHP 版本(FPM/CLI worker),
  • 给错误打上运行时标签(旧版 vs 8.5),
  • 按端点/worker 类型统计弃用数量。

在 PHP 中,你可以在前端控制器(或框架 bootstrap)里为预发环境添加一个最小的错误处理器:

php 复制代码
set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
    if (!(error_reporting() & $severity)) {
        return false;
    }
    // 示例:把弃用/警告路由到专用 logger channel
    if ($severity === E_DEPRECATED || $severity === E_USER_DEPRECATED) {
        error_log("[DEPRECATED] {$message} in {$file}:{$line}");
        return true; // 已处理
    }
    return false; // 让正常处理器运行
});

保持临时性。目标是上线期间的清晰度,不是一个永久的自定义错误框架。

确保"绕过处理器的警告"不会让你措手不及

PHP 8.5 弃用了在用户输出处理器内部产生输出的行为,并指出警告会绕过处理器以确保可见性。

如果你做自定义输出缓冲的骚操作,在预发环境用真实流量模式测试它们。

上线策略:金丝雀或蓝绿部署,加上实际能用的回滚

这是运维升级成败的关键。

金丝雀基础(最小可行的安全上线)

  1. 先把 PHP 8.5 部署到小比例的流量。
  2. 对比错误率和延迟与基线。
  3. 保持金丝雀足够长的时间以覆盖真实业务流程(不只是健康检查)。

实现取决于你的技术栈:

  • 负载均衡器后面的两个 FPM pool,
  • Kubernetes deployment 加权重路由,
  • 两个 ASG / 两个 service 加渐进流量切换。

蓝绿部署适合"大爆炸"式组织

如果你的组织偏好硬切换:

  • 保持新旧环境都在线,
  • 切换流量,
  • 保持旧环境热备以便快速回滚。

回滚计划必须包含依赖和缓存

回滚不只是"指回旧的 PHP 二进制"。

回滚现实性检查清单:

  • 保留之前的容器镜像(或包)。
  • 保留之前的 composer.lock 制品。
  • 知道切换运行时时 opcache/APCu 或框架缓存是否需要清理。
  • 确保数据库迁移是向后兼容的(或明确排除在升级范围外)。

如果回滚需要三个人和一份 wiki 页面,在压力下它会失败。

升级后检查清单:先验证正确性,再验证性能,然后清理

一旦 PHP 8.5 开始服务真实流量,你的工作还没完。现在进入"验证和稳定"模式。

即时检查(第一小时)

  • 扫描错误日志查找新的致命错误/异常。
  • 观察日志量是否突然增长(通常是弃用)。
  • 在生产主机上验证 composer check-platform-reqs
  • 用合成检查验证核心流程(登录、结账、上传、定时任务端点)。

首日检查

  • 对比 p95/p99 延迟与基线。
  • 检查内存使用:
    • FPM worker RSS 增长,
    • worker 回收频率,
    • 队列 worker 内存泄漏。
  • 验证后台任务:
    • worker 在新运行时上已重启,
    • 没有"卡住"的队列,
    • 没有静默失败。

首周清理

  • 移除你为上线视角添加的临时额外日志。
  • 把高频弃用转成跟踪的工单并逐步消灭。
  • 再次收紧 CI:
    • 提高弃用的标准,
    • 确认稳定后从矩阵中移除旧 PHP 版本。

关于 PHP 8.5 特性的快速兼容性说明(不展开教程)

PHP 8.5 引入了新的语言和运行时特性,如管道操作符、URI 扩展、clone-with 等。

升级期间不需要采用它们。但你应该:

  • 确保你的 linter/formatter 识别 PHP 8.5 语法,
  • 确保 CI 在 PHP 8.5 上运行后再合并任何 8.5 专属语法,
  • 把特性采用和运行时迁移分开(范围控制)。

结论:把升级当部署来对待,就会顺利

升级到 PHP 8.5 通常不是"重写"问题。是纪律问题:

  • 定义范围,
  • 审计真实平台,
  • 运行 CI 矩阵,
  • 系统地分类弃用和不兼容,
  • 有意识地管理依赖,
  • 上线前添加可观测性,
  • 带着策略和回滚路径部署,
  • 切换后验证和稳定。

如果你遵循这个顺序,PHP 升级就不再可怕------它只是另一个常规的运维变更。

相关推荐
BingoGo12 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack12 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
feifeigo1234 天前
matlab画图工具
开发语言·matlab
dustcell.4 天前
haproxy七层代理
java·开发语言·前端