PHP 5.6 升 8.x:跳级就是引爆
先下结论:PHP 5.6 直接升 8.x 是一条不归路。7.0 砍 mysql_*、7.2 移除 each/create_function、8.0 收紧类型系统,三波破坏性变更叠到一起,等于一次引爆,结果是排错范围无限扩大,代价指数级上升。
正确路线只有一个:5.6 → 7.2 → 7.4 → 8.0 → 8.1/8.2。每级至少跑一轮静态扫描和测试,确认无 E_DEPRECATED 再进下一级。
先选路径,别直接动手
三种常见选型:
- 大爆炸重写:仅适合代码量 2 万行以下、测试覆盖 80% 以上、业务可以整体停摆的小项目。风险集中,回滚机会基本没有。
- 逐级就地升级:适合大多数遗留系统。每步可运行、可回滚,是这篇文章要讲的主线路。
- Strangler 绞杀者:几十万行 ERP/后台,业务不能停。边跑边替换,周期长但安全。
迁移主线
资产盘点
php -m核对扩展,确认目标版本对应包名(如 php8.2-redis)。- 检查
composer.json的 php 约束和框架版本,TP5.1 以下、Laravel 8 以下官方不支持 8.x。 - 全局搜高危函数:
mysql_、ereg、each(、create_function(、mcrypt_、__autoload。 - 备份 composer.lock、数据库、php.ini。
双运行时分流
多版本 php-fpm 并存,Nginx 按 location 切换:
location /api/new/ { fastcgi_pass php82-fpm:9000; }
location / { fastcgi_pass php56-fpm:9000; }
这个配置既是灰度入口,也是回滚开关——改 upstream 30 秒切回。
静态扫描
composer global require squizlabs/php_codesniffer phpcompatibility/php-compatibility
phpcs --standard=PHPCompatibility --runtime-set testVersion 8.2 src/
error 必须清零,warning 最晚到 7.4 阶段清完。php -l 只验语法,查兼容性还得靠 PHPCompatibility。
逐级改
- 7.2:mysql_* → PDO,ereg → preg,__autoload → spl_autoload_register。
- 7.4:each() → foreach,create_function() → 箭头函数,json_decode 第二参数禁止传 null。
- 8.0:处理 TypeError、隐式转换、parse_str 必须带第二参数、mktime() 至少 1 参。
- 8.1/8.2:readonly 冲突、动态属性弃用、内部方法重写需要兼容返回类型。
每级都开 E_ALL + E_DEPRECATED,不允许带 warning 进下一级。
重装依赖
rm -rf vendor composer.lock
composer install
composer check-platform-reqs
注意是 install 不是 update。依赖解决失败时用 composer why-not php 8.2 定位是哪只包在拦路。
测试与灰度
PHPUnit 升 9/10,老用例里用 == 比较数字字符串的会挂。先放 1% 流量跑两周,盯 php-fpm.log 和业务错误日志,再逐步放量。收尾删除 5.6-fpm,开 OPcache 与 JIT,date.timezone 写死 Asia/Shanghai。
必炸清单
按爆炸顺序排:
- mysql_*:7.0 起调用即报 Call to undefined function。替换成 PDO 时注意:拼接 SQL 不改成占位符等于没改。
- each / create_function / ereg / mcrypt:直接移除。mcrypt → openssl 时 IV 和填充逻辑不同,老数据需写兼容层。
- 松散比较语义反转:
"123a" == 123在 5.6/7.x 为 true,8.x 为 false,业务分支静默走反,不报错,最阴险。 - 类型约束生效:
function add(int $a)收"20"在 8.x 直接抛 TypeError,老代码里全是这种隐式依赖。 - parse_str:不传第二参数直接报错;
$row['name'][0]对非字符串也抛 TypeError,先判断类型。 - 动态属性弃用:未声明属性赋值在 8.1 起被弃用。加类型声明,或临时用
#[AllowDynamicProperties]。 - Serializable 弃用:改
__serialize/__unserialize;老 Session 里的旧对象会反序列化失败,清 session 或写迁移脚本。 - ext-redis 版本:5.x 以下不支持 PHP 8,表现为构造函数抛 RedisException,不是连接失败,排查方向别搞错。
- 本地/线上版本不一致:8.2 本机跑过 composer install 后,lock 文件锁入 8.2 专属语法(如枚举),线上 8.1 直接白屏。本机必须用目标 PHP 版本重新 install。
适用边界
逐级升级不是唯一答案:小代码库高测试覆盖可以考虑重写;几十万行不能停业务的必须 Strangler;其余场景逐级升级的性价比最高。判断标准是代码量、测试覆盖度、业务连续性,三个维度。
最后补一个环境判断:页面间歇 500 且无日志,先查 error_log 是不是还指向 5.6 的旧路径,8.x 下 www-data 没权限会静默吞日志。这种问题经常被当成代码 bug 查半天。