老项目 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,每级跑一遍
-
切到 7.2:修
mysql_*→PDO/mysqli,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关键字冲突、动态属性弃用(类里加类型声明或#[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=1、max_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*2或function($a){...} -
ereg*/split()→preg_*/explode -
mcrypt_*→openssl_encrypt(IV/填充逻辑不同,加解密老数据要写兼容层) -
money_format→NumberFormatter -
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 存对象反序列化会失败 -
readonly、never成保留字,老项目里有叫这名字的方法/类要改名
6. 框架专项
-
ThinkPHP 5.1 及以下 :官方不支持 8.x。
think\Cache.php里create_function必须手改匿名函数,each()循环全换foreach,database.php的type不能是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-redis5.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 周(中型项目),核心不是改代码多累,而是别把三道断裂层合并成一次爆炸。