从 PHP 8.2 升级到 8.4:先打补丁,再升版本

PHP 8.2 在 2026 年 12 月 31 日终止支持。今天先装 8.2.35 修复 PHP2Shell,再参照 8.3 和 8.4 的变更清单与扫描命令迁到 8.4。

两件事,按这个顺序做

PHP 8.2 的安全修复持续到 2026 年 12 月 31 日,从今天算大约还有十周。之后不会再有官方版本。见 PHP 8.2。

  1. 今天: 安装 8.2.35。它包含 CVE-2026-109618 和 CVE-2026-109619 的 PHP2Shell 修复。重启所有 PHP worker,并 确认正在运行的版本。
  2. 年底之前: 迁到 8.4(在 8.4.27 中修复)。它的安全修复持续到 2028 年 12 月 31 日。对比见 选哪个版本。

就算第二件事要花几周,也先把第一件做了。同一分支内的补丁版本改动很小。跨小版本升级则需要 staging 环境和测试。

如果你的发行版或主机商还没有推出 8.2.35,修复 下面的各软件包指南列出了每家厂商已经发布的版本。用虚拟主机的话,去问主机商,并参考 虚拟主机。

升级的操作顺序

  1. 备份文件和数据库。测试一次恢复。
  2. 在 8.4 上搭一个 staging 副本,扩展和 php.ini 设置保持一致。
  3. 把 CMS、框架、插件和 Composer 包更新到声明支持 8.4 的版本。composer outdated 和 composer why-not php 8.4 会显示是什么在卡着你。
  4. 运行下面的兼容性扫描。
  5. 在 staging 上设置 error_reporting = E_ALL 和 log_errors = On。测试登录、表单、上传、结账和定时任务。看日志。
  6. 切换生产环境,重启所有 PHP worker,确认版本。
  7. 几天之内保留在面板里一键切回 8.2 的能力。

你在 8.2 上的日志里已经能看到 8.2 的废弃警告,主要是动态属性和 utf8_encode()。先修这些,因为它们在 8.4 上依然是废弃状态。

PHP 8.3 的变更

8.3 的破坏性改动不多。下面这些是实际项目里会碰到的。

  • range() 更严格了。 步长为 0、递增区间用负步长,以及无穷大或 NAN,都会抛出 ValueError。range(5, 'z') 这样的混合调用会产生警告。
  • number_format() 在 $decimals 为负数时,现在会在小数点前进行舍入,以前是直接忽略。
  • 负数数组下标。 对空数组执行 $a[-5] = 'x' 之后,下一个 $a[] 的下标是 -4,以前是 0。
  • 日期错误 现在使用 DateError 和 DateException 类,不再是警告。检查用 try/catch 包住日期解析的代码。
  • 深度递归 在调用栈接近上限时可能抛出 Error。
  • 已废弃: 不带参数的 get_class() 和 get_parent_class(),assert_options() 以及 assert.* INI 设置,只传一个参数的 ReflectionProperty::setValue(),还有 MT_RAND_PHP 模式。
  • 对非数字字符串做自增 已被软废弃(soft deprecated)。新代码请用 str_increment()。
  • 已移除: INI 指令 opcache.consistency_checks。把它从 php.ini 里删掉,否则启动时会有提示。

PHP 8.4 的变更

  • 隐式可空参数。 function f(Foo $a = null) 已废弃,要写成 ?Foo $a = null。过时的库会在每个请求里触发这条警告。先更新软件包,再评估你自己的代码。
  • E_STRICT 作为错误级别已移除。 带 E_USER_ERROR 的 trigger_error() 已废弃,改为抛出异常。
  • CSV 函数。 依赖 fgetcsv()、fputcsv() 和 str_getcsv() 的 escape 参数默认值已废弃。请显式传入 escape。
  • 移到 PECL: IMAP、OCI8、PDO_OCI 和 PSpell。如果你调用了 imap_* 函数,要安装 PECL 包或者换一个库。
  • Session 设置。 修改 session.sid_length、session.sid_bits_per_character、session.use_only_cookies、session.use_trans_sid、session.trans_sid_tags、session.trans_sid_hosts 或 session.referer_check 已废弃。SID 常量也一样。
  • mysqli: mysqli_ping()、mysqli_kill() 和 mysqli_refresh() 已废弃。
  • round() 遇到无效模式会抛出 ValueError,旧的预舍入逻辑也被删掉了,所以少数边界情况的结果会变。
  • exit() 的行为和函数一样。 它遵守 strict_types,类型无效时抛出 TypeError。
  • JIT 默认值变了。 如果你只通过 opcache.jit_buffer_size 开启 JIT,还需要设置 opcache.jit。
  • bcrypt cost 默认值从 10 改为 12。登录会变慢一些。已有的哈希仍然可以验证。
  • 命名冲突。 8.4 新增的函数或类,会让定义了同名函数或类的代码出错。用 function_exists() 保护你自己的辅助函数。
  • 最低库版本: libcurl 7.61.0、libxml2 2.9.4 和 OpenSSL 1.1.1。检查一下老服务器。mod_php 最低需要 Apache 2.4。

扫描和修复命令

在开发机或 CI 里运行。

composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"
vendor/bin/phpcs -i

vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 8.2-8.4 \
  --ignore='*/vendor/*,*/node_modules/*' --report-full=compat-report.txt .

范围 8.2-8.4 会报告影响其中任一版本的问题,这样两个版本并行运行期间代码都是安全的。切换完成后,改用 --runtime-set testVersion 8.4。^10.0.0@dev 这个约束取自 2026 年 10 月 22 日 PHPCompatibility 的 README,需要 PHP_CodeSniffer 4.0.2 或更新版本。

Rector 可以修掉其中一部分问题,比如显式可空类型。提交之前先看 diff。

composer require rector/rector --dev
vendor/bin/rector process --dry-run
vendor/bin/rector process

在 rector.php 里写 return RectorConfig::configure()->withPaths([__DIR__ . '/src'])->withPhpSets();。这样 Rector 会从 composer.json 读取 PHP 版本。

别人写的插件或主题,不要去改它们的代码,更新它们就好。WordPress 用户见 更改 PHP 版本。

最后审阅:2026年10月22日