
前言从 PHP 7.1 直接升到 8.4跨了 7 个大版本。上线之后最常见的画面是首页白屏、日志里一片红报错信息却各说各话——Fatal error: Uncaught Error: Call to undefined function each()Fatal error: Array and string offset access syntax with curly braces is no longer supportedFatal error: Undefined constant APP_DEBUG明明 7.1 上只是一个警告页面能打开但金额判断、权限判断悄悄算错了没有任何报错日志被Deprecated: Creation of dynamic property、Deprecated: Implicitly marking parameter as nullable刷满。前三种是“硬失败”改完就好第四种最危险因为它是静默的行为变更第五种则是下一次升级比如 8.4 升到 9.0的预告。本文按“硬错误 → 行为变更 → 弃用告警”三类来组织给出每一类的典型报错、原因和改法并提供一个可以在 CI 里跑的扫描脚本。需要先说明升级策略不要一步从 7.1 跳到 8.4。PHP 8.0 是破坏性变更最集中的版本8.1 到 8.4 又各自追加了一批弃用。合理的路径是 7.1 → 7.4 → 8.0 → 8.1 → 8.2 → 8.3 → 8.4每一跳都让测试套件全绿再往下走。这样做看起来麻烦但每一跳的报错量都小到能当天改完。一、硬错误这些代码在 8.x 上直接跑不起来这一类是致命错误Fatal error必须改代码。报错 / 写法移除版本原因改法each()8.0 移除函数被删除改用foreachcreate_function()8.0 移除内部用了 eval 的不安全实现改用闭包function () use (...)$str{0}8.0 移除花括号下标语法被删除改用$str[0]money_format()8.0 移除依赖 locale行为不一致改用NumberFormatter或number_format()未定义常量8.0 起致命以前会当作字符串现在抛 Error补上定义或用defined()判断内部函数参数非法8.0 起抛 TypeError以前只发警告并返回 null调用前校验参数类型同名方法当构造函数8.0 移除PHP 4 风格构造器彻底删除改用__construct()几个最容易忽略的点未定义常量从“警告”变成“致命错误”这一条杀伤面极大。老代码里常见的if (DEBUG) {...}、if (ENV prod)在 7.1 上只是警告并按字符串DEBUG处理结果恒为真到 8.0 直接Error: Undefined constant。更麻烦的是它同时也是行为变更以前恒为真的判断如果你顺手把常量定义补上逻辑就可能反过来。内部函数的参数校验变严例如把数组传给strpos()、把null传给strlen()的某些用法在 8.0 会抛TypeError而不是返回null。这类问题通常出现在“入参没做校验”的旧工具函数里建议在入口处统一做类型归一化?php declare(strict_types1); // 运行环境PHP 8.0 /** 把可能是 null / 数组 / 对象的入参安全地变成字符串 */ function to_string(mixed $value): string { return match (true) { is_string($value) $value, $value null , is_scalar($value) (string) $value, is_object($value) method_exists($value, __toString) (string) $value, default , }; }二、行为变更不报错但结果变了这一类最阴险因为它不会出现在日志里。变更版本旧行为新行为字符串与数字比较变严8.00 abc为true为false数字字符串比较8.01e1 10为true为falsesort()等排序稳定8.0相等元素顺序不确定稳定排序顺序与输入一致抑制运算符8.0能抑制部分致命错误不再吞掉致命错误$GLOBALS整体赋值8.1 起受限可整体替换不允许整体写入字符串偏移越界读取8.0返回空串并警告仍返回空串但警告级别变化第一条是升级后“业务结果算错”的头号原因。旧代码里那种if ($status 0)而$status可能来自$_GET的写法在 7.1 上会把abc判成等于 0到 8.0 变成不等——如果这个判断是用来兜底“参数缺失”的升级后兜底就失效了。改法只有一条全量替换成严格比较。换成!换成!switch换成matchmatch用的是严格比较这正是它比switch更适合做类型判断的原因。用静态分析工具PHPStan、Psalm把phpVersion设为 8.4再打开它们的“严格比较”相关规则可以把大部分漏网的松散比较找出来。三、弃用告警现在只是噪音将来就是致命错误下面这些在 8.x 上只是Deprecated但如果不处理下一次大版本升级会直接变成 Fatal。弃用内容弃用版本报错文本改法把null传给非可空的内置函数参数8.1Deprecated: strlen(): Passing null to parameter先用?? 归一化float 隐式转 int丢失精度8.1Deprecated: Implicit conversion from float显式intval()并确认业务上可接受创建动态属性8.2Deprecated: Creation of dynamic property声明属性或加#[\AllowDynamicProperties]过渡${var}字符串插值8.2Deprecated: Using ${var} in strings改用{$var}utf8_encode()/utf8_decode()8.2Deprecated: Function utf8_encode() is deprecated用mb_convert_encoding()无参调用get_class()8.3Deprecated: Calling get_class() without arguments显式传入$this或对象隐式可空参数类型8.4Deprecated: Implicitly marking parameter $x as nullableint $x null改成?int $x nullE_STRICT常量8.3 起弃用、8.4 移除使用该常量时告警乃至出错从error_reporting配置里删掉其中两条值得单独说动态属性8.2 起弃用在 ORM 与老框架里极其常见。升级到 8.4 后日志会被刷屏但如果直接加#[\AllowDynamicProperties]等于把问题掩盖到下一次大版本。正确顺序是先在开发环境把告警当错误暴露出来逐个属性补声明实在来不及的类再用属性过渡。隐式可空参数8.4 起弃用的写法在 7.1 时代遍地都是// ❌ PHP 8.4 起弃用隐式把参数标成可空 function findUser(string $name, int $limit null) {} // ✅ 显式写出可空类型 function findUser(string $name, ?int $limit null) {}注意这里的区别很微妙int $limit null的默认值让参数“实际上可空”但类型声明没写?8.4 要求把意图写清楚。批量修可以用正则配合人工确认不要盲改。四、完整可运行示例升级体检脚本下面这个脚本PHP 8.4扫描指定目录把上面提到的“高危写法”按文件与行号列出来。它只做文本匹配不能替代静态分析工具但胜在零依赖、能立刻在 CI 里跑起来php upgrade_scan.php ./src?php declare(strict_types1); // 运行环境PHP 8.0在 8.4 上运行效果最好 /** * 高危模式 [说明, 建议] * 正则只做粗筛命中后仍需人工确认 */ const PATTERNS [ /\beach\s*\(/ [each() 已在 PHP 8.0 移除, 改用 foreach], /\bcreate_function\s*\(/ [create_function() 已在 PHP 8.0 移除, 改用闭包], /\$[A-Za-z_]\w*\s*\{\s*[\\d]/ [花括号字符串下标已在 PHP 8.0 移除, 改用 $str[0]], /\bmoney_format\s*\(/ [money_format() 已在 PHP 8.0 移除, 改用 NumberFormatter], /\$GLOBALS\s*\s*/ [整体写入 $GLOBALS 在 PHP 8.1 起受限, 改为逐个键赋值], /\$\{[A-Za-z_]/ [${var} 插值在 PHP 8.2 起弃用, 改用 {$var}], /\butf8_(en|de)code\s*\(/ [utf8_encode/decode 在 PHP 8.2 起弃用, 改用 mb_convert_encoding], /\bget_class\s*\(\s*\)/ [无参 get_class() 在 PHP 8.3 起弃用, 传入 $this 或对象], /function\s\w\s*\([^)]*\b(int|string|float|bool|array)\s\$\w\s*\s*null/ [隐式可空参数在 PHP 8.4 起弃用, 改成 ?int $x null], /\bE_STRICT\b/ [E_STRICT 自 PHP 8.3 弃用、8.4 移除, 从 error_reporting 中删除], /[^!][^]/ [存在松散比较 , 确认是否可以改为 ], /[^!]![^]/ [存在松散比较 !, 确认是否可以改为 !], ]; /** return string[] */ function phpFiles(string $dir): array { $result []; $it new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) ); foreach ($it as $file) { if ($file-isFile() $file-getExtension() php) { $result[] $file-getPathname(); } } sort($result); return $result; } $target $argv[1] ?? null; if ($target null || !is_dir($target)) { fwrite(STDERR, 用法: php upgrade_scan.php 源码目录\n); exit(1); } $findings 0; $files 0; foreach (phpFiles($target) as $path) { $lines file($path, FILE_IGNORE_NEW_LINES); if ($lines false) { continue; } $files; foreach ($lines as $no $line) { foreach (PATTERNS as $pattern [$what, $how]) { if (preg_match($pattern, $line) 1) { $findings; printf(%s:%d\n 问题: %s\n 建议: %s\n 代码: %s\n, $path, $no 1, $what, $how, trim($line)); } } } } printf(\n扫描完成: %d 个文件, %d 处待确认\n, $files, $findings); exit($findings 0 ? 1 : 0);输出示例textsrc/Legacy/Report.php:42 问题: each() 已在 PHP 8.0 移除 建议: 改用 foreach 代码: while (list($k, $v) each($row)) { 扫描完成: 37 个文件, 12 处待确认脚本以“有发现就返回非 0”的方式退出可以直接当成 CI 的一道关卡保证新写的代码不再引入这批高危写法。常见坑点1. 一次性从 7.1 跳到 8.4❌ 直接换镜像面对几百个报错无从下手 ✅ 按 7.1 → 7.4 → 8.0 → 8.1 → 8.2 → 8.3 → 8.4 分阶段每跳都跑通测试2. 只修 Fatal不管 Deprecated❌ 日志里Deprecated成千上万选择无视 ✅ 弃用告警是下个大版本的致命错误预告应在当次升级中清零3. 依赖0 abc的旧语义❌ 保留松散比较升级后判断静默失效 ✅ 全量改成/!用静态分析辅助排查4. 用#[\AllowDynamicProperties]一劳永逸❌ 给所有类加上该属性动态属性告警消失但技术债保留 ✅ 先补声明属性只在确有必要的过渡期使用该属性5. 忘记改composer.json的 PHP 约束❌ 代码改完了composer.json里还写着php: ^7.1依赖解析装出老版本包 ✅ 同步更新 PHP 版本约束与依赖版本6. 忽略error_reporting与 ini 的变化❌ 配置里引用E_STRICT升级到 8.4 后直接出错 ✅ 清理 ini 与代码中的已移除常量7. 升级后不重新验证扩展❌ 只关注代码忘了扩展如 mbstring、redis、gd 等也要换成对应 8.4 的版本 ✅ 用php -m与php -i对照升级前后的扩展清单8. 没有回滚方案❌ 直接在生产环境切换 PHP 版本出问题只能硬扛 ✅ 灰度切换、保留旧版本运行环境且准备好数据库层可回滚的变更顺序总结类别版本节点是否报错处理方式函数/语法移除8.0 为主Fatal error按对照表逐条替换未定义常量8.0Fatal error补定义并核对逻辑是否被改变类型比较变严8.0无报错全量改为严格比较内部函数参数校验8.0 / 8.1TypeError 或 Deprecated入口处归一化参数类型动态属性8.2Deprecated声明属性隐式可空参数8.4Deprecated写成?type $x null常量与 ini 清理8.4视用法而定删除已移除的常量引用跨大版本升级的难点不在“改得多”而在“不知道要改什么”。把报错分成硬错误、行为变更、弃用告警三层来处理硬错误按表改、行为变更靠严格比较与静态分析兜住、弃用告警用扫描脚本清零。真正决定升级成功与否的是分阶段推进和每阶段都有测试兜底这两件事。