
前言把项目从 PHP 7.4 或 8.0 升到 8.2 之后最典型的症状有三类一是页面直接白屏、接口返回 500但日志里那条Fatal error指向的却不是你的业务代码而是某个没人维护的老扩展二是首页正常一点进后台某个功能就报 Call to undefined function三是最难受的一类——什么都不报错但金额算错了、分页少了一条、JSON 里的数字变成了字符串。为什么会出现这种情况PHP 的版本升级从来不是换个解释器这么简单。它同时改变了四样东西语言语法、内置函数签名、内置函数行为、以及随发行包一起分发的扩展extension与第三方库library的版本。你的代码只占其中一小部分真正卡住升级的往往是最后两样。本文给出一套可以在五分钟内跑完的定位流程从报错分层开始到命令行体检、静态扫描最后给一个可以直接复制运行的兼容性探针脚本probe script一次把当前环境支持哪些函数、哪些类打印出来。需要说明的是本文标题里的 8.2 是目标版本文中所有某特性属于哪个版本的表述都以官方发布说明为准例如Random\Randomizer是PHP 8.2引入的json_validate()是PHP 8.3引入的而array_find()系列是PHP 8.4引入的。用探针脚本时要按这个对照来判断不要凭印象。一、先分清四个层次再决定用什么工具兼容性问题不是一个问题是四类问题。它们的症状、定位手段、修复成本完全不同。先分类能省掉一大半排查时间。层次典型症状报错级别首选定位手段语法层整个文件直接不执行白屏Parse errorphp -l逐文件语法检查API 层函数/类不存在功能整块失效Fatal errorfunction_exists/class_exists探针弃用层页面能用但日志疯狂刷屏Deprecatederror_reportingE_ALL打开全部级别行为层不报错但结果和以前不一样无静默回归测试 逐条比对发布说明前两层是硬伤好找后两层才是真正的坑。尤其第四层PHP 8.x 里有大量静默变更例如 PHP 8.0 起字符串与数字的比较规则改了0 foo从true变成了false。这条变更不会产生任何日志但会让大量if ($status 0)之类的判断走进另一个分支。另外要记住一个判断顺序先确认报错来自 PHP 本身还是来自扩展。日志里如果出现 Call to undefined function mb_substr()那不是版本问题是mbstring扩展没装如果出现 Call to undefined function each()那才是版本问题——each()在 PHP 8.0 已被移除。二、五分钟命令行体检第一步永远是拿到现在到底跑的哪个版本、装了哪些扩展这两个事实。很多所谓的兼容性问题最后发现是机器上跑的根本不是你以为的那个 PHP。# 1. 确认真的在用哪个 PHP别被 PATH 骗了 which -a php php -v php -r echo PHP_BINARY, PHP_EOL; # 2. 列出已加载扩展和升级前对比 php -m /tmp/modules-new.txt diff /tmp/modules-old.txt /tmp/modules-new.txt # 3. 逐文件语法检查语法层问题一次扫干净 find /path/to/project -name *.php -print0 | xargs -0 -n1 php -l /tmp/lint.log 21 grep -v No syntax errors /tmp/lint.log # 4. 强制打开所有错误级别再跑一次入口 php -d error_reportingE_ALL \ -d display_errors1 \ -d log_errors1 \ -d error_log/tmp/php-error.log \ /path/to/project/public/index.phpphp -l只能告诉你语法对不对它不会告诉你函数是否存在。这一点非常关键PHP 里未定义的函数是在运行时才报错的所以php -l全绿不代表代码能跑。Composer 项目还要多一条# 检查当前 PHP 版本是否满足 composer.lock 里所有包的 platform 约束 composer check-platform-reqs # 查看某个包为什么不允许装到 PHP 8.2 composer why-not php 8.2三、PHP 8.2 必须逐条核对的几个点打开error_reportingE_ALL并触发到相关代码路径后8.2 上最容易刷屏的就是下面这几条。它们都属于弃用层本身不致命但在 9.0 会变成致命错误而且很多框架的日志级别配置会把 Deprecated 直接吞掉。变更点版本触发症状处理方式动态属性dynamic property8.2 起弃用Creation of dynamic property 刷屏显式声明属性或给类加#[\AllowDynamicProperties]${var}字符串插值8.2 起弃用Using ${var} in strings is deprecated改成{$var}形式utf8_encode()/utf8_decode()8.2 起弃用调用点报 Deprecated改用mb_convert_encoding()或iconv()strtolower()/strtoupper()8.2 起行为变更土耳其语等 locale 下结果变了不要依赖setlocale()按 ASCII 语义写代码只读类readonly class8.2 新增老版本上直接 Parse error只有目标环境确实是 8.2 才能用DNF 类型8.2 新增老版本上 Parse error同上注意部署机版本是否一致这里要特别提醒不要把 PHP 8.2 的新语法写进需要同时兼容 8.0 的代码库。DNF 类型形如括号包裹的交叉类型再与联合类型组合和只读类是 8.2 才有的#[\Override]属性是 8.3 才有的属性钩子Property Hooks是 8.4 才有的。这些语法一旦写进去在老版本上是 Parse error连php -l都过不了。四、代码实战一个可运行的兼容性探针下面的脚本不依赖任何框架和第三方库存成probe.php直接跑它会打印当前环境的能力清单并主动触发一次弃用检测。建议把它放进 CI每次升级前跑一遍。?php declare(strict_types1); error_reporting(E_ALL); echo 环境 \n; printf(PHP 版本 : %s\n, PHP_VERSION); printf(整数位宽 : %d bit\n, PHP_INT_SIZE * 8); printf(浮点精度设置 : precision%s serialize_precision%s\n, ini_get(precision), ini_get(serialize_precision)); printf(OpenSSL : %s\n, defined(OPENSSL_VERSION_TEXT) ? OPENSSL_VERSION_TEXT : 未加载); echo \n; // 函数可用性函数名 引入版本 $functions [ str_contains 8.0, str_starts_with 8.0, fdiv 8.0, get_debug_type 8.0, json_validate 8.3, array_find 8.4, array_any 8.4, ]; echo 函数可用性 \n; foreach ($functions as $fn $since) { printf(%-16s 需要 PHP %-4s : %s\n, $fn, $since, function_exists($fn) ? 可用 : 不可用); } // 类可用性 $classes [ Random\\Randomizer 8.2, SensitiveParameter 8.2, Fiber 8.1, WeakMap 8.0, ]; echo \n 类可用性 \n; foreach ($classes as $cls $since) { printf(%-20s 需要 PHP %-4s : %s\n, $cls, $since, class_exists($cls) ? 可用 : 不可用); } // 主动探测动态属性是否触发弃用 echo \n 动态属性探测 \n; class DynProbe {} $obj new DynProbe(); $obj-notDeclared x; echo 给未声明属性赋值未中断执行\n; // 打印本次运行捕获到的弃用信息 echo \n 本次运行收集到的弃用 \n; $collected []; set_error_handler(function (int $no, string $msg, string $file, int $line) use ($collected): bool { if ($no E_DEPRECATED || $no E_USER_DEPRECATED) { $collected[] sprintf(%s (%s:%d), $msg, basename($file), $line); } return true; }); $obj2 new DynProbe(); $obj2-another 1; restore_error_handler(); if ($collected []) { echo 未捕获到弃用信息\n; } else { foreach ($collected as $line) { printf(- %s\n, $line); } }在一个确实运行 PHP 8.2 的环境上php probe.php会输出类似下面的内容具体数值取决于你的环境 环境 PHP 版本 : 8.2.20 整数位宽 : 64 bit 浮点精度设置 : precision14 serialize_precision-1 OpenSSL : OpenSSL 3.0.11 19 Sep 2023 函数可用性 str_contains 需要 PHP 8.0 : 可用 str_starts_with 需要 PHP 8.0 : 可用 fdiv 需要 PHP 8.0 : 可用 get_debug_type 需要 PHP 8.0 : 可用 json_validate 需要 PHP 8.3 : 不可用 array_find 需要 PHP 8.4 : 不可用 array_any 需要 PHP 8.4 : 不可用 类可用性 Random\Randomizer 需要 PHP 8.2 : 可用 SensitiveParameter 需要 PHP 8.2 : 可用 Fiber 需要 PHP 8.1 : 可用 WeakMap 需要 PHP 8.0 : 可用 动态属性探测 给未声明属性赋值未中断执行 本次运行收集到的弃用 - Creation of dynamic property DynProbe::$another is deprecated (probe.php:71)这张输出就是一份体检报告json_validate显示不可用说明当前环境低于 8.3任何依赖它的代码都必须有降级分支。五、静态扫描把运行时问题提前到编译期探针只能覆盖你跑到的代码路径真正的全量排查要靠静态分析。三个工具的分工是这样的# 1. PHP_CodeSniffer PHPCompatibility专扫版本兼容性 composer require --dev phpcompatibility/php-compatibility vendor/bin/phpcs -p . \ --standardPHPCompatibility \ --runtime-set testVersion 8.2- # 2. PHPStan / Psalm类型与未定义符号 vendor/bin/phpstan analyse src --level5 # 3. Rector自动改代码把弃用写法升级成新写法 vendor/bin/rector process src --dry-runPHPCompatibility的价值在于它会读你composer.json里声明的testVersion然后逐条比对每个函数、每个常量、每处语法的引入版本。--runtime-set testVersion 8.2-这个写法表示目标是 8.2 及以上此时它会同时报告8.2 里被移除的写法和比 8.2 更新的写法后者是防止你误用高版本语法。Rector 的--dry-run一定要先跑它会直接改文件不加这个参数就跑等于在没有备份的情况下动手术。常见坑点1. 只看 Fatal忽略 Deprecated❌error_reporting(E_ALL ~E_DEPRECATED);把弃用全关掉日志确实干净了升级到 9.0 时一次性全爆。✅ 开发环境保持E_ALL在php.ini里单独给弃用降级只用于生产error_reporting E_ALL ~E_DEPRECATED只写在生产的php.ini代码里不要硬编码。2. 用php -l当成兼容性检查❌php -l全绿就认为升级完成。它只做语法解析不解析函数是否存在。✅ 语法检查 探针脚本 PHPCompatibility 三件套一起跑。3. 本地 Windows 通过线上 Linux 挂❌ 类文件名UserModel.php里写class usermodel在 Windows 上 PSR-4 自动加载能命中Linux 上直接 Class not found。✅ 文件名、命名空间、类名三者的大小写必须严格一致并在 CI 的 Linux 容器里跑一遍测试。4. 只升 PHP不升扩展❌ 升级了 PHP 但redis、igbinary、imagick还是编译给 7.4 的.so加载时报 API version mismatch。✅ 升级前先php -m存档升级后用diff对比缺哪个补哪个。5.display_errors关了就当没报错❌ 生产环境display_errorsOff且log_errorsOff白屏后完全无迹可循。✅ 保持display_errorsOff但log_errorsOn并确保error_log指向一个你真的会去看的文件。6. 用抑制符掩盖问题❌$cache-get($key)连 Call to undefined method 都被吃掉只表现为返回值恒为null。✅ 用method_exists()或try/catch显式处理把留给确实无法避免的场景。7. 用--ignore-platform-reqs强行装包❌composer install --ignore-platform-reqs绕过版本检查装进来的包在运行时才炸。✅ 先用composer check-platform-reqs看清楚哪一条不满足再决定是升包还是降 PHP。8. 直接复用旧php.ini❌ 把 7.4 的php.ini原样拷给 8.2里面有些指令已被移除启动时抛 Unknown directive。✅ 以新版php.ini-production为基准只把确认仍存在的自定义项搬过去。总结层次检出手段是否报错建议动作语法层php -lParse error升级前全量扫一遍API 层function_exists/class_exists探针Fatal error关键函数加降级分支弃用层error_reportingE_ALL 日志Deprecated纳入 CI 告警行为层回归测试 发布说明逐条比对无补单元测试覆盖定位 PHP 8.2 兼容性问题的核心思路就一句话先用命令行把版本和扩展这两个事实钉死再用探针和静态扫描把运行时问题提前到编译期。报错永远不会骗你但静默的行为变更会——所以第四层必须靠测试来兜底而不是靠眼睛看日志。