PHP 8.6 特性冻结倒计时:实用新特性与30项弃用清单

PHP 8.6 特性冻结倒计时:实用新特性与30项弃用清单

一、发布节奏:特性冻结就在这几天

PHP 8.6 的开发已经进入收尾阶段。根据官方发布计划:

里程碑日期
Beta 1(软特性冻结)2026 年 8 月 13 日
Beta 32026 年 9 月 10 日
特性冻结(Feature Freeze)2026 年 9 月 22 日
RC 1(硬冻结)2026 年 9 月 24 日
RC 42026 年 11 月 5 日
正式发布(GA)2026 年 11 月 19 日

也就是说,9 月 22 日之后,没赶上这趟车的提案只能等 PHP 8.7。本版由 Daniel Scherzer、Matteo Beccati 和 Joe Ferguson 负责发布管理。

整体而言,8.6 是一个务实的版本:没有 JIT 级的大改动,没有属性钩子,也没有联合类型那样的语言重塑。它的看点集中在两处——几个立刻能用上的实用特性,以及一份规模罕见的弃用清单

二、头号特性:部分函数应用(Partial Function Application)

这是 8.6 里讨论度最高的改动,投票结果是 33 票赞成、0 票反对——在 PHP internals 里几乎等于全票通过。

它的作用很直接:调用函数时留出占位符,PHP 返回一个闭包而不是立即求值。? 标记单个待补参数,用 ... 吸收剩余的所有参数作为可变参数尾部。

过去要固定某个函数的某个参数,你得手写一个 fn() 箭头函数或者 function() use (...);现在这一整类样板代码可以直接省掉。生成出的闭包会保留原函数的参数名、类型和默认值,而且前置传入的参数是立即求值的,不是延迟到闭包被调用时才求值。

它补上了去年管道运算符留下的另一半。

三、其他确定落地的新特性

clamp() —— 终于进内核了

几乎每个项目都手写过 max($min, min($max, $value)),也几乎总有人把嵌套写反过。8.6 把它变成了原生函数:

clamp(101, min: 0, max: 100)   // 返回 100
clamp("a", "x", "z")           // 返回 "x"

它遵循 PHP 常规比较规则,因此不限于整数——字符串、DateTime 对象都能用。如果传入非法边界(比如最小值大于最大值,或者 NAN),会抛出 ValueError

Time\Duration —— 让超时带上单位

PHP 8.6 引入 final readonly class Time\Duration(35 票赞成、1 票反对)。

这是一个看起来很小、实际影响深远的类。二十年来,PHP 的各类 API 接收超时参数时用的都是裸 int——毫秒?微秒?秒?只能靠文档和记忆。Duration 让超时参数第一次能携带自己的单位。

工厂方法覆盖了常见单位:fromNanoseconds()fromMicroseconds()fromMilliseconds()fromSeconds()fromMinutes()fromHours(),以及 fromIso8601DurationString()。实例方法提供 add()sub()multiplyBy()divideBy()negate()absolute() 和静态的 compare()

内核中第一个使用它的是 IO\Poll\Context::wait(?Time\Duration $timeout)。可以预期,框架和客户端库会在一两个版本内跟进,在现有的 int/float 参数之外同时接受 Duration。

只读属性可以有默认值了

自 PHP 8.1 引入 readonly 属性以来,下面这行代码一直是致命错误:

Readonly property Foo::$name cannot have default value

8.6 开始它可以正常编译。该 RFC 于 2026 年 8 月 7 日通过,24 票赞成、0 票反对、5 票弃权,随后合并进 php-src。

这项改动不引入新语法、不改引擎内部表示、也没有破坏性变更,只是移除了一项编译期检查。但对插件系统、迁移执行器、规则注册表这类场景来说,它填补了一个长期不便的缺口——默认值会被视为初始化赋值,在构造函数体运行之前完成。此后的任何写入(包括构造函数内部)都算修改,会失败。

其余值得关注的改进

  • 新的轮询 APIIO\Poll\ContextStreamPollHandleEvent::Read,为底层 I/O 就绪检查提供支持
  • #[\.Override] 支持类常量:8.3 给了属性,8.5 给了属性钩子,8.6 给了常量
  • 内置 SortDirection 枚举enum SortDirection { Ascending; Descending; },取代每个项目都会重新声明的 SORT_ASC 风格整数常量
  • 更好的错误信息:错误会显示函数参数,json_decode() 会指出解析失败的具体位置
  • 反射增强:属性上的 isReadable()isWriteable(),且作用域感知
  • 函数参数文档注释:可以为单个函数参数附加 doc comment
  • pack()/unpack() 字节序修饰符<> 现在可用于浮点数和有符号整数,处理二进制协议时可省掉手写的字节交换
  • 闭包优化:注意,原本提案有两半——自动推断 static 与缓存无状态闭包,最终只有缓存这一半落地,static 推断因边界情况无法可靠识别而被作者主动放弃
  • 此外还有 grapheme_strrev()mysqli_quote_string()Locale::getDisplayKeyword()IntlNumberRangeFormatter、流的 TLS 会话恢复与 0-RTT 早期数据、Pdo\Pgsql::ATTR_CHUNK_SIZE

四、必须留意的安全默认值变更

这是本版中最容易"静默出事"的一项——会话相关的 INI 默认值改了

