老项目 PHP 5.6 升级 PHP 8 完整迁移步骤与兼容坑汇总

老项目 PHP 5.6 升级 PHP 8 完整迁移步骤与兼容坑汇总(2026 实战版)

PHP 5.6 自 2018 年底 EOL 至今已超 7 年,无安全补丁、Composer 2.8+/Laravel 10+/Symfony 6+ 全部拒装。从 5.6 直奔 PHP 8.x 不是"版本号 +1",而是跨过 7.0(去 mysql_)、7.2(去 each/create_function)、8.0(严格类型与 TypeError 化)三道断裂层。 正确做法是逐级跳板 + 静态扫描 + 双运行时灰度*,而不是删 vendor 裸装 PHP 8。


一、升级前决策:选哪种迁移路径

方案 适用场景 风险
大爆炸重写 代码 < 2 万行、单模块、测试覆盖 > 80% 集中暴雷,回滚难
逐级就地升级(推荐) 多数遗留系统 中,可控
Strangler 绞杀者模式 几十万行 ERP/后台,不能停业务 低,但周期长

绝大多数国内老项目(原生 PHP / TP3-5.1 / Laravel 5.x)走逐级就地升级最划算:

5.6 → 7.2 → 7.4 → 8.0 → 8.1/8.2(8.2 目前生产最稳)

为什么不直接 5.6→8.2:中间所有的 E_DEPRECATED 提示被跳过,等于把 7.0/7.2/8.0 三波破坏性变更一次性引爆,线上排错成本指数级上升。


二、迁移六步法(逐级落地)

