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