配置项原默认值8.6 新默认值
session.use_strict_mode01
session.cookie_httponly01
session.cookie_samesite不设置"Lax"

方向是对的,安全默认值收紧是好事。但它确实会打断一部分现有部署:跨站 POST 流程,以及任何通过 JavaScript 读取会话 cookie 的代码,都会停止工作。

建议做法:在 php.ini 里显式固定这三项,让升级过程无法在背后改动它们,然后再有意识地逐项调整。

五、真正的重头戏:约 30 项弃用

如果只读一段,就读这段。8.6 的弃用清单是为 PHP 9.0 铺路的路线图

1. mb_ereg* 进入倒计时(唯一的真移植项目)

mbstring 的 mb_ereg 系列背后的正则引擎 Oniguruma,已于 2025 年 4 月 24 日停止上游维护。PHP 的回应是:8.6 弃用,PHP 9.0 移除

涉及 mb_ereg()mb_eregi()mb_ereg_replace()mb_regex_encoding() 及其同族函数。应迁移到 PCRE,即使用带 u 修饰符的 preg_*

需要为它留足预算——这是语义迁移,不是重命名。两个引擎在足够多的细节上存在分歧,每迁移一个模式都应配套测试。Drupal 已经为此开了 issue,一定程度上说明了它的普遍程度。

2. "对象当数组用"的时代终结

以下函数不再接受对象作为输入:array_walk()array_walk_recursive()deflate_init()inflate_init()、zlib 与 bzip2 的流过滤器参数、mb_convert_variables()http_build_query()

修复几乎总是在调用处加 get_object_vars($obj)

3. 关键字正在被收回

为了让未来的语法有词可用,PHP 必须先弃用这些词的 userland 用法,等一两个版本再正式保留:

  • readonly 作为函数名(39 票对 1 票
  • islet 作为标识符(is 被留作未来模式匹配之用)
  • namespace 作为类常量名
  • _ 作为常量

4. 其他会影响日常的弃用

  • __construct()__destruct() 返回值被弃用;finally 块中返回同样如此(39 票对 3 票,是整个弃用系列中票数最悬殊的一项)
  • trim()ltrim()rtrim()chop() 现在默认剥离换页符 \f——如果解析定宽文本,这是静默的输出变化
  • 一批过去只发警告的函数改为抛异常array_filter()$mode、GMP 的移位与幂运算、pcntl_alarm()/pcntl_exec()Phar::mungServer()、curl 的 CURLOPT_READFUNCTION 返回值、不可字符串化的 intl 时区、SimpleXMLElement 与会话 cookie 设置中的 NUL 字节
  • preg_grep() 出错时返回 false,而不是部分数组——检查一下你对返回值的处理
  • is_a()is_subclass_of() 以字符串作为第一个参数被弃用
  • ArrayIterator 中继承而来却毫无意义的方法、SplFileObject 的 CSV 方法、不完整的 session_set_save_handler() 实现被弃用
  • DOM 的只读属性改用 public private(set)——写入会报错,而不是静默失败

好消息是:8.6 本身不会有什么东西坏掉。被弃用的函数仍然正常工作,只是开始发出警告。这正是团队往往低估的地方。

六、没能上车的提案

一些被投票否决的提案同样值得知道,避免被过时的文章误导:

  • 弃用 list():23 票对 23 票平局。PHP 的语言变更需要三分之二多数,平局不是"差一点",而是失败
  • 管道赋值运算符:被否决
  • let 构造 / 块级作用域:被否决,但 let 仍被保留为关键字
  • 绑定擦除的泛型类型:被否决——泛型仍是 PHP 长期悬而未决的问题
  • __exists() 魔术方法、array_only_keys()/array_except_keys()array_path_get()/array_path_exists()、前缀/后缀字符串函数:均被否决

仍在讨论中、可能顺延到 8.7 的有:函数自动加载(标记 5)、BackedEnum::values()ReflectionAttribute::getCurrent() 在 beta 阶段仍在投票,依赖它之前应确认最终状态。

七、现在该做什么

距离 GA(11 月 19 日)还有两个月,大多数团队有充足的缓冲期。但有几件事现在做成本最低:

  • 如果你维护库:这周就把 PHP 8.6 的 beta 版本对你的测试套件跑一遍。Beta 按设计是不稳定的,但这正是你的 bug 报告还能改变点什么的窗口——RC1 在 9 月 24 日封锁分支之后就没机会了
  • 如果你维护应用:把弃用清单对着自己的代码库读一遍,尤其是任何可以追溯到 PHP 5 或早期 PHP 7 的部分。在 11 月发现一个十年前的辅助函数踩了弃用项,总比在升级之后发现要好
  • 检查会话语义:确认跨站 POST 流程与前端读取 cookie 的逻辑,提前在 php.ini 显式固定那三项配置
  • 评估 mb_ereg 使用面:这是本版唯一真正的移植工程,越早排期越好

PHP 8.6 不是一次激进的底层重写,而是一门语言在被有意维护:保留关键字以备未来语法、剥离失去上游维护的正则引擎、终结"对象当数组用"的时代、把警告变成异常、收紧安全默认值。对现在就读懂这些信号的人来说,PHP 9.0 会是一次容易得多的升级。