尧图精选

PHP7.0字符串在Docker怎么用

🕒 发布时间:2026/10/1 17:45:35 📁 来源:尧图网络
前言同一段处理字符串的 PHP 代码在开发机上跑得好好的一进 Docker 容器就出问题——这是很典型的一类“环境差异”故障。常见的症状包括Fatal error: Call to undefined function mb_strlen()本地明明能用中文昵称在容器里截断后成了乱码写库时报Incorrect string valuejson_encode()返回false日志里只有一句“序列化失败”看不出哪里错setlocale(LC_TIME, zh_CN.UTF-8)一直返回false日期格式化永远是英文用了mysql_real_escape_string()的老代码在 PHP 7.0 上直接报“未定义函数”。这些问题的根子不在 Docker而在于PHP 的“字符串”本质上是一串字节而不是一串字符。本地环境恰好装了 mbstring 扩展、恰好生成了 locale、MySQL 恰好是 utf8mb4于是所有隐含假设都被满足了容器是一个干净的最小系统这些假设一个都不成立问题就全冒出来了。本文以 PHP 7.0 在 Docker 中的运行为背景讲清楚字节与字符的区别、容器里缺什么、以及怎样写出不依赖环境“恰好配好”的字符串处理代码。需要明确一点PHP 7.0 的官方安全支持在 2019 年 1 月就已经结束它只能用于维护老系统本文的结论对 7.0 到 8.x 全都成立新项目请直接使用 8.x。一、PHP 的字符串是字节数组$s 中;这个变量里存的不是“一个汉字”而是 3 个字节E4 B8 AD。所有“非 mb_ 前缀”的字符串函数都按字节工作函数作用单位对 中文abc 的结果说明strlen()字节9每个汉字 3 字节substr($s, 0, 2)字节半个汉字得到非法 UTF-8 序列mb_strlen($s, UTF-8)字符5需要 mbstring 扩展mb_substr($s, 0, 2, UTF-8)字符中文需要 mbstring 扩展preg_match(/^.$/u, 中)字符1/u修饰符打开 Unicode 模式由此可以推出两个结论第一按字节截断必然可能切碎多字节字符。substr($str, 0, 10)在英文上没问题在中文上可能产出半截字符这个非法字节序列随后会让json_encode()返回false、让 MySQL 报错、让浏览器显示成问号——错误发生的地方离真正的原因非常远这是此类问题最难查的地方。第二mysql_*系列函数在 PHP 7.0 里已经被彻底移除它们是 PHP 5.5 起废弃、7.0 移除的。如果你的老代码里还有mysql_real_escape_string()把镜像换成php:7.0之后会直接致命错误。这不是“字符串处理”的问题但它是 PHP 7.0 迁移中最常见的字符串相关报错必须一起改掉换成 PDO 或 mysqli并用预处理语句prepared statement代替手工转义。二、容器里第一个会炸的是 mbstring官方 PHP 镜像把扩展分成了两类编译进去的和需要用docker-php-ext-install现装的。哪些默认编译进去了随镜像版本而变唯一可靠的做法是先看容器自己的输出docker run --rm php:7.0-cli php -m这条命令会列出容器里真正加载的模块。如果输出的列表里没有mbstring那么任何mb_*函数都会致命错误。装它只需要两行docker run --rm php:7.0-cli php -r var_dump(function_exists(mb_strlen));在 Dockerfile 里补上FROM php:7.0-cli RUN docker-php-ext-install mbstring \ docker-php-ext-install pdo_mysqldocker-php-ext-install会编译并自动启用扩展。这里有个容易忽略的点不能只在开发时用docker run临时装因为容器重启后安装就没了——扩展必须写在 Dockerfile 里或者用 volumes 持久化否则“昨天还好好的”会周期性复发。如果环境不允许改动镜像代码侧就必须有降级路径。好消息是常见的字符串操作不一定要靠 mbstringUTF-8 的编码规则是自描述的遍历字节就能判断一个字符占几个字节。下面这个函数完全不依赖任何扩展?php declare(strict_types1); // PHP 7.0 /** * 判断 UTF-8 字符的首字节后面还跟几个续接字节 * 续接字节形如 10xxxxxx */ function utf8CharLen(int $byte): int { if ($byte 0x80) { // 0xxxxxxx ASCII return 1; } if (($byte 0xE0) 0xC0) { // 110xxxxx 2 字节 return 2; } if (($byte 0xF0) 0xE0) { // 1110xxxx 3 字节汉字在这里 return 3; } if (($byte 0xF8) 0xF0) { // 11110xxx 4 字节emoji return 4; } return 1; // 非法首字节按 1 字节跳过保证不会死循环 } /** 按“字符”截断绝不切碎多字节字符 */ function utf8Cut(string $s, int $maxChars): string { $len strlen($s); $i 0; $chars 0; while ($i $len $chars $maxChars) { $step utf8CharLen(ord($s[$i])); if ($i $step $len) { break; // 末尾残留半个字符直接丢掉 } $i $step; $chars; } return substr($s, 0, $i); }这段代码的价值不只是“能用”它也解释了mb_substr内部在做什么。容器里装了 mbstring 就优先用mb_substr()没装就用这个兜底行为可控。三、locale 与数据库字符集容器和宿主机不一样的地方locale 在容器里通常是空的。最小镜像只带C和POSIX所以下面这行在容器里返回falsevar_dump(setlocale(LC_ALL, zh_CN.UTF-8)); // bool(false)它带来的实际影响是依赖 locale 的格式化函数拿不到中文结果。想让容器支持需要在构建时生成 localeRUN apt-get update \ apt-get install -y locales \ locale-gen zh_CN.UTF-8 \ rm -rf /var/lib/apt/lists/*# 验证容器里到底有哪些 locale docker run --rm php:7.0-cli locale -a数据库字符集则是另一个高频坑。utf8在 MySQL 里最多存 3 字节emoji4 字节会直接报Incorrect string value。连接时显式指定utf8mb4是唯一稳妥的做法$pdo new PDO( mysql:hostdb;dbnameapp;charsetutf8mb4, $user, $pass, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES false, ] );注意charsetutf8mb4写在 DSN 里比事后执行SET NAMES utf8mb4更可靠因为它在连接建立时就已经生效。四、完整可运行示例把上面的要点串成一个可以直接跑的脚本保存为str_demo.php用官方镜像运行docker run --rm -v $PWD:/app -w /app php:7.0-cli php str_demo.php?php declare(strict_types1); // 运行环境PHP 7.0本文所有函数在 7.0-8.x 上行为一致 // ---------- 1. 环境体检 ---------- printf(PHP 版本 : %s\n, PHP_VERSION); printf(mbstring : %s\n, extension_loaded(mbstring) ? 已加载 : 未加载); printf(iconv : %s\n, extension_loaded(iconv) ? 已加载 : 未加载); printf(default_charset: %s\n, ini_get(default_charset)); printf(locale(zh_CN) : %s\n, var_export(setlocale(LC_ALL, zh_CN.UTF-8), true)); // mbstring 在 7.0 上不一定存在存在时显式统一内部编码 if (extension_loaded(mbstring)) { mb_internal_encoding(UTF-8); } // ---------- 2. 字节 vs 字符 ---------- $s 中文abc; printf(strlen%d 字符数%d\n, strlen($s), str_char_count($s)); // ---------- 3. 安全截断不依赖 mbstring ---------- $name 张三丰是个很长很长的名字; printf(前 3 个字符: %s\n, utf8Cut($name, 3)); printf(字节截断的后 20 字节能否 json_encode: %s\n, json_encode(substr($name, 0, 20)) false ? 否非法 UTF-8 : 是); printf(字符截断后能否 json_encode: %s\n, json_encode(utf8Cut($name, 4), JSON_UNESCAPED_UNICODE) false ? 否 : 是); // ---------- 4. 正则的 /u 修饰符 ---------- $broken substr($name, 0, 20); // 故意切碎一个汉字 $m1 preg_match(/^./u, $broken, $match); printf(preg_match(/u) 返回: %s preg_last_error%d\n, var_export($m1, true), preg_last_error()); // ---------- 5. JSON 失败要查错因 ---------- $json json_encode([name $broken], JSON_UNESCAPED_UNICODE); if ($json false) { printf(json_encode 失败: %s\n, json_last_error_msg()); } // ---------- 6. 函数定义放在最后与上面的调用无关 ---------- function str_char_count(string $s): int { if (extension_loaded(mbstring)) { return mb_strlen($s, UTF-8); } $len strlen($s); $i 0; $chars 0; while ($i $len) { $step utf8CharLen(ord($s[$i])); if ($i $step $len) { $chars; // 末尾半个字符也算一个避免结果忽大忽小 break; } $i $step; $chars; } return $chars; } function utf8CharLen(int $byte): int { if ($byte 0x80) { return 1; } if (($byte 0xE0) 0xC0) { return 2; } if (($byte 0xF0) 0xE0) { return 3; } if (($byte 0xF8) 0xF0) { return 4; } return 1; } function utf8Cut(string $s, int $maxChars): string { $len strlen($s); $i 0; $chars 0; while ($i $len $chars $maxChars) { $step utf8CharLen(ord($s[$i])); if ($i $step $len) { break; } $i $step; $chars; } return substr($s, 0, $i); }在容器里跑出来的结果text仅示意格式具体数值随镜像而变PHP 版本 : 7.0.33 mbstring : 未加载 iconv : 已加载 default_charset: UTF-8 locale(zh_CN) : false strlen9 字符数5 前 3 个字符: 张三丰 字节截断的后 20 字节能否 json_encode: 否非法 UTF-8 字符截断后能否 json_encode: 是 preg_match(/u) 返回: false preg_last_error4 json_encode 失败: Malformed UTF-8 characters, possibly incorrectly encoded两个最值得记住的输出是最后三行preg_match()在/u模式下遇到非法 UTF-8 返回的是false而不是0而false在if里和0一样为假很多人因此把“数据坏了”误判成“没匹配上”preg_last_error()返回的非 0 值才是真正的信号。常见坑点1. 只按字节判断长度❌if (strlen($nickname) 20) { ... }——中文用户 7 个字就被判超长 ✅mb_strlen($nickname, UTF-8)或用上面不依赖扩展的字符计数2. 用 substr 截断多字节字符串❌$title substr($title, 0, 30);可能切出半个汉字 ✅ 优先mb_substr()没有 mbstring 时用按 UTF-8 首字节判断的自实现版本3. 把preg_match的返回值和 false 混为一谈❌if (!preg_match(/^[\x{4e00}-\x{9fa5}]$/u, $name)) { 视为非法输入 }✅ 先判断preg_last_error() ! PREG_NO_ERROR区分“不匹配”和“数据非法”4. 忽略 json_encode 的返回值❌$body json_encode($data);然后直接把false发给下游 ✅ 检查 false并打印json_last_error_msg()加JSON_UNESCAPED_UNICODE让中文可读5. 用 MySQL 的 utf8 存 emoji❌ DSN 里写charsetutf8用户输入 emoji 时插入报Incorrect string value✅ 用charsetutf8mb4并确认表的字符集也是 utf8mb46. 以为本地有 mbstring 容器就有❌ 从不检查php -m上线才发现mb_strlen未定义 ✅ 构建阶段检查扩展或在代码里对扩展可用性做降级判断7. 临时docker exec装扩展❌ 在运行中的容器里docker-php-ext-install mbstring重建容器后扩展消失 ✅ 扩展写进 Dockerfile一次构建到处运行8.setlocale失败后继续用❌setlocale(LC_ALL, zh_CN.UTF-8); echo strftime(%B);静默输出英文 ✅ 检查setlocale()的返回值容器里改用与 locale 无关的时间格式化函数9. 还在用mysql_*❌mysql_real_escape_string($s)——PHP 7.0 起这些函数已被移除 ✅ 换 PDO 或 mysqli用预处理语句传参不做手工转义总结问题容器里的表现处理方式mbstring 缺失Call to undefined function mb_*Dockerfile 里docker-php-ext-install mbstring字节截断乱码、Incorrect string value按字符截断或自实现 UTF-8 安全截断locale 为空setlocale()返回 false镜像里locale-gen或改用与 locale 无关的函数非法 UTF-8json_encode返回 false、preg_match返回 false检查json_last_error()/preg_last_error()数据库字符集emoji 存不进去DSN 写charsetutf8mb4老 APImysql_*致命错误迁移到 PDO / mysqli 预处理容器不背这个锅它只是把“页面字符集、扩展、locale、数据库字符集”这几层隐含假设全部剥掉了。与其在每个函数前面手忙脚乱地加mb_不如先把这条链路固定成一条明确规则——输入一律视为字节流边界处才谈字符所有截断和计数都按字符做所有编码转换都显式指定 UTF-8。做到这一点代码在容器里和在宿主机上就会有一致的行为。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →