精准的网络布局,能让你的营销费用花在真正有价值的地方。

十年PHP5老系统升级PHP8,30多个坑和一份迁移手册

技术分享 2026-11-07 0 阅读

一套跑了十年的PHP5系统迁到PHP8,前后踩了30多个坑。本文按评估、环境、语法、错误模型、比较与类型、数据库、接口、灰度回滚的顺序整理成可执行的迁移手册,附关键代码对照,并说明升级后接口层重构与网站、小程序、APP并行改造的思路。

十年PHP5老系统升级PHP8,30多个坑和一份迁移手册

一套 2014 年前后上线、长期运行在 PHP 5.3 / 5.4 上的业务系统,十年间不断往上叠功能:官网、小程序接口、APP 后端、内部管理后台、支付回调、第三方对接全都在里面。支撑到期是绕不开的现实问题——PHP 5.6 的官方安全支持在 2018 年 12 月结束,PHP 7.4 也在 2022 年 11 月停止维护,继续留在老版本意味着没有安全补丁、Composer 依赖装不上、云厂商镜像越来越难找。

我们把整套系统迁到了 PHP 8.1 / 8.2,过程中踩了 30 多个坑。下面按 评估 → 环境 → 代码 → 灰度 → 收尾 的顺序整理,可以直接拿去对照执行。

一、迁移评估,先量清楚有多少代码要动

迁移拖成烂尾的项目,多数不是败在改代码,而是败在开工前不知道有多少地方要改。

1. 用工具先把雷扫出来

  • PHPCompatibility:PHP_CodeSniffer 的规则集,按目标版本扫描语法与函数级不兼容,产出可统计的报告。

  • Rector:做批量自动改写,移除函数、类型声明、命名空间这类机械替换能省掉大量人工。

  • Composer 检查:看依赖的平台约束、是否还有只支持 PHP5 的包,很多时候卡住的不是自己的代码而是第三方库。

  • 扩展清单:php -m 在新旧环境各跑一次做差集,缺扩展会直接 Fatal error。

2. 把入口清单列全

至少要覆盖:网站前台与后台、小程序接口、APP 接口、支付与退款回调、定时任务(CLI)、队列消费、第三方推送、文件上传下载、导出任务。CLI 和 FPM 是两套运行环境,工作目录、include_path、内存限制、超时时间都不一样。定时任务最容易被漏掉,然后在上线第二天凌晨三点炸掉。

3. 目标版本别一步到位

建议先定在仍有安全维护的 8.1 或 8.2。直接跳到 8.4,等于把 8.x 的全量变更和 8.3、8.4 新增的废弃项叠在一起排查,定位难度翻倍。等系统稳定跑过一个版本周期,再考虑往上抬。

二、环境与扩展,先让它能跑起来

PHP5 时代的依赖现状替换方向
mysql 扩展7.0 起移除mysqli 或 PDO_MYSQL
mcrypt 扩展7.2 起移除openssl,padding 需自行对齐
ereg / split 系列7.0 起移除preg_match / preg_split
mbstring.func_overload8.0 起移除显式调用 mb_ 系列函数
老版 redis / memcached 客户端与新运行时 ABI 不兼容升级扩展或改用 phpredis 新版
gd、zip、soap、simplexml仍在,但默认编译项有变化按实际调用补齐,别依赖默认

这一步的目标很朴素:让系统能启动、能出首页、能连上库。跑不起来之前谈重构没有意义。

三、30 多个坑,按类型分组

1)被移除的语法与函数(第一批必改)

PHP5 时代写法PHP8 现状替代方案
mysql_query / mysql_fetch_array7.0 起移除mysqli 或 PDO
ereg / eregi / split / spliti7.0 起移除preg_match / preg_split
preg_replace 的 /e 修饰符7.0 起移除preg_replace_callback
each()8.0 起移除foreach 或 current / next
create_function()8.0 起移除匿名函数
__autoload()8.0 起移除spl_autoload_register 或 Composer
与类同名的方法当构造函数8.0 起不再生效改成 __construct
静态方式调用非静态方法8.0 起报 Error先实例化再调用
define 第三个参数设大小写不敏感8.0 起移除常量名统一大写
用 $str{0} 花括号取字符8.0 起移除改用 $str[0]
implode($arr, ',') 参数反序8.0 起移除implode(',', $arr)
get_magic_quotes_gpc、money_format、fgetss8.0 起移除自行处理 / NumberFormatter / strip_tags
直接使用未定义常量8.0 起抛 Error(原来是 Warning)先 define 或改 const
必选参数写在可选参数之后8.0 起签名报错调整参数顺序

另外提醒一个细节:PHP 8 开始支持 Attributes,注释里出现 #[ 开头的写法可能被解析成属性而不是注释,老代码里用 # 写的备注要顺手排查一遍。

2)错误模型变了,异常不再是异常

场景PHP8 行为处理方式
内部函数参数个数不对7.0 起抛 ArgumentCountError补全参数,别指望返回 null
参数类型不符抛 TypeError调用前显式转换
除数为零、取模为零DivisionByZeroError运算前判零
用 @ 抑制致命错误8.0 起不再吞掉改为 try / catch 或显式判断
set_error_handler 捕获 Error捕获不到配 set_exception_handler 或 catch Throwable
trim(null)、htmlspecialchars(null)8.1 起 Deprecated传参前统一转成空字符串
htmlspecialchars 默认 flags8.1 起默认含 ENT_QUOTES显式写 flags,避免输出转义结果变化

老代码习惯了「函数报个 Warning 返回 false 继续往下走」,PHP8 直接抛异常,很多原本静默失败的链路会变成 500。建议在入口统一兜一层 Throwable,把错误落到日志而不是直接吐给用户。

3)比较与类型语义,最难查的一类

表达式PHP5 / 7 结果PHP8 结果
0 == 'abc'truefalse
in_array(0, ['a','b'])truefalse
'abc' + 1(非数字字符串算术)等同 1抛 TypeError
substr 越界取值返回 false返回空字符串
count(null)07.2 起 Warning,8.0 起 TypeError

这类问题不会报错,只会让业务逻辑悄悄走错分支:订单状态判断、权限校验、金额比较都中过招。整改原则很明确——比较一律用 ===,数组查找加 true 参数,外部输入先做强转和校验。

// 旧写法:/e 修饰符在 PHP7 已移除
// $html = preg_replace('/\{(\w+)\}/e', '$vars[1]', $tpl);

// 新写法:回调里逻辑显式可控
$html = preg_replace_callback('/\{(\w+)\}/', function ($m) use ($vars) {
    return $vars[$m[1]] ?? '';
}, $tpl);

// 比较语义:PHP8 中 0 == 'ok' 为 false,老代码的分支会悄悄改变
if ((string) $status === 'ok') {
    // 明确类型,不依赖隐式转换
}

4)字符串、数组与正则的细节

  • 嵌套三元不加括号,7.4 起废弃、8.0 起直接报错,改成 if 更省事。

  • array_key_exists 传对象,7.4 起废弃,对象属性判断用 property_exists。

  • strftime / gmstrftime 在 8.1 起废弃,时间格式化统一收口到 date 或 DateTime。

  • libxml_disable_entity_loader 在 8.0 起废弃(默认已安全),老代码里那行调用删掉即可。

  • 命名参数是 8.0 的新能力,内部函数参数改过名,如果代码里已经用了命名参数要逐个核对。

  • 金额、比例这类浮点转字符串,建议统一走 sprintf 或 number_format,别依赖隐式转换。

5)数据库与编码

问题点现象处理建议
PDO 模拟预处理limit 绑定参数变成字符串导致语法错误关掉 ATTR_EMULATE_PREPARES,limit 用 PDO::PARAM_INT
ONLY_FULL_GROUP_BY老 SQL 直接报错改写 group by 字段,不要一味放宽 sql_mode
零日期 0000-00-00严格模式下写入失败统一改 NULL 或合法日期
字符集emoji、生僻字截断或乱码建库、建表、连接层统一 utf8mb4
浮点序列化精度序列化后数值对不上serialize_precision 设为 -1,金额用整数分或 decimal
// 旧写法:函数在 PHP7 已移除,PHP8 下直接 Fatal error
// $res = mysql_query('SELECT * FROM orders WHERE id = ' . $id);

// 新写法:预处理绑定,顺带堵住注入
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES   => false,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([(int) $id]);
$order = $stmt->fetch();

