PHP 8.6 的 trim() 默认多剥一个 \f:定宽文本和分页流会静默错位
PHP 8.6 对 trim() 家族的默认字符集做了一处改动:trim()、ltrim()、rtrim()、chop() 现在默认也会去掉换页符 \f(0x0C)。改动没有 deprecation,没有警告,只是返回的字符串短了一个字节。
参考:PHP 8.6 迁移说明、php.watch/versions/8.6。
默认字符集从 6 个变成 7 个
在 8.6 之前,这几个函数不带第二个参数时,剥的是固定的 6 个字符:
" " 空格 0x20
"\t" 制表符 0x09
"\n" 换行 0x0A
"\r" 回车 0x0D
"\0" NUL 0x00
"\x0B" 垂直制表符 0x0B
8.6 在这个集合里补上了 \f(换页符 0x0C)。行为差异只体现在字符串首尾恰好有 \f 的时候——中间出现的 \f 不受影响。
为什么叫静默变化
函数签名没变,返回值类型没变,不会抛异常也不会记日志。调用方只看到字符串长度变了:
- 靠列偏移解析定宽记录的代码,首尾少一个字节就可能整体错位;
- 先
trim()再按字符数substr()或str_pad()的代码,算出来的宽度和实际不一致; - 把
trim()结果做哈希、做缓存键、写进数据库的,值变了但没有痕迹。
这类问题不会在 CI 里报错,只会在某几行数据上出现“看起来少了一列”的现象。
典型出问题的场景
- 打印/票据文件。很多报表流用
\f作为分页分隔,一段内容末尾带\f送进trim(),分页符就没了,打印机不再换页。 - 老式 COBOL 风格定宽导出。记录按固定列对齐,末尾填充字节里可能就含
\f,剥掉后整条记录的列宽对不上。 - 从 PDF 或终端抓下来的文本。PDF 文本提取和终端截屏里
\f很常见,清洗时常被一并trim()掉。 - 日志里用
\f分隔保留块。把\f当块边界符的日志格式,trim()之后块边界消失,解析器拿不到分块点。
复现与验证
// PHP 8.6 之前:int(3)
// PHP 8.6: int(3) —— 尾部 \f 被剥掉后是 "abc"
var_dump(strlen(trim("abc\f")));
// 看清楚到底剥了什么
var_dump(bin2hex(trim("abc\f"))); // 8.6: "616263"
var_dump(bin2hex("abc\f")); // 始终: "6162630c"
// 左侧同理
var_dump(bin2hex(ltrim("\fabc"))); // 8.6: "616263"
var_dump(bin2hex(rtrim("abc\f"))); // 8.6: "616263"
var_dump(bin2hex(chop("abc\f"))); // 8.6: "616263"
升级前建议在旧版本上跑一遍上面几行,升级后再跑一遍对比 bin2hex 的输出。
修复:显式写第二个参数
要保留旧行为,把字符集写全,不要依赖默认值:
$s = trim($s, " \t\n\r\0\x0B"); // 8.6 之前的行为,不含 \f
反过来,如果确实想剥 \f,也同样显式写出来,别指望默认值:
$s = trim($s, " \t\n\r\0\x0B\f"); // 明确包含换页符
ltrim()、rtrim()、chop() 同理。对于解析定宽数据的代码,这一步基本是必须的——默认字符集本来就不该是隐式契约。
同一版本里其余需要回归的改动
8.6 还有一批“从警告变异常”的调整,升级后要跑一遍回归:
array_filter()的$mode传非法值时改为抛ValueError;- GMP 的移位与幂运算、
pcntl_alarm()/pcntl_exec()、Phar::mungServer()、CURLOPT_READFUNCTION的返回值、不可字符串化的 intl 时区、SimpleXMLElement与会话 cookie 设置中的 NUL 字节,都从警告改为抛异常; preg_grep()出错时返回false,不再返回部分数组;- DOM 的只读属性改用
public private(set)。
PHP 8.6 计划于 2026-11-19 发布。搜索代码库里的 trim(、ltrim(、rtrim(、chop(,把没有第二个参数的调用逐个确认一遍,是这次升级里成本最低、收益最直接的一项检查。