前言
PHP 7.0 发布于 2015 年 12 月,它不是一个"字符串版本",但恰恰在字符串处理上带来了几个影响深远的变化:一个是新增了 Unicode 码点转义\u{},另一个是把一大批古老的字符串函数(ereg、split、mysql_*)直接移除。前者常被忽略,后者是老项目迁移时最痛的一刀。
典型症状有三类:升级到 7.0 后老代码大面积报Call to undefined function ereg();在 7.0 环境里照着新文章写str_contains(),同样报未定义函数(那是 PHP 8.0 才有的);用strlen()统计中文长度,得到的数字永远比实际字数大两倍。
本文梳理 PHP 7.0 上真正可用、且值得用的字符串技巧:码点转义、随机令牌、批量正则回调、多字节处理的正确姿势、??与字符串下标,以及declare(strict_types=1)对字符串参数的副作用。文中的示例最低版本 PHP 7.0,可直接运行。
一、\u{}码点转义:7.0 新增的字符串能力
PHP 7.0 之前,想在字符串里写一个 Unicode 码点,只能先算出 UTF-8 字节再用\x拼,或者干脆把字符复制进去。7.0 加入了\u{十六进制}语法:
$hello = "\u{4F60}\u{597D}"; // 你好
$emoji = "\u{1F600}";两条必须记住的规则:只能用在双引号字符串和 heredoc 里(单引号和 nowdoc 中会原样输出\u{4F60}这七个字符);大括号是必须的,写你不会被识别。它和json_decode处理的\uXXXX(固定四位、不需要大括号)是两套不同语法,别混用。
二、7.0 新增的几件趁手工具
除了码点转义,7.0 还给字符串相关任务补了几块短板:
| 函数 / 语法 | 用途 | 备注 |
|---|
random_bytes()/random_int() | 生成密码学安全的随机字节 / 整数 | 7.0 新增,替代mt_rand()做令牌 |
preg_replace_callback_array() | 一次配置多组"正则 => 回调" | 7.0 新增,替代多个preg_replace_callback |
??空合并运算符 | $conf['k'] ?? 'default' | 7.0 新增,对未定义下标不报 Notice |
<=>太空船运算符 | 排序比较 | 7.0 新增,常用于usort |
unserialize($s, ['allowed_classes' => false]) | 限制反序列化可实例化的类 | 7.0 新增第二参数 |
intdiv() | 整除 | 7.0 新增,与字符串无关但常一起用 |
要特别提醒两个不属于7.0 的常见误传:hash_equals()和password_hash()分别是 5.6 和 5.5 就有的,不是 7.0 新特性;而str_contains()/str_starts_with()/str_ends_with()要等到PHP 8.0,在 7.0 上必须用strpos()/strncmp()/substr_compare()手工实现。
三、多字节字符串:7.0 上的正确写法
PHP 的字符串是字节数组,strlen()数的是字节数。"你好"是 6 字节,strlen()返回 6 而不是 2。7.0 上处理 UTF-8 字符串的正确工具是mb_*系列与带/u修饰符的 PCRE:
- 取长度:
mb_strlen($s, 'UTF-8'); - 截取:
mb_substr($s, 0, 10, 'UTF-8'); - 拆成单字符数组:不要用
mb_str_split(),它要 PHP 7.4 才有。7.0 上的写法是preg_split('//u', $s, -1, PREG_SPLIT_NO_EMPTY); - 大小写:
mb_strtolower()/mb_strtoupper(),注意它们不做完整的区域化折叠(如德语 ß); - 正则:给模式加
/u,否则.会按字节匹配,中文会被拆成半个字符。
四、完整可运行示例
<?php
// 最低版本:PHP 7.0
declare(strict_types=1);
// 1) 码点转义:只能写在双引号或 heredoc 中
$greeting = "\u{4F60}\u{597D}";
$emoji = "\u{1F600}";
echo $greeting, $emoji, PHP_EOL; // 你好😀
// 2) 生成令牌:不要再自己拼 rand()
$token = bin2hex(random_bytes(32)); // 64 个十六进制字符
echo strlen($token), PHP_EOL; // 64
// 3) 一次配置多条替换规则
$text = '联系方式:138-0000-0001,邮箱 ada@example.com';
$masked = preg_replace_callback_array([
'/\b(\d{3})-(\d{4})-(\d{4})\b/' => static function (array $m): string {
return $m[1] . '-****-' . $m[3];
},
'/\b([\w.]+)@([\w.]+)\b/' => static function (array $m): string {
return substr($m[1], 0, 1) . '***@' . $m[2];
},
], $text);
echo $masked, PHP_EOL; // 联系方式:138-****-0001,邮箱 a***@example.com
// 4) 多字节:长度、截取、拆字符
$cn = '你好世界';
echo strlen($cn), PHP_EOL; // 12(字节)
echo mb_strlen($cn, 'UTF-8'), PHP_EOL; // 4(字符)
echo mb_substr($cn, 1, 2, 'UTF-8'), PHP_EOL; // 好世
// mb_str_split() 要 PHP 7.4,7.0 用 preg_split
$chars = preg_split('//u', $cn, -1, PREG_SPLIT_NO_EMPTY);
echo count($chars), PHP_EOL; // 4
echo implode('/', $chars), PHP_EOL; // 你/好/世/界
// 5) ?? 让配置读取不再报 Notice
$conf = [];
$env = $conf['env'] ?? 'production';
$port = (int) ($conf['port'] ?? 8080);
echo $env, ':', $port, PHP_EOL; // production:8080
// 6) 限制反序列化可实例化的类
$payload = serialize(new stdClass());
$restored = unserialize($payload, ['allowed_classes' => false]);
var_dump($restored instanceof __PHP_Incomplete_Class); // bool(true)
// 7) 8.0 才有的函数在 7.0 上必须手工替代
$haystack = 'php-7.0-string-tips';
$needle = 'string';
var_dump(strpos($haystack, $needle) !== false); // bool(true)
var_dump(strncmp($haystack, 'php', 3) === 0); // bool(true)这段代码覆盖了 7.0 上最实用的几个点。注意第 7 项:str_contains()在 7.0 上会直接Fatal error,而strpos()的返回值在"匹配位置为 0"时是0,用if (strpos(...))判断会把"开头就匹配"误判为没找到——这是最经典的坑,必须用!== false。
常见坑点
1. 在 7.0 上调用 8.0 才有的字符串函数
❌str_contains($h, $n)/str_starts_with($h, $n)——Call to undefined function,整站 500。抄来的代码片段常常不标注版本。 ✅ 7.0 写法:strpos($h, $n) !== false、strncmp($h, $n, strlen($n)) === 0、substr_compare($h, $n, -strlen($n)) === 0。
2. 用strpos()的返回值直接做布尔判断
❌if (strpos($h, $n)) { ... }——匹配位置是 0 时返回0,判断为假,于是"以关键词开头的记录全部漏掉"。 ✅ 一律!== false。
3. 在 7.0 上用mb_str_split()
❌ 那是 PHP 7.4 才加入的函数,7.0/7.1/7.2/7.3 都报未定义。 ✅preg_split('//u', $s, -1, PREG_SPLIT_NO_EMPTY);记得加/u,空字符串会返回空数组。
4. 还在用 7.0 已经移除的老函数
❌ereg('^[0-9]+$', $s)、split(',', $s)、mysql_query()——PHP 7.0 中这些已被删除,不是"弃用",而是彻底不存在。 ✅preg_match('/^[0-9]+$/D', $s)、explode(',', $s)、mysqli/PDO。
5. 用mt_rand()生成密码重置令牌
❌md5(mt_rand())——mt_rand是可预测的伪随机数,令牌可被枚举。 ✅bin2hex(random_bytes(32))(7.0 新增),要用整型随机数则random_int(1, 100)。
6. 用strlen()判断字符数
❌if (strlen($nickname) > 10)想限制"十个字",中文昵称三个字就被拒绝。 ✅mb_strlen($nickname, 'UTF-8')。反过来,做存储长度校验(如VARCHAR(255))时要用strlen()数字节。
7. 在单引号里写\u{}
❌'\u{4F60}'——单引号字符串不做转义解析,页面上直接显示出这七个字符。 ✅ 用双引号或 heredoc;同时注意双引号里的$需要转义。
8.htmlspecialchars()不传编码
❌htmlspecialchars($input)——依赖default_charset与默认 flags,输出编码不确定;另外要记住 PHP 8.1 起默认 flags 变成了ENT_QUOTES | ENT_SUBSTITUTE,7.0 上默认是ENT_COMPAT(不转义单引号)。 ✅ 在 7.0 上显式写全:htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')。
总结
| 技巧 | 引入版本 | 关键点 |
|---|
\u{}码点转义 | PHP 7.0 | 只能在双引号 / heredoc 中 |
random_bytes()/random_int() | PHP 7.0 | 替代mt_rand生成令牌 |
preg_replace_callback_array() | PHP 7.0 | 一组规则一次替换 |
??空合并 | PHP 7.0 | 读数组下标不再报 Notice |
preg_split('//u', ...)拆字符 | 一直可用 | 7.4 之前没有mb_str_split() |
str_contains()/str_starts_with() | PHP 8.0 | 7.0 上必须用strpos/strncmp替代 |
ereg/split/mysql_* | 7.0 已移除 | 迁移时最容易集中爆发 |
在 PHP 7.0 上写字符串代码,核心就两句话:用mb_*和/u处理多字节,用!== false处理查找。把版本边界记清楚(\u{}与random_bytes是 7.0 的、mb_str_split是 7.4 的、str_contains是 8.0 的),就不会再被"函数不存在"和"中文长度不对"这两类问题反复绊住。