Step 0:资产盘点(1-3 天)

  • php -m 列扩展,核对 redis/gd/curl/mbstring/opcache 在目标 PHP 版的包名(如 php8.2-redis,不再是 php7-mysqlnd

  • composer.json"php": ">=5.6" 约束,以及框架版本(TP5.1 以下不支持 8.x,Laravel < 8 不支持 8.x)

  • 全库搜高危函数:mysql_ereg_each(create_function(mcrypt___autoload$GLOBALS 整体写

  • 备份 composer.lock、数据库、当前 php.ini

Step 1:装双运行时(同一台机 / 容器)

用 php-fpm 多版本并存,Nginx 按 location 分流:

复制代码
location /api/new/ { fastcgi_pass php82-fpm:9000; }
location /          { fastcgi_pass php56-fpm:9000; }

保证老代码在线,新版本逐步接流量。

Step 2:静态扫描(比肉眼找快 100 倍)

复制代码
composer global require squizlabs/php_codesniffer
composer global require phpcompatibility/php-compatibility
phpcs --standard=PHPCompatibility --runtime-set testVersion 8.2 src/

报告里 Found X errors, Y warnings 的 error 行必须清零,warning(deprecated)在 7.4 阶段清完。

Step 3:逐级升 PHP,每级跑一遍

  1. 切到 7.2:修 mysql_*→PDO/mysqli,eregpreg__autoloadspl_autoload_register

  2. 切到 7.4:修 each()foreachcreate_function()→箭头函数/匿名函数,json_decode 第二个参数不许传 null

  3. 切到 8.0:处理 TypeError、隐式转换、parse_str 必须带第二参数、mktime() 至少 1 参

  4. 切到 8.1/8.2:处理 readonly 关键字冲突、动态属性弃用(类里加类型声明或 #[AllowDynamicProperties])、内部方法重写必须兼容返回类型

每升一级:error_reporting = E_ALL + 显式开 E_DEPRECATED,看日志跑回归,别带着 warning 进下一级。

Step 4:依赖重装(关键)

PHP 大版本变了,旧 vendor 绝对不能留

复制代码
rm -rf vendor composer.lock
# composer.json 里 "php" 改成 "^8.2"
composer install   # 不是 update,让锁文件按新平台重算
composer check-platform-reqs

遇到 your requirements could not be resolved → 用 composer why-not php 8.2 定位是哪个包拦的,升那个包或换替代品。

Step 5:测试与灰度

  • PHPUnit 升级到 9/10,老用例里用 == 比较字符串数字的会挂(见下文类型坑)

  • 先 1% 流量进 PHP 8 池,盯 php-fpm.log + 业务错误日志两周

  • 回滚只需改 Nginx upstream,30 秒切回 5.6

Step 6:生产收尾

  • 删 5.6-fpm,统一 php81/82-fpm

  • 开 OPcache:opcache.enable=1max_accelerated_files=20000,计算密集再开 opcache.jit=1255

  • 时区写死 date.timezone = Asia/Shanghai,老项目常靠隐式时区警告混过去


三、PHP 5.6 → 8.x 兼容坑汇总(按"必炸"排序)

1. 数据库层:mysql_* 全死(7.0 起)

mysql_connect / mysql_query / mysql_real_escape_string 调用即 Call to undefined function

改法:PDO 预处理或 mysqli,拼接 SQL 的写法必须重构成占位符,不能只换函数名。

2. 已删除函数(8.0 起致命错误)

  • each()foreach ($a as $k => $v)

  • create_function()fn($a)=>$a*2function($a){...}

  • ereg*/split()preg_*/explode

  • mcrypt_*openssl_encrypt(IV/填充逻辑不同,加解密老数据要写兼容层)

  • money_formatNumberFormatter

  • fgetss / image2wbmp / gmp_random → 对应新 API

3. 类型系统与隐式转换(最阴险,不报错但结果变)

  • "123a" == 123 在 5.6/7.x 为 true,PHP 8 严格比较语义下业务分支会走反

  • 函数声明 function add(int $a)"20" 旧版警告,8.x 直接 TypeError

  • json_decode($s, null) 第二个参数不允许 null,必须 true/false

  • 字符串当数组下标 $str[0] 取字符在 8.0 对非字符串变量直接 TypeError,老代码里 $row['name'][0] 要先用 is_string 守一道

4. 语法与运行时行为

  • 嵌套三元 ?:?: 必须加括号

  • 函数参数默认值后跟必填参数 function t($a=[], $b) 8.0 起 deprecated,删默认值

  • parse_str($s) 不传第二参数直接报错,必须 parse_str($s, $out)

  • mktime() 无参调用不行,用 time()

  • assert('1==2') 字符串断言不再执行,改传布尔表达式

  • usort 比较函数返回 true/false 弃用,改返回 -1/0/1 或太空船 $a<=>$b

  • foreach 按引用遍历后复用变量名,8.x 行为更严格,老"奇技淫巧"会出脏数据

5. 类与对象

  • 8.1 起类动态属性弃用:$obj->foo = 1 没在类里声明会 deprecation,加 public $foo#[AllowDynamicProperties]

  • 8.0 起内部类方法重写必须声明兼容返回类型,继承 ArrayAccess/Iterator 的老自定义类常炸

  • Serializable 接口弃用,改 __serialize/__unserialize,老 Session 存对象反序列化会失败

  • readonlynever 成保留字,老项目里有叫这名字的方法/类要改名

6. 框架专项

  • ThinkPHP 5.1 及以下 :官方不支持 8.x。think\Cache.phpcreate_function 必须手改匿名函数,each() 循环全换 foreachdatabase.phptype 不能是 mysqlnd 只能 mysql/pdo_mysql,控制器 __invoke 要补 Request 类型声明

  • Laravel 5.x :先升框架到 8(要 PHP 7.3+)→ 9(8.0+)→ 10/11(8.1+),不能只换 PHP;迁移时用 Laravel Shift 或逐版跟官方 upgrade guide,重点改 up(): void 返回类型、模型属性类型

  • 原生项目 :重点查自动加载(__autoload 死)、全局 require_once 链、低版本写的 session_register

7. 扩展 ABI 坑

  • ext-redis 5.x 以下不支持 PHP 8,构造函数失败抛 RedisException 不是连接失败

  • gd 要 2.3+,imagettftext 字体路径处理更严

  • opcache 旧配置项改名,直接复用 5.6 的 php.ini 会启动失败

  • Docker Alpine 用户:php8-mysqlnd 不存在,装 php8-mysqli php8-pdo_mysql


四、一张最小改造清单(直接抄)

复制代码
// 1. mysql_* → PDO
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', $u, $p);
$stmt = $pdo->prepare('SELECT * FROM u WHERE id=?');
$stmt->execute([$id]);

// 2. each → foreach
foreach ($arr as $k => $v) { /* ... */ }

// 3. create_function → 箭头函数
$fn = fn($a, $b) => $a + $b;

// 4. 松散比较改严格
if ((int)$_GET['id'] === $uid) {}

// 5. usort 比较函数
usort($list, fn($a, $b) => $a['sort'] <=> $b['sort']);

// 6. parse_str 必须带接收变量
parse_str($qs, $params);

// 7. 类动态属性显式声明
class Order { public $extra; }

五、常见翻车现场

  • "本地 8.2 能跑,线上 8.1 白屏" ​ → composer install 时本机 PHP 是 8.2,lock 锁了 8.2 专属语法(如枚举),线上 8.1 解不出,必须本机也用目标版本 PHP 跑 install

  • Session 反序列化报 Cannot unserialize ​ → 老 Session 文件里有 Serializable 旧对象,清 session 或写迁移脚本重放

  • 页面偶尔 500 但无日志 ​ → error_log 指向了 5.6 的旧路径,8.x 下 www-data 没权限,日志静默丢弃

  • TP 项目首页正常,后台 500 ​ → 后台模块用了 each() 遍历表单数组,前台没走到那条分支


把"逐级跳板 + PHPCompatibility 扫描 + 双运行时灰度"三条守住,PHP 5.6 升 8.2 的周期可以压到 2-4 周(中型项目),核心不是改代码多累,而是别把三道断裂层合并成一次爆炸

相关推荐
Android研究员1 小时前
Android进阶之事件分发机制深度剖析
android·前端·面试
峥嵘life3 小时前
Android WiFi连接过程 wpa_supplicant 日志分析
android·开发语言·php
Android小码家4 小时前
OWASP 移动应用安全之授权(MASTG-DEMO-0090)
android·安全·frida
Summer-Bright4 小时前
消费者 AI 变现竞争:从“一家独大“到“iOS vs Android“,谁在为 AI 买单?
android·人工智能·ios·ai·自然语言处理·agi
Xzaveir5 小时前
企业号码资产平台:逐号状态、事件日志与批次聚合
android·人工智能
寒水馨5 小时前
macOS下载、安装scrcpy-v4.1(附安装包scrcpy-macos-aarch64-v4.1.tar.gz)
macos·adb·手机控制·scrcpy·投屏软件·android投屏·屏幕镜像
峥嵘life5 小时前
Android 系统应用蓝牙分享失败分析解决
android·gitee
恋猫de小郭6 小时前
Flutter 3D 渲染的全新选择和应用场景
android·前端·flutter
—Miss. Z—6 小时前
计算机二级MySQL——简单应用题(事件&视图)
android·mysql·adb