从 PHP 7.4 升级到 8.4:哪些地方会坏,按什么顺序处理

从 PHP 7.4 升级到 8.4 的逐步路线。按版本列出 8.0、8.1、8.2、8.3 和 8.4 中真正会影响线上站点的不兼容变更,并给出扫描和修复命令。

先看这里

PHP 7.4 在 2022 年 11 月 28 日结束支持。针对 PHP2Shell 的修复只以源码 backport 分支的形式存在,没有任何 7.4 发布版本包含这些修复。出路是换到仍受支持的分支。目标版本是 8.4(8.4.27 已修复)。参见 该选哪个版本 和 PHP 7.4。

7.4 到 8.4 之间的改动没法跳过,但可以一次做完。本页按版本列出每一步的变化。每一条都出自 PHP 手册的迁移指南,链接见文末来源。

行得通的顺序

  1. 备份文件和数据库,并实际做一次恢复测试。
  2. 把站点复制到运行 8.4、扩展完全相同的预发布 (staging) 环境。
  3. 把框架、CMS、插件和 Composer 包升级到声明支持 8.4 的版本。运行 composer outdated 和 composer why-not php 8.4,看是什么在拦着你。
  4. 用 PHPCompatibility 扫描你自己的代码(命令见下文)。
  5. 修复扫描结果,先处理 8.0 的条目,因为它们会让代码直接出错。
  6. 在预发布环境开启 error_reporting = E_ALL 和 log_errors = On。把每个流程都走一遍,然后看日志。
  7. 修复 8.1 到 8.4 的弃用 (deprecation) 提示。它们现在不会让代码出错,但会塞满日志,相关功能也可能在之后的版本里被移除。
  8. 切换生产环境,重启所有 PHP worker,然后 确认版本。

PHP 8.0:会让代码出错的变更

8.0 是大版本,大部分实际问题都出在这里。

  • 字符串与数字的比较。 0 == "foo" 过去为 true,现在为 false。0 == "" 和 42 == "42foo" 也一样。查找与用户输入或数据库值做松散 == 比较的地方,改用 ===。
  • 警告变成了异常。 对非数字字符串做算术运算会抛出 TypeError。对不可计数的值调用 count() 会抛出 TypeError。未定义的常量会抛出 Error(7.4 会把它当成字符串)。调用内置函数时参数个数不对会抛出 ArgumentCountError。
  • Notice 变成了 Warning。 读取未定义的变量、数组键或属性,现在会记录一条 warning。旧主题和旧插件会用这类提示把日志塞满。
  • 移除的函数。 each()、create_function()、__autoload()、money_format()、get_magic_quotes_gpc() 和 fgetss() 都已删除。改用 foreach、匿名函数和 spl_autoload_register()。
  • 移除的语法。 $str{0}(改用 $str[0])、(real) 和 (unset) 类型转换,以及与类同名的构造方法(改用 __construct())。
  • 字符串拼接的优先级。 "Sum: " . $a + $b 现在先做加法。原本想要旧顺序的地方请加括号。
  • 命名参数。 call_user_func_array() 现在会把字符串键当作参数名,以前是忽略。检查传入关联数组的调用。
  • 资源变成了对象。 curl_init() 返回 CurlHandle,GD 和 OpenSSL 函数同样返回对象。检查 is_resource($ch) 的代码现在会判断失败,改为检查 false。
  • @ 不再屏蔽致命错误。 错误处理函数里判断 error_reporting() == 0 的地方,应改为判断 error_reporting() & $errno。
  • 默认值。 error_reporting 默认是 E_ALL。PDO 的默认错误模式改为抛出异常。
  • 保留名称。 match 成了关键字,mixed 不能再用作类名。#[ 现在开始一个注解 (attribute),以前是注释。
  • 扩展与模块。 mbstring.func_overload 已删除,XML-RPC 移到了 PECL。Apache 模块名从 php7_module 改成了 php_module,旧的 LoadModule 和 IfModule 配置行因此不再匹配。
  • 8.0 中弃用的写法。 可选参数之后的必选参数、libxml_disable_entity_loader(),以及返回布尔值而非整数的排序回调。在 usort() 里用 <=>。

