从 PHP 8.1 升级到 8.4:要修什么,按什么顺序

PHP 8.1 没有官方的 PHP2Shell 修复版本。本文列出 8.2、8.3 和 8.4 中会让线上站点出问题的变更,并给出扫描命令和稳妥的升级顺序。

你现在的处境

PHP 8.1 在 2025 年 12 月 31 日结束支持。针对 PHP2Shell(CVE-2026-109618 和 CVE-2026-109619),PHP 项目只为 8.1 发布了源码 backport 分支,没有包含这些修复的 8.1 发布版本。已修复的版本是 8.2.35、8.3.36、8.4.27 和 8.5.12。官方支持的修复办法是往上升级。参见 PHP 8.1。如果暂时还升不了,看 这段时间能做什么。

目标版本是 8.4(8.4.27 已修复)。为什么不选 8.2,见 该选哪个版本。

如果你是从 7.4 或 8.0 升级,请看 7.4 到 8.4 升级指南,里面也包括 8.0 和 8.1 的变更。

工作顺序

  1. 备份文件和数据库,并做一次恢复测试。
  2. 搭一套运行 8.4 的预发布 (staging) 副本,扩展和 php.ini 设置与生产一致。
  3. 把 CMS、框架、插件和 Composer 包升级到声明支持 8.4 的版本。composer why-not php 8.4 会列出拦路的依赖。
  4. 运行下文的兼容性扫描。
  5. 先修 8.2 列表里的问题。动态属性和 utf8_encode() 是最常见的。
  6. 在预发布环境开启 error_reporting = E_ALL 和 log_errors = On。把登录、表单、上传、结账和定时任务都跑一遍,再看日志。
  7. 切换生产环境,重启所有 PHP worker,然后 确认正在运行的版本。

从 8.1 直接升到 8.4 没有问题。把下面三个版本小节当成一份检查清单来用。

PHP 8.2

  • 动态属性已弃用。 给类里从未声明的属性赋值,会记录一条弃用 (deprecation) 提示。请声明该属性。临时办法是在类上加 #[\AllowDynamicProperties]。stdClass 以及带 __get() 和 __set() 的类不受影响。这类提示最常来自旧插件。
  • "${var}" 和 "${expr}" 形式的插值已弃用。 改用 "{$var}" 或 "$var"。
  • utf8_encode() 和 utf8_decode() 已弃用。 改用 mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1') 或 iconv()。
  • 相对可调用写法,例如 "self::method" 和 ["parent", "method"],已弃用。改用 [self::class, 'method'] 或 self::method(...)。
  • mbstring 编码。 把 Base64、QPrint、Uuencode 和 HTML-ENTITIES 当作 mbstring 编码使用已弃用。改用 base64_encode()、quoted_printable_encode()、convert_uuencode() 和 htmlentities()。
  • 大小写转换函数忽略 locale。 strtolower()、strtoupper()、ucfirst()、ucwords() 等函数现在一律按 "C" locale 处理。带重音符号或非拉丁字母的文本,请用对应的 mb_* 版本。
  • 较小的变更。 str_split('') 返回 [],以前返回 [""]。var_export() 输出的类名前会带上反斜杠。open_basedir 挡住所有路径时,glob() 返回空数组,以前返回 false。

PHP 8.3

8.3 改动很少,所以拿它当备选目标也说得过去。

  • range() 更严格了。 步长为 0、递增范围使用负步长,以及传入 INF 或 NAN,都会抛出 ValueError。一些混合类型的调用,例如 range(5, 'z'),现在会产生 warning。
  • number_format() 在 $decimals 为负数时,现在会在小数点之前的位上舍入,以前会忽略这个值。
  • 数组键。 给空数组赋一个负数索引之后,下一个 $a[] 使用的键是这个索引加一,以前是 0。
  • 日期相关错误 改用新的 DateError 和 DateException 类,以前是 warning。
  • 栈溢出检测。 递归层数非常深时,现在可能在进程崩溃之前抛出 Error。
  • 已弃用。 不带参数调用 get_class() 和 get_parent_class(),assert_options() 和 assert.* 系列 INI 设置,只传一个参数的 ReflectionProperty::setValue(),分开传主机和端口的 ldap_connect(),以及 MT_RAND_PHP 模式。
  • 对非数字字符串做自增,例如 $s = 'a'; $s++,已被软弃用 (soft deprecated)。新代码应调用 str_increment()。
  • 移除的 INI 设置。 opcache.consistency_checks。

PHP 8.4

  • 隐式可空参数。 function f(Foo $a = null) 已弃用,改写为 ?Foo $a = null。过时的库每个请求都会触发这条提示,所以先升级依赖包,再评估你自己的代码。
  • E_STRICT 作为错误级别已删除,带 E_USER_ERROR 的 trigger_error() 已弃用。改为抛出异常。
  • CSV 函数。 依赖 fgetcsv()、fputcsv() 和 str_getcsv() 的 escape 参数默认值的写法已弃用,请显式传入。另外,分隔符或包围符不是单字节时,str_getcsv() 会抛出异常。
  • 移到 PECL 的扩展。 IMAP、OCI8、PDO_OCI 和 PSpell。从 PECL 安装,或者换用其他库。
  • Session 的 INI 设置。 修改 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 已弃用。除非确实需要,把它们从 php.ini 和 .user.ini 中删掉。
  • 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 新增的任何函数或类,都会让已经定义了同名函数或类的代码出错。polyfill 要用 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.1-8.4 \
  --ignore='*/vendor/*,*/node_modules/*' --report-full=compat-report.txt .

testVersion 8.1-8.4 会报告影响该范围内任一版本的问题,这样在 8.1 和 8.4 并存期间代码都能安全运行。切换完成后改成 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 读取目标版本。

不属于你的插件和主题代码,不要扫描,也不要改写,升级它们就行。WordPress 的操作步骤见 更改 PHP 版本。

最后审阅:2026年10月22日