PHP 8.6 的 RFC 投票已经有一段时间了。不过这次没有 PHP 8.5 那种大规模清理门户的阵仗,但每一条拿出来都挺实在,一方面是填补多年空白,另一方面是解决社区长期痛点。下面老王大概说个遍。
1. Debugable Enums:枚举终于能自定义 var_dump 了
PHP 8.1 引入枚举的时候,定了一条规矩:枚举不允许使用任何魔术方法。理由也说得通,大部分魔术方法设计来处理对象状态的,而枚举的 case 本质上是单例,没状态。
但 __debugInfo() 这个魔术方法其实不需要状态,它就是管 var_dump() 输出什么格式的。偏偏当时一刀切了,不让用。
这条 RFC(作者 Daniel Scherzer,投票 16 票赞成、2 票反对)把这个限制解除了。现在枚举可以写自己的 __debugInfo() 方法:
enumStatus: string{ case Active = 'active'; public function__debugInfo(): array { return [self::class . '::' . $this->name . ' = ' . $this->value]; }}var_dump(Status::Active);// enum(Status::Active) (1) { [0]=> string(19) "Status::Active = active" }
不改的话行为完全不变,想自定义输出格式的话现在可以了。后续社区还在讨论,看是不是要给枚举逐步开放更多魔术方法的支持。
2. Oniguruma 维护终止 + mbregex 函数废弃:三十年遗留终于说再见
这条是 PHP 8.6 里最硬核的一条,投票结果 24 票全票通过,反映了社区的强烈共识。
Oniguruma 是一个正则表达式库,PHP 一直在用它支撑 mb_ereg_* 这套多字节正则函数。2025年4月24日,Oniguruma 官方维护宣告结束,PHP 决定顺势把整个 mbregex 分支彻底下线。
具体计划是:PHP 8.6 里废弃这批函数,PHP 9.0 直接删掉。涉及整整18个函数:mb_ereg、mb_ereg_match、mb_ereg_replace、mb_ereg_search_*、mb_eregi、mb_eregi_replace、mb_regex_encoding、mb_regex_set_options、mb_split……全在里面。
废弃提示语长这样:
Deprecated: Function mb_ereg_replace() is deprecated since 8.6,Oniguruma functions support ends PHP 9.0
不过 PHP 留了后路。作者专门做了一个独立的扩展叫 mb_onig(发布在 Packagist 上),安装方式和普通 PHP 扩展一样:./configure --enable-mbstring --disable-mbregex 然后 pecl install mb_onig。这样依赖这套函数的代码在 PHP 8.x 和 9.x 里都能继续跑,只是需要手动装一下扩展。
这个改动对只用 UTF-8 的项目基本无感,但如果你的业务涉及到其他字符集(Shift-JIS、Big5、EUC-JP 之类的),升级前最好检查一下代码里有没有 mb_ereg 系列函数的调用。
3. enum SortDirection:排序方向终于有类型安全了
SortDirection 是 PHP 8.6 新增的一个全局枚举,两个 case:Ascending 和 Descending。投票 22:2,通过。
这个 RFC 的作者 在 RFC 文档里写了一段挺有共鸣的话:
PHP 的标准库至今没有任何类型安全的方式来表达排序方向。现在我们用 SORT_ASC / SORT_DESC 这种整型常量(只对 array_multisort 有效),或者 SCANDIR_SORT_ASCENDING(只对 scandir 有效),或者干脆裸字符串 ASC / DESC,或者 bool 型的 $ascending 参数,或者干脆两个分开的函数 sort() 和 rsort()。
实际上社区早就自发解决了这个问题,Doctrine 两年前就自己搞了个 Doctrine\Common\Collections\Order 枚举。SortDirection 这个 RFC 就是把这件事官方化,让所有框架和用户代码能统一在一个类型上。
enumSortDirection{ case Ascending; case Descending;}// 用法$query->orderBy('created_at', SortDirection::Descending);
枚举是 unbacked 的(不带值的),因为排序方向不存在一个唯一的序列化标准,SQL 用 'DESC',URL 参数可能用 'desc',JSON 可能用 0/1,各自用 match() 转就行。这个设计很克制,但够用。
后续 PHP 标准库本身(scandir() 等函数)的参数也会逐步跟进支持这个枚举,不需要额外 RFC,只是个 widening 类型的过程。
4. DocComments For Function Parameters:参数文档终于可以写到参数旁边了
这个 RFC 加了一个很多人盼了很久的功能:ReflectionParameter::getDocComment()。
之前 PHP 函数的文档注释只能写在函数声明上面,@param 标注参数名称和类型。但问题是:参数的类型已经在参数列表里声明了一遍,文档又要在 DocComment 里再写一遍,两边容易不同步,特别是多人协作或者代码频繁改动的时候。现在可以把文档直接写在参数后面:
functionsearch( /** 搜索关键词 */ string $query, /** 最大返回条数,默认10 */ int $num = 10) {}// 也可以写在类型前面functionsearch( string $query /** 搜索关键词 */, int $num = 10 /** 最大返回条数,默认10 */) {}
两种写法都支持,和属性 DocComment 的风格保持一致。调用方式和已有反射 API 一致:
foreach((new ReflectionFunction('search'))->getParameters() as $p) { echo $p->name . ": " . $p->getDocComment() . PHP_EOL;}
文档状态直接标记为"Implemented",上线时间就是 PHP 8.6。对 IDE 和静态分析工具来说这是个好消息,参数级别的文档现在可以在不读整个函数 DocComment 的情况下直接取到了。
5. Add Form Feed in Trim Functions:终于和 Python、JavaScript 一致了
trim() 函数的默认去除字符列表里,一直少了一个 \f(Form Feed,ASCII 12)。这条 RFC 把 \f 补进去了,投票 25:0,全票通过,属于那种早就该做了的小改动。
之前:
trim("\fHello World\f"); // "\fHello World\f" — 没变化
之后:
trim("\fHello World\f"); // "Hello World" — 正常去掉了
RFC 文档里详细说明了为什么应该加:POSIX 的 isspace() 把 \f 定义为空格字符,Python 的 str.strip() 默认去掉 \f,JavaScript 的 String.prototype.trim() 也去掉 \f,甚至 PHP 自己的 is_numeric() 解析字符串时也把 \f 当成空白字符处理,唯独 trim() 特殊。现在统一了。
这是 PHP 8.6 的一次小兼容性变化,文档明确标注为"backward incompatible"(但影响很小),已经在 commit 里落地了:https://github.com/php/php-src/commit/da1e89fd3db0b9d2017976d774270ee7ce3b35a7
6. grapheme_strrev:反转字符串,终于能正确处理 Emoji 了
strrev() 函数是 PHP 里祖传的函数,但它按字节反转,不按人类可读的方式处理多字节字符。想反转一个包含 Emoji 或中文的字符串,strrev() 基本会得到一堆乱码。
grapheme_strrev() 就是来解决这个问题的,它按字素簇(grapheme cluster)反转,跟 Swift 的 String.reversed() 行为一样。
// 反转希腊文+日文+家庭 emoji(5个人)echo grapheme_strrev("αあいうえお👨👨👧👦");// 输出:"👨👨👧👦おえういあα"// (每个 emoji 是一个完整的字素簇,不会被拆开)// 阿拉伯语也从右到左正确处理echo grapheme_strrev("مرحبا"); // "بحرم"
这个函数加在 ext/intl 扩展里,投票 20:0,零反对。作为参考,亚洲和阿拉伯语用户对这类功能的需求是长期的,之前只能靠 mb_strrev 这种第三方实现,效果参差不齐。现在有了官方实现,IDE 提示、类型检查、文档都能一步到位。
7. Add "clamp()" function:数值边界限制,终于有原生函数了
clamp 这个函数在其他语言里早就有了,C++、Python、Java、C# 都有,CSS 也有 clamp()。现在 PHP 也补上了。
语法很简单:clamp($value, $min, $max)。如果 value 在 min 和 max 之间,就返回 value;如果比 max 大就返回 max;如果比 min 小就返回 min。
clamp(2, 1, 3); // 2 — 在范围内,原样返回clamp(0, 1, 3); // 1 — 小于最小值,返回 1clamp(6, 1, 3); // 3 — 大于最大值,返回 3clamp("d", "c", "g"); // "d" — 字符串也支持clamp(new DateTime('2025-08-20'), new DateTime('2025-08-15'), new DateTime('2025-09-15'))->format('Y-m-d'); // "2025-08-20"
有几个细节需要注意:
(1)支持 int、float、string、DateTime 等多种可比较类型
(2)min > max 时抛出 ValueError(无效边界)
(3)min 或 max 传 NAN 也抛出 ValueError,但 value 是 NAN 时原样返
(4)接受命名参数,可以打乱顺序调用:clamp(min: 0, value: $angle, max: 90)
之前大家都是自己写:$clamped = min($max, max($min, $value))。RFC 文档里也测了性能,原生函数比 userland 实现还要快一点,而且自带边界验证和 NAN 处理,开发体验更干净。
8. isReadable/isWritable Reflection methods:终于能判断一个属性能不能读写
这条 RFC 加了两个新方法:ReflectionProperty::isReadable() 和 ReflectionProperty::isWritable()。作者 Ilija Tovilo 和 Larry Garfield,投票 27:0,零反对。
为什么要加这个?因为 PHP 8.1 引入了 readonly 属性,PHP 8.4 又引入了非对称可见性(public private(set))。现在一个属性是不是可以读或可以写,不再是简单看 isPublic() 就能判断的事了。
classBook{ public private(set) string $title = 'Default'; public readonly string $isbn;}$prop = new ReflectionProperty(Book::class, 'title');// 全局作用域能读吗?$prop->isReadable(); // true — public 的 get$prop->isWritable(); // false — private(set) 不让外部写// 指定某个类的作用域来判断$prop->isReadable(Book::class);$prop->isWritable(Book::class);
$scope 参数可以用 self::class 表示当前类,或者直接传类名字符串。$object 参数是可选的,如果不传,只看静态信息;如果传了,会额外检查对象实例层面的状态(比如 readonly 属性有没有被写过了)。
对做框架、做 ORM、做依赖注入容器的开发者来说,这两个方法是非常实用的工具。以前想判断一个属性能不能写,只能尝试读一下然后 catch 异常,现在有反射 API 可以在不触发副作用的情况下提前知道。
9. New function mysqli_quote_string:填了一个埋了二十年的坑
这条是安全相关的 RFC,作者 Kamil Tekiela,投票通过。
简单说:mysqli::real_escape_string 有一个已知的 SQL 注入漏洞,在某些 SQL_MODE 下可以被绕过。具体来说,当设置 SET SQL_MODE="NO_BACKSLASH_ESCAPES" 时,real_escape_string 的转义机制会失效,但调用方往往意识不到这一点,导致看似转义过的字符串实际上还包含危险字符。
新函数 mysqli::quote_string()(也有静态函数 mysqli_quote_string($mysqli, $string))直接参考了 PDO::quote() 的设计:自动转义、自动包裹单引号、始终用单引号(不提供双引号选项,因为双引号在标准 SQL 里不是字符串定界符)。
// 老写法(有隐患)$sql = sprintf('SELECT id FROM foo WHERE name="%s"', $mysqli->real_escape_string($value));// 在 NO_BACKSLASH_ESCAPES 模式下,恶意输入仍可注入// 新写法(安全)$sql = sprintf('SELECT id FROM foo WHERE name=%s', $mysqli->quote_string($value));// 自动转义并包裹单引号,注入失败
RFC 文档里明确提到:real_escape_string 理想情况下应该在后续版本废弃,但这条 RFC 本身不包含废弃步骤,留给社区后续讨论。这也是合理的,一次性把安全函数加进来,让用户迁移,废弃可以单独处理。
总结
这 9 条 RFC 放在一起看,PHP 8.6 不是一个大版本,但每条拿出来都是社区长期讨论的结果。有的是填补语言能力空白(SortDirection、clamp、grapheme_strrev),有的是清理历史遗留(mbregex、myqsli_quote_string 的注入问题),有的是提升开发者体验(参数 DocComment、属性反射方法),还有的是修复之前版本带来的复杂度(Debugable Enums)。
整体来看,PHP 8.6 延续了 8.x 以来的演进节奏。具体的发布日期按 PHP 的惯例应该是 2026 年年底左右,大概11月份,在此之前还有 Friends 和 Scope Functions 这两个讨论中的 RFC 值得关注,但它们最终落地哪个版本还不确定。
