前言
做版本评估时经常有人问:从 7.2 升到 7.3,字符串处理这块到底多了什么?把升级说明翻一遍,看到的净是 hrtime()、is_countable()、函数调用尾逗号这些条目,好像跟字符串一点关系都没有,于是判断"字符串功能没变",放心升级,结果上线当天出现语法错误导致的空白页。
真实情况有两点需要先纠正。第一,7.2 到 7.3 之间 PHP 没有新增任何字符串处理函数 。大家常说的 mb_ord()、mb_chr()、mb_scrub() 这三个多字节(multibyte)函数,是 PHP 7.2.0 加进来的,不是 7.3。第二,7.3 在字符串方向上真正的改动是 heredoc 与 nowdoc 的灵活语法(Flexible Heredoc and Nowdoc Syntax),它是一套解析规则而不是函数,写得不合规时报的是编译期语法错误(ParseError),症状比"函数不存在"更难定位。
本文先把"函数增量究竟发生在哪个版本"讲清楚,再拆开 7.3 的 heredoc 新解析规则,最后给出从 7.2 升到 7.3 时的自查点和几个真会踩的坑。
一、结论先行:字符串函数的增量发生在 7.2
下面这张表是这条时间线上真正该记住的东西。注意"与字符串是否相关"这一列------7.3 新增的东西里,语法层的 heredoc 才是重点。
| 特性 | 引入版本 | 类型 | 与字符串处理的关系 |
|---|---|---|---|
mb_ord() |
7.2.0 | 函数 | 直接相关:字符转码点 |
mb_chr() |
7.2.0 | 函数 | 直接相关:码点转字符 |
mb_scrub() |
7.2.0 | 函数 | 直接相关:清洗非法字节序列 |
| 灵活 heredoc / nowdoc 语法 | 7.3.0 | 语法 | 直接相关:字符串字面量的写法与解析 |
| 大小写不敏感常量废弃 | 7.3.0 | 废弃 | 间接相关:define() 常量的名字比较 |
is_countable() |
7.3.0 | 函数 | 无关(数组) |
hrtime() |
7.3.0 | 函数 | 无关(时间) |
所以如果有人告诉你"7.3 加了一批字符串函数",那是记错了版本号。反过来,如果你在 7.2 上写 mb_ord($s) 报 Call to undefined function,原因不是"这个函数要 7.3",而是:要么你的 mbstring 扩展没启用,要么你实际跑的其实是 7.1 或更早的版本。
二、7.2 补上的三个多字节函数
2.1 mb_ord 与 mb_chr:码点和字符互转
在 7.2 之前,要拿一个 UTF-8 字符的码点(code point),标准做法是先转成 UTF-32BE 再用 unpack('N') 取整数,绕一大圈。7.2 直接给了两个函数:
php
<?php
// 需要 PHP 7.2+ 且启用 mbstring 扩展
var_dump(mb_ord('A', 'UTF-8')); // int(65)
var_dump(mb_ord('中', 'UTF-8')); // int(20013)
var_dump(mb_chr(20013, 'UTF-8')); // string(3) "中"
// 和 chr()/ord() 的区别:后两个只认单字节
var_dump(ord('中')); // int(228),只是第一个字节
var_dump(strlen('中')); // int(3),字节数,不是字符数
下面这段是 7.2 之前的手工替代实现,如果你维护的代码要同时兼容 5.x 和 7.x,可以把它作为兜底:
php
<?php
// 兼容 PHP 5.4 及以上;mb_convert_encoding + unpack 实现取码点
function legacy_mb_ord(string $s, string $encoding = 'UTF-8')
{
$utf32 = mb_convert_encoding($s, 'UTF-32BE', $encoding);
$arr = unpack('N', $utf32);
return $arr === false ? false : $arr[1];
}
var_dump(legacy_mb_ord('中')); // int(20013)
要留意一个语义差别:mb_ord() 在失败时返回 false,而手工实现的 unpack 版本在输入是空串时会因为长度不足直接报错,所以调用前该做长度判断就做。
2.2 mb_scrub:把非法字节替换掉
mb_scrub() 的用途是"洗"字符串:把目标编码里不合法的字节序列,替换成 mbstring 设定的替换字符(由 mb_substitute_character() 决定,默认是问号)。典型场景是从老库、老日志里读出来的混合编码文本,直接塞进 json_encode(),只要有一个坏字节,整个 json_encode() 就返回 false,接口直接吐 null。
php
<?php
// 需要 PHP 7.2+
$raw = "正常\xB2\xE2\xCA\xD4"; // 后面是 GBK 字节,在 UTF-8 里非法
$clean = mb_scrub($raw, 'UTF-8');
var_dump($clean); // 坏字节变成替换字符
$json = json_encode(['msg' => $clean], JSON_UNESCAPED_UNICODE);
var_dump($json); // 这次能编码成功
// 检查 mb_substitute_character 的当前设置
var_dump(mb_substitute_character()); // 默认 int(63),即问号
这里的关键认知是:mb_scrub() 是破坏性操作,它不报错、不抛异常,只是把看不懂的字节换掉。所以它只适合"先保证链路能跑通、后面再治理数据"的场景,不能当数据校验用。
三、7.3 真正的字符串改动:heredoc / nowdoc 灵活语法
3.1 旧规则有多死板
在 7.3 之前,heredoc 的结束标记(closing marker)必须顶格写在第 0 列,而且标记后面除了分号或换行,不能有别的东西。这导致两个很难受的写法限制:
php
<?php
// 7.2 及以前:结束标记必须顶格
// 想缩进对齐?不行,这样写是语法错误
// $sql = <<<SQL
// SELECT * FROM users
// SQL;
第二种限制更疼:因为结束标记后必须换行,你没法把 heredoc 直接当函数参数写。
3.2 7.3 改了哪两条
PHP 7.3 的灵活 heredoc 语法放开了两条限制:
- 结束标记允许缩进,缩进量会从 heredoc 的每一行内容里剥掉。
- 结束标记后面不必再换行,可以直接跟逗号、分号、右括号等后续记号。
php
<?php
// 需要 PHP 7.3+
// 1. 结束标记可以缩进,内容里的对应缩进会被剥掉
function buildSql(): string
{
$sql = <<<SQL
SELECT id, name
FROM users
WHERE status = 1
SQL;
return $sql;
}
echo buildSql();
// 输出:
// SELECT id, name
// FROM users
// WHERE status = 1
// 2. 结束标记后可以直接接逗号和右括号
function logLine(string $level, string $msg): void
{
echo "[{$level}] {$msg}\n";
}
logLine('info', <<<TEXT
第一行
第二行
TEXT);
3.3 代价:缩进是"剥掉"的,不是"忽略"的
这一条是升级时最容易出事的地方。结束标记的缩进量,会从每一行内容里原样减掉。也就是说,如果你的 heredoc 内容里本来就有有意义的行首空格(比如要拼一段 YAML、一段固定宽度文本、或者代码片段),剥掉缩进后内容就变了:
php
<?php
// 需要 PHP 7.3+
// 内容本意是每行前面要留 4 个空格,但结束标记缩进了 4 格,这 4 格被当成公共缩进剥掉了
$yaml = <<<YAML
server:
port: 8080
YAML;
var_dump($yaml);
// 输出变成:
// "server:\n port: 8080"
// 第一层缩进没了,嵌套层级看起来就错了
要看清楚"剥掉了多少",一个办法是把结果 var_dump 出来看转义后的 \n 和空格,不要靠肉眼看终端输出。
3.4 新规则下的两类编译期错误
7.3 之后,下面两种情况会直接是 ParseError,脚本一行都跑不了:
- 内容里某一行的缩进比结束标记还浅。解析器不知道要剥掉几格,只能报错。
- 同一段 heredoc 里混用制表符和空格做缩进。缩进量没法统一比较,同样报错。
排查建议:全项目统一用空格缩进,并在编辑器里打开不可见字符显示。这类错误在本地开发时通常会被提前发现,怕的是从别处拷来的代码文件在保存时被自动转换了缩进。
四、升级前值得顺手检查的两件事
第一件是 mbstring.func_overload。这个 ini 设置在 PHP 7.2 里被标记为废弃(deprecated),到 PHP 8.0 被彻底移除。它一旦被打开,strlen()、substr()、strpos() 这些单字节函数会被替换成 mb_* 版本,于是同一个字符串在开着它的机器上"长度是 6",在没开的机器上"长度是 18"。跨版本迁移时这类环境差异比代码差异更难查,建议无论升不升级都先把它关掉,需要字符数就显式写 mb_strlen()。
第二件是大小写不敏感常量。define('FOO', 1, true) 这种第三个参数为真的写法在 PHP 7.3 里被废弃,PHP 8.0 移除了该参数。检索引擎里搜一遍带第三个参数的 define( 调用,成本很低。
代码实战:一份自检脚本
保存成 check_env.php,在开发机和生产预发环境各跑一遍,可以直接看出两个版本环境到底差在哪:
php
<?php
// 兼容 PHP 7.2 与 7.3,用于对比两个环境的实际能力
$report = [];
$report['PHP 版本'] = PHP_VERSION;
$report['mbstring 已加载'] = extension_loaded('mbstring') ? 'yes' : 'no';
$report['func_overload'] = (string) ini_get('mbstring.func_overload');
$s = '中文abc';
// 字节长度与字符长度的差距,是最直观的"环境是否被 func_overload 改过"的信号
$report['strlen'] = strlen($s);
$report['mb_strlen'] = mb_strlen($s, 'UTF-8');
// 7.2+ 才有的函数
$report['mb_ord'] = function_exists('mb_ord') ? mb_ord('中', 'UTF-8') : '不存在(7.2 以下)';
$report['mb_chr'] = function_exists('mb_chr') ? mb_chr(20013, 'UTF-8') : '不存在(7.2 以下)';
$report['mb_scrub'] = function_exists('mb_scrub') ? mb_scrub("ok\xB2\xE2", 'UTF-8') : '不存在(7.2 以下)';
foreach ($report as $k => $v) {
printf("%-16s : %s\n", $k, var_export($v, true));
}
在两个环境里分别执行 php check_env.php,把两份输出并排放在一起,差异点一目了然。比如 mb_ord 这一行如果显示"不存在",说明那台机器上的 PHP 低于 7.2,谈"7.3 的新字符串函数"就没有意义了。
常见坑点
1. 把 7.2 引入的函数当成 7.3 的新特性
- ❌ 升级文档里写"7.3 新增 mb_ord/mb_chr/mb_scrub,7.2 用不了"
- ✅ 这三个函数是 PHP 7.2.0 引入的。7.3 在函数层面没有字符串新增,改的是 heredoc 语法
2. 以为 heredoc 缩进只是"好看",不影响内容
- ❌ 缩进后直接上线,不检查字符串内容是否被剥掉了行首空格
- ✅ 结束标记的缩进量会从每一行剥掉,拼 YAML、固定宽度文本、代码片段时要
var_dump确认实际内容
3. 内容行缩进比结束标记浅
- ❌
<<<SQL的内容从第 0 列开始,结束标记却缩进了 8 格 - ✅ 所有内容行的缩进必须大于等于结束标记的缩进,否则是编译期 ParseError
4. 同一段 heredoc 里制表符和空格混用
- ❌ 编辑器自动缩进用 Tab,手写的内容行用空格
- ✅ 全项目统一空格缩进,并打开不可见字符显示检查;混用会直接报错
5. 用 mb_scrub 当数据校验
- ❌
$clean = mb_scrub($input, 'UTF-8');然后认为输入合法了 - ✅
mb_scrub()静默替换坏字节,不报错。要用mb_check_encoding($input, 'UTF-8')先判断合法性,再决定拒绝还是清洗
6. 升级后 strlen 的语义悄悄变了
- ❌ 换机器后没注意
mbstring.func_overload,substr()切中文的边界变了却没改代码 - ✅ 关掉
mbstring.func_overload(7.2 起已废弃,8.0 移除),需要按字符处理时显式写mb_substr()、mb_strlen()
7. 把 mb_ord 的结果当成字节序列用
- ❌ 拿到
mb_ord('中')得到 20013,就直接塞进需要原始字节的地方 - ✅ 它返回的是 Unicode 码点,不是 UTF-8 编码后的字节。要字节序列用
bin2hex('中')或编码转换
8. 拿 7.1 的环境当 7.2 验证
- ❌ 在老机器上验证"7.2 的函数能不能用",报未定义函数就以为版本说明写错了
- ✅ 先跑一遍上面的自检脚本确认
PHP_VERSION,再谈特性对应关系
总结
| 问题 | 答案 |
|---|---|
| 7.3 新增了哪些字符串函数 | 没有新增。mb_ord()、mb_chr()、mb_scrub() 是 7.2.0 引入的 |
| 7.3 在字符串上的真实改动 | heredoc / nowdoc 灵活语法:结束标记可缩进、后面可接其他记号 |
| 灵活语法的副作用 | 结束标记的缩进会被逐行剥掉,有意义的行首空格会消失 |
| 什么时候会报语法错误 | 内容行缩进比结束标记浅,或制表符与空格混用 |
| 跨版本还要查什么 | mbstring.func_overload(7.2 废弃、8.0 移除)、大小写不敏感常量(7.3 废弃) |
一句话概括:7.2 到 7.3 在函数 上几乎没有字符串层面的增量,真正的变化在语法 上,而语法变化带来的风险恰恰更大------函数缺失会立刻报错,缩进被剥掉却只会安静地改变字符串内容。升级前把 heredoc 里的每一段多行文本 var_dump 一遍,比读十遍升级说明都有用。