ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP5.6老项目怎么平滑升级到PHP8

PHP5.6老项目怎么平滑升级到PHP8 前言一个跑了七八年的 PHP 5.6 项目要升到 PHP 8第一天的场景通常是这样的把服务器上的 PHP 换掉页面直接白屏日志里躺着PHP Fatal error: Uncaught Error: Call to undefined function mysql_connect() PHP Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given PHP Fatal error: Uncaught Error: Undefined constant STATUS_PAID这些都不是「运气不好」而是 5.6 到 8.x 这四年多里累积的必然结果。中间删掉整批函数mysql_*、ereg*、split、each、create_function等把一大批原本只是Warning的情况升级成了异常还改动了两处非常隐蔽的运算符语义。一次性全部对上是不可能的任务。可行的方法只有一个搭一条升级阶梯一次只跨一小步。另外先记一个前提——PHP 5.6 早在 2018 年底就停止了支持PHP 7.x 全系列也已停止安全维护所以目标建议直接定在 PHP 8.1 或更高。本文按阶梯拆解每个阶段要改什么、怎么验证最后给出一段可直接运行的全项目自检脚本。一、搭升级阶梯五步走阶段目标版本核心任务验收标准05.6现状补齐测试、打开E_ALL报错、把错误日志收集起来有可重复运行的测试基线17.0清掉被删除的函数与语法最大的一批php -l全通过无Fatal error27.4处理弃用项each、create_function、花括号下标弃用警告清零38.0应对「警告变异常」与比较语义变化测试全绿无未捕获TypeError48.1 → 8.5收尾弃用null 传参、动态属性、隐式可空参数日志里没有Deprecated阶段 0 最容易被跳过也最致命。老项目往往没有任何自动化测试正确做法不是「先升了再说」而是先给最核心的三五条业务路径补上测试。没有测试的升级等于闭着眼改代码。工具链准备# 用 Docker 同时跑多个 PHP 版本改造过程中随时对照 docker run --rm -v $PWD:/app -w /app php:7.4-cli php -l index.php docker run --rm -v $PWD:/app -w /app php:8.1-cli php -r echo PHP_VERSION, PHP_EOL; # 语法检查只查语法不执行升级阶段最安全的批量工具 find . -name *.php -not -path ./vendor/* -print0 \ | xargs -0 -n1 -P4 php -lphp -llint是个被低估的工具它不执行代码能在升级前就把「某个语法在新版本被移除」的问题一次性全部找出来几十万行的项目也就跑几十秒。再配合两个静态分析工具PHPCompatibilityPHP_CodeSniffer 的规则集专门检查跨版本兼容性和 Rector自动重写代码能把大部分弃用写法直接改掉。它们的具体命令随版本变化较大装好后用--help看用法即可不要照抄旧文档。二、阶段 15.6 → 7.0 要先清掉的删除项PHP 7.0 是删除最狠的一个版本下面这些在 5.6 里还能跑的东西7.0 起直接消失被移除替代方案备注mysql_*全系列mysqli或PDO_MySQL5.5 就弃用了7.0 才真正删掉ereg/eregi/ereg_replace/eregi_replacepreg_match等正则语法要从 POSIX 改成 PCREsplit/splitipreg_split/explodepreg_replace的/e修饰符preg_replace_callback当年被大量滥用也是最常见的代码注入源magic_quotes_*相关函数自己用预处理语句注册全局变量时代产物% %与script languagephp标签?php ... ?模板文件里偶尔还留着 new对new结果取引用直接赋值PHP 5 起对象就是引用语义了这一阶段的典型事故是「ereg换preg正则没改」POSIX 正则里(是字面量PCRE 里是分组换过去以后匹配结果完全不同而且是静默不同——不报错只是结果错了。这类改动必须靠测试兜底。同时 PHP 7.0 引入了Throwable接口和Error类以前抓不住的致命错误现在能用catch接住了可以在入口加一层兜底?php declare(strict_types1); // 需要 PHP 7.0Throwable 接口 set_exception_handler(static function (Throwable $e): void { error_log(sprintf( [%s] %s: %s in %s:%d\n%s, date(c), $e::class, $e-getMessage(), $e-getFile(), $e-getLine(), $e-getTraceAsString() )); http_response_code(500); echo 服务暂时不可用; });这段代码在 5.6 上写不出来没有Throwable也没有$e::class这种写法但它是升级过程中最有价值的调试工具所有漏网的错误都会被记进日志而不是变成一片空白页。三、阶段 27.x 期间的弃用项7.x 的弃用项到 8.0 才真正删除但必须在 7.x 阶段就改完否则跳 8.0 时会集中爆炸弃用 → 移除版本变化替代each()7.2 弃用8.0 移除foreachcreate_function()7.2 弃用8.0 移除匿名函数assert()传字符串会被 eval7.2 弃用8.0 移除传布尔表达式未加引号的裸字符串常量7.2 弃用8.0 报Error加引号track_errors与$php_errormsg7.2 弃用8.0 移除error_get_last()get_magic_quotes_gpc()7.4 弃用8.0 移除直接当作falsemoney_format()7.4 弃用8.0 移除NumberFormatter花括号下标$str{0}、$arr{0}7.4 弃用8.0 移除$str[0]implode($glue, $array)参数写反7.4 弃用8.0 移除正确顺序array_key_exists()传对象7.4 弃用8.0 移除property_exists()(real)强制转换、is_real()7.4 弃用8.0 移除(float)、is_float()「未加引号的裸字符串常量」要单独说老代码里$status ACTIVEACTIVE本该是ACTIVE在 5.6 上只是当字符串用7.2 给一条警告到 8.0 直接抛Error: Undefined constant。一个引号缺失就是一次 500而且这类问题php -l查不出来只能靠运行期日志或静态分析工具。花括号下标同理$row{0}在老项目里非常常见尤其是做字符串截取的报表代码8.0 下直接被移除。四、阶段 37.4 → 8.0从「警告」到「异常」的鸿沟这是体感最剧烈的一步PHP 8.0 把一大批原本「出个警告继续跑」的行为改成了抛异常。变化5.6/7.x 的表现PHP 8.0 起count()传null或标量Warning返回1TypeError未定义常量Warning当作字符串用Error抑制符作用于致命错误吞掉错误不再生效substr()越界返回false返回数字与非数字字符串比较0 foo为true0 foo为false.与/-的优先级同级左结合.优先级更低在无父类的类里调用parent::报错信息含糊Errorassert()传字符串字符串被eval移除两条最隐蔽的语义变化值得单独展开比较语义。0 foo在 PHP 8.0 起是false。这会影响所有「用松散比较判断空值」的老代码?php declare(strict_types1); // 需要 PHP 8.0 $status $_GET[status] ?? ; // ❌ 想判断「状态为空」但 0 以前是 true8.0 起变成 false if ($status 0) { echo 空状态; } // ✅ 明确表达意图 if ($status || $status 0) { echo 空状态; }点号的优先级。PHP 8.0 起.字符串连接的优先级低于和-。echo 总计 . $a $b;在 7.4 下解析为(总计 . $a) $b得到的是一段数字在 8.0 下解析为总计 . ($a $b)是正确字符串。同一个表达式两个版本结果不同而且都不报错。升级后如果发现拼接输出对不上先怀疑这一条。五、阶段 48.1 到 8.5 的收尾弃用到了这一步项目已经能跑剩下的工作是把弃用警告清零否则下一次升级还得重来一遍。弃用项版本处理方式把null传给内部函数的非可空参数8.1显式转换mb_strlen((string) $v)Serializable接口8.1改用__serialize()/__unserialize()动态属性对未声明属性赋值8.2声明属性或加#[\AllowDynamicProperties]${var}字符串插值8.2改成{$var}utf8_encode()/utf8_decode()8.2改用mb_convert_encoding()隐式可空参数function f(string $x null)8.4改成?string $x null第 3 条对老项目杀伤力最大靠$this-xxx $y动态挂属性的代码在 8.2 下会开始刷Deprecated: Creation of dynamic property每个请求几十条。六、完整可运行示例兼容性自检脚本把下面这段存成compat_scan.php放在项目根目录运行PHP 7.4 及以上均可运行因此可以在升级前就先跑一遍?php declare(strict_types1); // 需要 PHP 7.4用法php compat_scan.php /path/to/project const REMOVED [ // 函数名小写 说明 mysql_connect PHP 7.0 移除改用 mysqli 或 PDO, mysql_query PHP 7.0 移除改用 mysqli 或 PDO, mysql_real_escape_string PHP 7.0 移除改用预处理语句, ereg PHP 7.0 移除改用 preg_match, eregi PHP 7.0 移除改用 preg_match 加 i 修饰符, split PHP 7.0 移除改用 preg_split 或 explode, each PHP 8.0 移除改用 foreach, create_function PHP 8.0 移除改用匿名函数, get_magic_quotes_gpc PHP 8.0 移除直接当作 false, money_format PHP 8.0 移除改用 NumberFormatter, is_real PHP 8.0 移除改用 is_float, ]; $root $argv[1] ?? __DIR__; $it new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS) ); $issues []; $scanned 0; foreach ($it as $file) { if (!$file-isFile() || $file-getExtension() ! php) { continue; } $path $file-getPathname(); if (strpos($path, DIRECTORY_SEPARATOR . vendor . DIRECTORY_SEPARATOR) ! false) { continue; // 第三方代码不归你管 } $scanned; $src (string) file_get_contents($path); $tokens token_get_all($src); // 字符串与注释不会被当成代码天然过滤 $count count($tokens); for ($i 0; $i $count; $i) { $tok $tokens[$i]; // 1) 被移除的函数调用T_STRING 后面紧跟 ( 且不在 T_FUNCTION 之后 if (is_array($tok) $tok[0] T_STRING) { $name strtolower($tok[1]); if (isset(REMOVED[$name])) { $next $tokens[$i 1] ?? null; if ($next () { // 排除函数声明本身function create_function(...) {} // 注意中间可能夹着空白要跳过空白往回找 $isDecl false; for ($j $i - 1; $j 0; $j--) { $prev $tokens[$j]; if (is_array($prev) $prev[0] T_WHITESPACE) { continue; } $isDecl is_array($prev) $prev[0] T_FUNCTION; break; } if (!$isDecl) { $issues[] sprintf( %s:%d %s() %s, $path, $tok[2], $name, REMOVED[$name] ); } } } } // 2) 花括号下标 $var{0}T_VARIABLE 紧跟 { if (is_array($tok) $tok[0] T_VARIABLE ($tokens[$i 1] ?? null) {) { $issues[] sprintf( %s:%d %s{...} 花括号下标PHP 8.0 已移除改用方括号, $path, $tok[2], $tok[1] ); } } } printf(扫描文件%d 个\n命中%d 处\n\n, $scanned, count($issues)); foreach ($issues as $line) { echo $line, \n; } // 有命中就返回非零退出码 exit($issues [] ? 0 : 1);php compat_scan.php . | head -50它用token_get_all()做词法分析而不是正则匹配字符串和注释里的mysql_query不会被误报真正的函数调用跑不掉。扫描完输出非零退出码可以直接接进 CI防止老代码被重新引入。常见坑点1. 跳过中间版本直接 5.6 → 8.0❌ 一次性跳四个大版本报错几千条完全无从下手。 ✅ 按 7.0 → 7.4 → 8.0 → 8.x 分步走每一步改完都要能跑通测试。中间版本有各自的扫描工具能用就用。2.mysql_*换成mysqli_*时没注意参数顺序❌mysql_query($sql, $link)直接改成mysqli_query($sql, $link)—— 这两个函数的参数顺序恰好相反mysqli_query(mysqli $mysql, string $query)。照抄不改就是满屏的类型错误。 ✅ 用PDO更省心它的接口在所有数据库上都一致坚持用mysqli的话每个函数都要对照官方签名逐个核对。3. 降级式地「保留」魔术引号行为❌ 当年为了兼容magic_quotes_gpc代码里到处是addslashes()/stripslashes()的成对调用。升级后魔术引号早已不存在这些调用变成了纯粹的数据破坏OBrien存进库里是O\Brien。 ✅ 全部删掉改用预处理语句prepared statement处理参数。4. 靠压掉所有警告❌mysql_query(...)、count($x)—— 以前能压住PHP 8.0 起对致命错误和异常都不再生效而且掩盖了真正需要修的地方。 ✅ 显式判断if (is_countable($x)) { count($x); }或者用??先把值归一化。5. 用宽松比较判断空值❌if ($row[status] 0)、if ($code )—— PHP 8.0 起字符串与数字的比较语义变了这些判断会静默地走向另一个分支。 ✅ 一律改成/!需要数值比较时先(int)转换。6. 忽略了点号的优先级变化❌echo id . $a $b;—— 同一行代码在 7.4 和 8.0 下结果不同而且都不报错。 ✅ 显式加括号echo id . ($a $b);。升级时建议全局搜一遍 . $后面紧跟数字变量的拼接。7. 只在开发环境验证就上线❌ 开发机没有装某些扩展gd、intl、bcmath代码里用了却被条件跳过生产环境一开就炸。 ✅ 用与生产一致的php.ini和扩展集做全量回归用composer check-platform-reqs或php -m对比两边扩展清单。总结阶段目标主要工作验证手段05.6补测试、开E_ALL、集中收集日志测试基线可重复运行17.0清mysql_*、ereg*、split、/ephp -l全量语法检查27.4清each、create_function、花括号下标、裸常量弃用警告清零38.0处理TypeError、比较语义、点号优先级测试全绿无未捕获异常48.1清 null 传参、动态属性、隐式可空参数日志无Deprecated平滑升级的关键不是「改得快」而是每一步都有客观的验收标准php -l通过、弃用警告清零、测试全绿、日志干净。达到这四个标准再进入下一步升级就从一场赌博变成了一道工序。5.6 和 7.x 早已停止安全维护拖得越久中间积累的删除项就越多。
返回列表