ARTICLE DETAIL

资讯详情

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

PHP 7.2 老项目字符集报错怎么修复

PHP 7.2 老项目字符集报错怎么修复 前言老项目跑在 PHP 7.2 上某天开始出现这类现象页面中文变成??????用户昵称里的 emoji 报Incorrect string value: \xF0\x9F\x98\x80 for column ...接口返回null而不是 JSON日志里刷出Deprecated: The mbstring.func_overload directive is deprecated。这些症状看起来散其实都指向同一个层面字符集character set与编码encoding在这条链路的某一环上不一致。一条完整的链路至少有五段PHP 源文件本身、PHP 的default_charset、数据库连接的字符集、表和列的定义、以及 HTTP 响应的Content-Type。任何一段没对齐中文就会在那一环上被截断、替换或者直接报错。PHP 7.2 这个版本号是有意义的本章要讲的几个点恰好落在它身上PHP 7.2 弃用了mbstring.func_overload指令该指令在 PHP 8.0 被彻底移除并且新增了JSON_INVALID_UTF8_SUBSTITUTE选项用于在json_encode()遇到非法 UTF-8 时用替代字符而不是直接失败。这两个变化一个影响老代码怎么改一个提供了新的兜底手段。本文按链路逐段排查 → PHP 层面的编码函数 → 数据库层 → JSON 与输出 → 完整示例展开。示例在 PHP 7.2 及以上都能跑。一、链路五段逐段对齐环节检查项期望值源文件编辑器保存编码UTF-8 无 BOMPHP 配置default_charsetUTF-8数据库连接连接字符集utf8mb4表 / 列字符集与排序规则utf8mb4/utf8mb4_unicode_ciHTTP 输出Content-Typetext/html; charsetUTF-8排查时按顺序验证先确认存进去的和取出来的是否一致再确认页面显示的和数据库里的是否一致最后才轮到 PHP 代码本身。绝大多数乱码问题里PHP 代码其实没错错的是某一环的字符集设定。一个特别容易漏掉的点是源文件本身的编码。如果某个.php文件是 GBK 保存的里面写的常量字符串就是 GBK 字节再怎么设置default_charset也救不回来输出里必然出现乱码。这类文件往往就是那一个别人接手的老模块。二、PHP 层面把字符串处理函数全部换成 mb_ 系列PHP 的strlen()、substr()、strpos()是按字节工作的。UTF-8 里一个汉字占 3 个字节用substr($s, 0, 10)截出来的很可能是一个被砍掉一半的汉字显示成一个?或者乱码。同理strlen(中)返回 3 而不是 1。?php declare(strict_types1); $s 中文字符串测试; echo strlen($s), PHP_EOL; // 18字节数 echo mb_strlen($s, UTF-8), PHP_EOL; // 6字符数 echo substr($s, 0, 4), PHP_EOL; // 前半段被切断可能显示乱码 echo mb_substr($s, 0, 4, UTF-8), PHP_EOL;// 中文字符老项目在 PHP 5.x 时代常用mbstring.func_overload 2来让strlen()等函数透明地按字符工作。这条指令在 PHP 7.2 被标记为弃用并产生Deprecated警告在 PHP 8.0 已被移除。所以如果你还在用func_overload升级过程中一定会看到大量弃用警告且最终必须改代码正确的做法是不再依赖任何全局覆盖显式调用mb_*函数并显式传编码参数。需要注意mb_substr()的第四个参数是编码mb_substr($string, $start, $length, $encoding)。省略它时会用mb_internal_encoding()的值老代码里经常出现本地测试是 UTF-8、服务器上mb_internal_encoding还是 ISO-8859-1的诡异差异所以显式传 UTF-8 是最省心的写法。另外处理的起点要先确认输入确实是合法 UTF-8?php declare(strict_types1); function ensureUtf8(string $s, string $from GBK): string { if (mb_check_encoding($s, UTF-8)) { return $s; } // 不是合法 UTF-8尝试按来源编码转换//IGNORE 用于跳过无法映射的字符 $converted iconv($from, UTF-8//IGNORE, $s); if ($converted ! false) { return $converted; } // iconv 失败时退回 mb_convert_encodingPHP 5.6 支持数组形式的 from_encoding return mb_convert_encoding($s, UTF-8, $from); }mb_convert_encoding(string $string, string $to_encoding, $from_encoding null)支持$from_encoding传数组如[GBK, GB2312, UTF-8]PHP 会依次尝试检测。iconv()和mb_convert_encoding()各有所长iconv的//TRANSLIT能把无法表示的字符近似转写//IGNORE则直接跳过但两者都会丢数据转换前后一定要比对长度或校验结果。三、数据库层utf8 不等于 utf8mb4MySQL 里的utf8别名utf8mb3最多只支持 3 字节字符而 emoji 和一部分生僻汉字是 4 字节的。存进去就会报SQLSTATE[HY000]: General error: 1366 Incorrect string value: \xF0\x9F\x98\x80 for column nickname at row 1修法分三步缺一不可-- 1. 表与列示例 ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 库默认字符集新表生效 ALTER DATABASE app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;; 3. 服务端配置 /etc/my.cnf 或 my.ini改完要重启 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci [client] default-character-set utf8mb4连接层是最容易被忽略的一环。只在 PHP 里执行SET NAMES utf8mb4是不够的一些驱动会用连接配置里的字符集去做字符串转义mysqli_real_escape_string、PDO 的引号处理两边不一致时会产生宽字节注入风险。正确做法是让驱动层知道字符集?php declare(strict_types1); // PDO把 charset 写进 DSN $pdo new PDO( mysql:host127.0.0.1;dbnameapp;charsetutf8mb4, app, secret, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES false, ] ); // mysqli用 set_charset()而不是 exec(SET NAMES ...) $mysqli new mysqli(127.0.0.1, app, secret, app); $mysqli-set_charset(utf8mb4); echo $pdo-query(SELECT character_set_client)-fetchColumn(), PHP_EOL; echo $pdo-query(SELECT character_set_connection)-fetchColumn(), PHP_EOL;注意mysql_*系列函数mysql_query、mysql_set_charset在PHP 7.0 就已经整组移除PHP 7.2 上根本不存在。如果你正在维护的项目里还有这些函数名说明它还没真正跑在 7.2 上那才是升级的第一道坎。四、JSON 与输出失败必须能被看见json_encode()遇到非法 UTF-8 时返回false不抛异常、不打警告。接口返回null这类什么都没说的故障很多时候根因就在这。从PHP 7.2 起可以加JSON_INVALID_UTF8_SUBSTITUTE让非法字节替换成从而拿到部分结果而不是整体失败?php declare(strict_types1); $bad 正常前缀\xC3\x28非法字节; $j1 json_encode([v $bad]); var_dump($j1); // bool(false) $j2 json_encode([v $bad], JSON_INVALID_UTF8_SUBSTITUTE); var_dump($j2 ! false); // bool(true) echo json_last_error_msg(), PHP_EOL; // 能拿到可读的错误描述用JSON_INVALID_UTF8_SUBSTITUTE只是让接口不至于整体失败它掩盖了数据里有坏字节这个事实。正确姿势是先修数据源再把这个选项当作兜底同时用json_last_error()/json_last_error_msg()记录日志。HTTP 输出侧要显式声明编码不要依赖浏览器猜?php declare(strict_types1); header(Content-Type: text/html; charsetUTF-8); // 输出转义时显式给编码不要省略第三个参数 echo htmlspecialchars($userInput, ENT_QUOTES, UTF-8);htmlspecialchars()的第三个参数是编码省略时会用default_charsetPHP 5.6 起默认就是UTF-8。但显式写出来能避免某个环境default_charset被 ini 文件改成了别的值这类只在生产出现的差异。实战完整可运行示例下面这段程序不依赖数据库在 PHP 7.2 及以上都能运行覆盖了字节/字符长度差异、非法 UTF-8 的检测与转换、JSON 的两种失败模式以及编码声明。?php declare(strict_types1); mb_internal_encoding(UTF-8); // 显式设置不依赖 php.ini header(Content-Type: text/plain; charsetUTF-8); echo PHP 版本: , PHP_VERSION, PHP_EOL; echo mb_internal_encoding: , mb_internal_encoding(), PHP_EOL; echo iconv 可用: , var_export(function_exists(iconv), true), PHP_EOL, PHP_EOL; $s 中文字符串测试; echo 1. 字节 vs 字符 \n; printf(strlen() %d\n, strlen($s)); printf(mb_strlen() %d\n, mb_strlen($s, UTF-8)); printf(substr 前 4 字节 : %s\n, substr($s, 0, 4)); printf(mb_substr 前 4 字符: %s\n, mb_substr($s, 0, 4, UTF-8)); echo \n 2. UTF-8 合法性检测与转换 \n; $good 中文; $bad 中文\xC3\x28; // 被截断的 UTF-8 序列 printf(good 合法 UTF-8: %s\n, var_export(mb_check_encoding($good, UTF-8), true)); printf(bad 合法 UTF-8: %s\n, var_export(mb_check_encoding($bad, UTF-8), true)); // GBK 与 UTF-8 互转 $gbk iconv(UTF-8, GBK, 测试文本); if ($gbk ! false) { printf(UTF-8 → GBK 字节数: %d\n, strlen($gbk)); $back iconv(GBK, UTF-8, $gbk); printf(GBK → UTF-8 内容 : %s\n, $back); printf(往返一致: %s\n, var_export($back 测试文本, true)); } // 无法映射的字符//IGNORE 会丢弃//TRANSLIT 会近似转写 $emoji OK; printf(iconv //IGNORE : %s\n, var_export(iconv(UTF-8, GBK//IGNORE, $emoji), true)); echo \n 3. json_encode 的两种失败模式 \n; $payload [name 张三, note 带坏字节\xC3\x28]; $j1 json_encode($payload); printf(默认 : %s\n, var_export($j1, true)); printf(json_last_error : %d (%s)\n, json_last_error(), json_last_error_msg()); if (defined(JSON_INVALID_UTF8_SUBSTITUTE)) { // PHP 7.2 $j2 json_encode($payload, JSON_INVALID_UTF8_SUBSTITUTE); printf(SUBSTITUTE(PHP7.2): %s\n, $j2); } else { echo 当前版本低于 7.2没有 JSON_INVALID_UTF8_SUBSTITUTE\n; } echo \n 4. 输出转义要显式给编码 \n; $input A B 引号 中文 100%; printf(htmlspecialchars: %s\n, htmlspecialchars($input, ENT_QUOTES, UTF-8)); echo \n 5. mbstring.func_overload 状态 \n; $overload ini_get(mbstring.func_overload); printf(mbstring.func_overload %s\n, var_export($overload, true)); if ($overload ! false $overload ! (int) $overload ! 0) { echo 注意该指令在 PHP 7.2 起弃用、PHP 8.0 起移除请改为显式调用 mb_* 函数\n; }输出节选 1. 字节 vs 字符 strlen() 21 mb_strlen() 7 substr 前 4 字节 : 中? - 被切断出现替换字符 mb_substr 前 4 字符: 中文字符 2. UTF-8 合法性检测与转换 good 合法 UTF-8: true bad 合法 UTF-8: false 3. json_encode 的两种失败模式 默认 : false json_last_error : 5 (Malformed UTF-8 characters, possibly incorrectly encoded) SUBSTITUTE(PHP7.2): {name:张三,note:带坏字节(}第 1 组里strlen()与mb_strlen()的差值、以及substr()截出来的替换字符就是老项目里列表页标题末尾出现一个问号的根因。第 3 组里json_last_error()返回 5正好对应JSON_ERROR_UTF8。常见坑点❌ 以为SET NAMES utf8mb4就等于设置好了连接字符集驱动层并不知道这个改动mysqli_real_escape_string()仍按连接配置的字符集转义存在宽字节注入面。✅用mysqli::set_charset(utf8mb4)或在 PDO 的 DSN 里写charsetutf8mb4。❌ 表用utf8mb4连接用utf8读的时候正常写 emoji 时报Incorrect string value因为连接层的字符集决定了服务端怎么解释这些字节。✅库、表、列、连接、客户端五处全部统一为utf8mb4。❌ 用substr()/strlen()处理中文截断在多字节字符中间产生非法 UTF-8 字节后面所有环节正则、JSON、数据库跟着连锁出错。✅统一用mb_substr()/mb_strlen()并且显式传UTF-8。❌ 依赖mbstring.func_overload让strlen()变成按字符算该指令在PHP 7.2 起弃用日志里出现DeprecatedPHP 8.0 起移除依赖它的代码在升级时行为会突然变化。✅代码里显式调用mb_*不再依赖任何全局覆盖。❌json_encode()失败只看结果返回false时不做判断直接把false写进响应体客户端收到空内容排查时毫无线索。✅每次json_encode()都检查返回值失败时用json_last_error_msg()记录PHP 7.2 可加JSON_INVALID_UTF8_SUBSTITUTE兜底。❌htmlspecialchars($s)不传编码如果环境的default_charset不是 UTF-8非 ASCII 字符会被替换成空串或乱码页面上表现为某些字直接消失。✅显式写htmlspecialchars($s, ENT_QUOTES, UTF-8)。❌ 用iconv()转码却不检查返回值遇到无法映射的字符比如 emoji 转 GBK时iconv()返回false被(string)一转换就成了空字符串数据无声丢失。✅检查返回值必要时用//IGNORE或//TRANSLIT并明确接受会丢字符这一代价转换前后对比长度。❌ 源文件用了带 BOM 的 UTF-8BOM 的三个字节会在header()之前被输出导致Cannot modify header information - headers already sent同时页面顶部多出几个不可见字符。✅保存为 UTF-8 无 BOM或在 CI 里加一条检测 BOM 的检查。总结症状最可能的环节处理中文变??????连接或表的字符集与数据不一致库/表/列/连接统一utf8mb4emoji 报Incorrect string value使用 3 字节的utf8utf8mb3升级为utf8mb4列表末尾出现半个字或问号substr()按字节截断改用mb_substr()显式传编码接口返回空 /nulljson_encode()遇非法 UTF-8 返回false判返回值 json_last_error_msg()PHP 7.2 可用JSON_INVALID_UTF8_SUBSTITUTE出现func_overload is deprecated依赖了已在 7.2 弃用的指令改为显式调用mb_*无法修改 header源文件带 BOM保存为 UTF-8 无 BOM字符集问题之所以难缠是因为每一环单独看都没错错的是环与环之间没对齐。排查时不要从代码改起而是按源文件 → PHP 配置 → 连接 → 表 → 输出的顺序逐段验证修的时候记住 PHP 7.2 这个分水岭的两个事实——mbstring.func_overload从此弃用8.0 移除JSON_INVALID_UTF8_SUBSTITUTE从此可用——就能把大部分玄学乱码变成可复现、可验证的普通 bug。
返回列表