PHP 8.1:大多是弃用,有两处行为变化

  • 向内置函数传 null。 给不可为空的参数传 null,例如 str_contains($x, null) 或 trim($x),会记录一条弃用提示。在旧 WordPress 代码里,这是日志噪音的最大来源。用 (string) $x 转换,或者写 $x ?? ''。
  • mysqli 会抛异常。 默认错误模式从静默改成了抛出异常。依赖静默失败的调用要包上异常处理,或者用 mysqli_report(MYSQLI_REPORT_OFF); 恢复旧模式。
  • htmlspecialchars() 的默认参数 改成了 ENT_QUOTES | ENT_SUBSTITUTE,所以 ' 会变成 &#039;。
  • 内部方法的返回类型。 实现 ArrayAccess、Iterator、Countable 或 JsonSerializable 的类,需要声明兼容的返回类型,或者加上 #[\ReturnTypeWillChange] 注解,否则会出现弃用提示。
  • 只实现 Serializable 的写法已弃用。补上 __serialize() 和 __unserialize()。
  • 其他弃用。 strftime()、FILTER_SANITIZE_STRING、把 false 当数组往里追加元素,以及会丢失精度的隐式浮点转整数,例如 $a[15.5]。
  • PDO 使用 MySQL 和 SQLite 驱动时,现在以原生类型返回整数和浮点数。

PHP 8.2:动态属性

  • 动态属性已弃用。 在从未声明 newField 的类上执行 $obj->newField = 1,会记录一条弃用提示。请声明该属性。临时办法是用 #[\AllowDynamicProperties]。stdClass 不受影响。
  • "${var}" 形式的字符串插值 已弃用。改写为 "{$var}" 或 "$var"。
  • utf8_encode() 和 utf8_decode() 已弃用。改用 mb_convert_encoding()。
  • 相对可调用字符串,例如 "self::method",已弃用。改用 [self::class, 'method']。
  • 大小写转换函数,例如 strtolower() 和 ucfirst(),现在忽略 locale。处理本地化文本请用 mb_* 系列函数。
  • str_split('') 返回空数组,以前返回 [""]。

PHP 8.3:改动很少

  • range() 更严格了。步长为 0、递增范围使用负步长,以及传入 INF 或 NAN,都会抛出 ValueError。
  • number_format() 在 $decimals 为负数时,现在会在小数点之前的位上舍入。
  • 数组的第一个索引为负数时,下一个隐式键会变化。先赋值 $a[-5] 再执行 $a[],得到的键是 -4,以前是 0。
  • 日期相关错误 现在使用 DateException 和 DateError 类,以前是 warning。
  • 已弃用。 不带参数调用 get_class(),assert_options() 和 assert.* 系列 INI 设置,以及只传一个参数的 ReflectionProperty::setValue()。

PHP 8.4:可空参数的弃用

  • 隐式可空参数。 function f(Foo $a = null) 已弃用,改写为 ?Foo $a = null。旧库每次加载都会触发这条提示,所以先升级依赖包,再评估你自己的代码。
  • E_STRICT 已删除,带 E_USER_ERROR 的 trigger_error() 已弃用。改为抛出异常。
  • CSV 转义字符。 依赖 fgetcsv()、fputcsv() 和 str_getcsv() 的 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 及相关设置已弃用,SID 常量同样已弃用。
  • round() 遇到无效的舍入模式会抛出 ValueError。旧的预舍入步骤已删除,少数结果会因此改变。
  • bcrypt 的 cost 默认值从 10 改为 12。登录会变慢,已有的哈希仍能正常校验。
  • OPcache JIT 的默认值变了。如果你只靠 opcache.jit_buffer_size 开启 JIT,还需要同时设置 opcache.jit。
  • 新名称可能冲突。 8.4 新增的任何函数或类,都会让定义了同名函数或类的代码出错。把你自己的辅助函数包在 function_exists() 判断里。
  • 依赖库的最低版本提高了。 手册列出的是 libcurl 7.61.0、libxml2 2.9.4 和 OpenSSL 1.1.1。很老的服务器可能达不到。

扫描与修复命令

在工作机或 CI 里运行这些命令,别在 Web 服务器上跑。

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 7.4-8.4 \
  --ignore='*/vendor/*,*/node_modules/*' --report-full=compat-report.txt .

# 切换之后:只针对 8.4
vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 8.4 \
  --ignore='*/vendor/*,*/node_modules/*' .

上面的版本约束取自 2026 年 10 月 22 日 PHPCompatibility README 中的写法。当前的约束请以 README 为准。PHPCompatibility 10 需要 PHP_CodeSniffer 4.0.2 或更高版本。

Rector 能自动完成一部分修复。提交之前先看一遍它生成的 diff。

composer require rector/rector --dev
vendor/bin/rector                      # 提示是否创建 rector.php
vendor/bin/rector process --dry-run    # 预览
vendor/bin/rector process              # 应用修改
<?php
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withPhpSets();   // 从 composer.json 读取目标 PHP 版本

这两个工具都替代不了预发布环境里的测试。它们只读代码,从不执行代码。不属于你的插件和主题代码别去动,升级它们就行。WordPress 用户请看 更改 PHP 版本。

最后审阅:2026年10月22日