6)加密与外部接口,最容易上线后才发现

  • openssl 的 padding:openssl_encrypt 默认 padding 为 0,需要显式加 OPENSSL_RAW_DATA 或 OPENSSL_ZERO_PADDING。从 mcrypt 迁过来时要手动补 PKCS7,否则密文和前端的解密结果对不上。

  • curl 证书校验:CURLOPT_SSL_VERIFYPEER 默认开启,以前为了跑通把它关掉的老代码,现在要靠正确配置 CA 证书解决。

  • json_encode 失败返回 false:脏数据、非 UTF-8 字符都会触发,接口签名和回调验签最容易在这里出错,建议加 JSON_THROW_ON_ERROR 或显式检查 json_last_error。

  • 小程序与 APP 接口签名:签名依赖 JSON 序列化顺序和转义方式,升级前后要固定 JSON_UNESCAPED_UNICODE 和字段顺序,否则同一份数据算出来的签名不同。

  • XML 回调:支付类回调多用 XML,simplexml 扩展要显式安装,入口处补一层结构校验。

7)框架与第三方库,可能是最大的工作量

ThinkPHP 3.2、CodeIgniter 2、Yii1 这类老框架在 PHP8 下问题集中:大量 preg_replace /e、mysql_* 调用、动态属性赋值。常用库也有坑:PHPExcel 需要换 PhpSpreadsheet,Smarty 2 要升到新版本,老版 PHPMailer、TCPDF 基本都要跟着升。建议先做依赖矩阵,评估「升级依赖」和「局部重写」哪个更划算。

8)运行时配置

  • display_errors 关闭、log_errors 打开,错误写文件或落日志平台。

  • E_ALL 的语义在新版本有变化,别沿用十年前的 error_reporting 数值。

  • max_input_vars 默认 1000,大表单、批量提交、复杂筛选条件很容易超限导致字段丢失。

  • opcache 与 realpath_cache_ttl 要按部署方式调,代码发布后行为不一致多半出在这里。

  • date.timezone 必须显式配置,CLI 与 FPM 要一致。

9)动态属性(8.2 起)

// PHP 8.2 起,给未声明的属性赋值会触发 Deprecated
$order = new Order();
$order->extra = 1;   // Deprecated: Creation of dynamic property

// 过渡方案:确实需要动态属性时显式声明
#[AllowDynamicProperties]
class Order {}

// 更稳的做法:字段声明出来,可读性和静态检查都更好
class Order2 { public int $extra = 0; }

四、灰度与回滚,怎么保证不出事故

  1. 环境隔离:新版本先跑在独立容器或独立域名上,与生产共用一套数据库副本,不改数据结构。

  2. 自动化回归:把核心链路做成接口冒烟脚本——登录、下单、支付回调、退款、消息推送、定时任务,每次改动跑一遍,比人工点页面可靠。

  3. 灰度分流:按 Nginx upstream 权重或用户白名单切流量,先内部账号,再 1% 真实用户,观察 24 小时。

  4. 可回滚:代码双版本共存,配置开关控制走哪套运行时;涉及数据写入的改造先做兼容双写,别做不可逆变更。

  5. 看指标:error_log 新增条目、5xx 比例、接口 P95 耗时、慢查询数量、任务失败率。其中错误日志里的 Deprecated 要当成待办清单,不要当成噪音。

五、迁完之后,老系统的下一步

升级到 PHP8 只是把地基补上,不是终点。这类十年老系统通常同时扛着官网、小程序接口、APP 后端和一堆第三方对接,业务逻辑和页面渲染混在一起,改一处牵动全身。比较划算的做法是借这次迁移把接口层抽出来:统一入口、统一签名、统一日志和错误码,前端该重做的重做,小程序和 APP 走同一套 API,以后再升级运行时,影响面就从「整个系统」收缩到「一层接口」。

尊云科技(ZUNYUN.CC)在这类项目上的常规路径是:先做老系统代码体检和兼容性评估,输出改造清单与排期;再按模块渐进式升级,全程保留可回滚;新增业务需求可以和升级并行推进,网站建设、APP开发、小程序开发、API 接口对接由同一套后端支撑,二次开发和老系统局部重构也在服务范围内,避免出现「升级一次、业务停摆三个月」的局面。

六、需要有人一起看代码的话

如果手上也有一套跑了很多年的 PHP5 系统,不确定该升级还是重写,可以先做一次代码体检:把扩展依赖、被移除函数、框架版本、入口清单跑一遍,通常两三天能出一份可执行的改造范围和工作量评估。需要的话可以联系尊云科技,我们协助做兼容性排查、升级方案评估,或者直接接手接口层重构与二次开发,把风险和工期摊开来算清楚再动手。