17611538698
webmaster@21cto.com

PHP 8.6 的所有新增功能

编程语言 0 18 1小时前
PHP 8.6: the language's steady evolution — Muhammet Şafak

21CTO导读:根据正式发布计划,PHP 8.6 将于几周后发布——具体日期是 2026 年 11 月 19 日。我像往年一样,在正式版发布前撰写了这篇文章,内容基于 RFC 文档和候选版本包。


本文会将PHP 8.6 的所有新增功能,从最酷的功能到最小的细节,按顺序排列,并附有示例和发布日期。

PHP 8.6 将于 2026 年 11 月 19 日正式发布。其主要改进是函数局部应用,这将改变每个 PHP 开发人员编写回调函数的方式。

我编译了PHP-8.5两个PHP-8.6分支php-src并进行了差异比较,因此以下所有内容均来自源代码,而非 RFC 列表。功能冻结期已于 9 月结束,因此在正式发布前,该列表不会更改。

功能介绍的顺序是:最让我兴奋的功能排在最前面,小细节最后介绍。

时间线


里程碑日期
RC52026年11月5日
PHP 8.6.02026年11月19日


说明:日期来自PHP wiki 的PHP 8.6 发布页面(https://wiki.php.net/todo/php86)。

1. 部分功能应用


就是这样。你编写一个函数调用,?在缺少参数的地方添加参数,PHP 会返回一个接受该参数的闭包。

$startsWithAdmin = str_starts_with(?, 'admin.');array_filter(['admin.users', 'web.home', 'admin.posts'], $startsWithAdmin);// ['admin.users', 'admin.posts']

现在想想你多久会写fn ($value) => something($value, 10),以及array_map()的文章。

整个结尾就变成了调用本身:

// Before$posts->map(fn (string$title) => Str::limit($title, 10));// PHP 8.6$posts->map(Str::limit(?, 10));

结果不出所料:

array(2) {    [0]=> string(13) "PHP 8.6 is..."    [1]=> string(13) "Partial fu..."}

这里有三个占位符:

  • ?正好可以代替一个论点。
  • ...取代所有剩余的论点。
  • 带有命名参数?(例如limit: ?)的参数仅取代该参数本身。

function greet(string $greeting, string $name, string $punctuation = '!') {    return "$greeting, $name $punctuation";}$hi = greet('Hi', ...);$hi('Nuno');// "Hi, Nuno!"$hi('Taylor', '?');   // "Hi, Taylor?"$hey = greet(name: 'Nuno', greeting: ?);$hey('Hey');// "Hey, Nuno!"Str::limit(?, end: '…', limit: 5); // named arguments in any order

返回的闭包是一个具有实际类型的真实函数。

我们使用反射可以证明这一点:

Closure [  static function {closure:pfa:pfa.php:13} ] {    - Bound Variables [1] {    Variable #0 [ $greeting ]}    - Parameters [2] {        Parameter #0 [  string $name ]        Parameter #1 [  string $punctuation = '!' ]    }}
这一点很重要,因为依赖注入容器和路由会使用反射来读取你的闭包。局部视图会保留原始函数的类型、默认值和名称。

A?始终会将参数设为必需参数,即使原始参数有默认值。...占位符会保留原始参数的默认值。PHP 也会在调用时捕获这些错误:

Error: Named parameter $a overwrites previous placeholderError: Cannot use named placeholder for unknown or variadic parameter $c

与管道操作符


PHP 8.5 中引入了管道操作符|>,但每个步骤都必须是一个带一个参数的可调用对象。因此,如果你用fn ($x) => str_replace(' ', '-', $x)在管道中间编写代码,管道操作符就失去了大部分魅力。部分应用解决了这个问题:


$slug = 'Hello World'  |> strtolower(...)  |> str_replace(','-', ?);//"hello-world"$title =' Hello PHP  |>trim(...)  |> Str::limit(?, 5)  |> strtoupper(...);// "HELLO..."

当一个包含一个占位符的局部视图位于管道的右侧时,OPcache 会将其编译成一个直接函数调用。其操作码与原1 |> foo(?)操作码相同foo(1),不会创建闭包。

2.Time\Duration


PHP 终于有了正式表示时间的数据类型。

use Time\Duration;$timeout = Duration::fromSeconds(90)    ->add(Duration::fromMilliseconds(500));$timeout->seconds;     // 90$timeout->nanoseconds; // 500000000$meeting = Duration::fromIso8601DurationString('PT1H30M');$meeting->seconds;     // 5400Duration::compare($meeting, Duration::fromMinutes(90)); // 0Duration::fromHours(2)->divideBy(3)->seconds;           // 2400

这是一个final readonly包含三个属性的类,分别是 ` ` $secondsa、` $nanosecondsb` 和$negative`c`。

每个方法都会返回一个新的实例:

object(Time\Duration)#4 (3) {  ["seconds"]=> int(5400) ["nanoseconds"]=> int(0)   ["negative"]=> bool(false)}

构造函数有fromHours(),fromMinutes(),fromSeconds(),fromMilliseconds(),fromMicroseconds(),fromNanoseconds(),fromIso8601,DurationString(),add(),sub(),multiplyBy(),divideBy(),negate(),absolute(),compare()。

检查代码中的每一个参数 int $seconds,int $timeoutInMs:缓存 TTL、HTTP 客户端超时、队列重试延迟、锁持续时间。

3.clamp()

你写了上max($min, min($max, $value))百遍,至少读错了一次。

clamp(150, 1, 100);     // 100clamp(-5, 1, 100);      // 1clamp(42, 1, 100);      // 42clamp(1.5, 0.0, 1.0);   // 1.0clamp('m', 'a', 'f');   // "f"
最经典的用法是测量用户发送给你的页面大小:
$perPage = clamp((int) ($_GET['per_page'] ?? 15), 1, 100);
它适用于 PHP 可以比较的任何值,包括字符串。$min大于号比较$max是一个 bug,PHP 会给出相应的提示:
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)

4. 具有默认值的只读属性


现在,只读属性可以直接声明其默认值:

final class Settings{    public readonly int $perPage = 15;    public readonlystring $theme = 'dark';}
该属性将保持只读状态:

$settings = newSettings();$settings->perPage; // 15$settings->perPage = 30;// Error: Cannot modify readonly property Settings::$perPage

这适用于你在每个项目中编写的数据对象:DTO、消息、具有固定配置的值对象。以前,必须将默认值移到构造函数中才能将属性设置为只读。

5. 向常量中的对象写入数据


现在你可以写入对象中由常量存储的属性:

final class Config{    public array $items = [];     public int $hits = 0;}const CONFIG = new Config();CONFIG->items['app.name'] = 'Acme';CONFIG->hits++;array(1) {  ["app.name"]=> string(4) "Acme"}
int(1)

常量仍然指向同一个对象,你无法替换它。只有对象本身会改变,就像readonly持有对象的属性一样。枚举类型的属性保持不可变。

6.#[\Override]关于常数


PHP 8.3 引入了#[\Override]for 方法,PHP 8.5 将其扩展到了属性。PHP 8.6 又增加了类常量和枚举类型:

interface HasTable{  const TABLE = 'models';}final class User implements HasTable{    #[\Override]    const TABLE = 'users';}

如果父常量被重命名或删除,PHP 会在编译时提示你:

Fatal error: Post::TABLE has #[\Override] attribute, but no matching parent constant exists

7.__debugInfo()关于枚举

枚举现在可以决定var_dump()、dump()或dd()显示什么。

enum Status: string{case Active = 'active';case Banned = 'banned';    public function __debugInfo(): array    {            return ['label' => ucfirst($this->value)];    }}var_dump(Status::Active);enum(Status::Active) (1) {  ["label"]=>  string(6) "Active"}

8. 告诉你 JSON 的位置错误


这个功能很小巧,我想当你第一次遇到 webhook 有效负载解析失败时,就会爱上它。

json_decode('{"name": "Nuno", "age": }');json_last_error_msg();Syntax error near location 1:25
JsonException抛出异常的元素JSON_THROW_ON_ERROR会占据同一行和同一列。

现在,每个显示该消息的地方,例如 HTTP 客户端或请求解析器的错误信息,都会显示该消息在有效负载中的位置。

9. 将流错误作为异常处理


一直以来,file_get_contents()文件缺失都会发出警告false。大多数框架会将该警告转换为带有错误处理程序的异常。

PHP 8.6 现在本身就可以做到这一点:

$context = stream_context_create([    'stream' => ['error_mode' => StreamErrorMode::Exception],]);try {        file_get_contents('/does/not/exist.txt', context: $context);  } catch (StreamException $e) {    $e->getMessage(); // "Failed to open stream: No such file or directory"}
模式有三种,包括有:

StreamErrorMode::Error:` readError`、`readError` 和 `readError`。此外还有 ` readError` 和`readError`用于读取不带处理程序的错误,以及一个枚举类型,用于避免比较消息字符串。

StreamErrorCode旧的行为仍然是默认行为。StreamErrorMode::ExceptionStreamErrorMode::Silentstream_last_errors()stream_clear_errors()

相关内容:fclose(),file_put_contents()现在当刷新或关闭失败时copy()返回false,例如在最后一次写入时磁盘已满。

10. 真正的轮询 API:Io\Poll


这是异步 PHP 的基础。它的epoll支持 Linux 和kqueuemacOS,采用统一的对象 API,无需安装任何扩展:

use lo\Poll\Context;use lo\Poll\Event;use Time\Duration;
[$a, $b] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM,STREAM_IPPROTO_IP);$poll = newContext();$poll->add(newStreamPollHandle($b), [Event::Read], data: 'worker-1');fwrite($a, 'ping');$watcher->getData();// "worker-1"foreach ($poll->wait(Duration::fromSeconds(1)) as $watcher) {  fread($b, 4);// "ping"}

你不会在控制器中编写这段代码。

它是为那些编写事件循环的人准备的,例如 ReactPHP、Amp 和 Revolt,它们目前要么回退到事件循环,stream_select()要么需要扩展才能使用epoll事件循环。所有基于这些框架构建的东西,例如 WebSocket 服务器和 HTTP 客户端,都可以免费使用事件循环。

其后端包括:` WebSocket`、 ` WebSocket` Auto、 `WebSocket` 、` WebSocket`和`WebSocket`。PollEpollKqueueEventPortsWSAPoll

11.SortDirection


一个包含两种情况的全局枚举:

enum SortDirection{    case Ascending;    case Descending;}

PHP 8.6 目前还没有任何函数支持它。它的存在是为了防止你的包自行创建
字符串'asc'和'desc':

function orderBy(string$column, SortDirection $direction = SortDirection::Ascending): string{    return $column . ' ' . match ($direction) {             SortDirection::Ascending => 'asc',        SortDirection::Descending => 'desc',       };}orderBy('created_at', SortDirection::Descending); // "created_at desc"
我希望->orderBy('created_at', SortDirection::Descending)在每个查询构建器中都能看到它。


12. 构建 URI


PHP 8.5 添加了Uri解析 URI 的扩展。PHP 8.6 添加了另一半功能,即构建器:

$uri = (newUri\Rfc3986\UriBuilder())->setScheme('https')->setHost('nunomaduro.com')->setPath('/posts')->setQuery('page=2')->build();$uri->toString(); // "https://nunomaduro.com/posts?page=2"
此外,还有Uri\WhatWg\UrlBuilder浏览器规则,getUriType()用于getHostType()判断主机是 IPv4、IPv6 还是域名Uri\WhatWg\url_percent_encode()。

13. 无需更改代码即可提高速度


这些内容来自“性能改进”部分。

  • array_map()使用一级可调用对象或局部视图(例如 ``)时,代码array_map(strtoupper(...), $names)会被编译成foreach循环。不会创建闭包,内部代码也不会回调到用户空间。
  • printf()仅使用%s并%d编译成字符串插值。
  • 无状态闭包会被缓存。static不捕获任何数据的闭包只会创建一次并重复使用。请参见以下示例。
  • 参数可以更快地传递给已知的构造函数,例如new self()。
  • json_encode()在数组和对象上速度更快,并且在JSON_PRETTY_PRINT。
  • array_sum()对于array_product()整数数组,,,,,,,array_unshift()并且只有一个分量的array_walk(),速度更快。array_intersect()str_pad()str_split()mb_str_pad()pathinfo()
  • 当虚拟机分配新的堆栈页时,深度递归速度更快。
  • 现在 JIT 可以在 Apple Silicon 上处理 ZTS 构建,而且 ZTS 构建速度更快。FrankenPHP 使用的就是这种构建方式。


可以从用户空间查看闭包缓存:

function make() { return static fn () =>1; }// bool(true)var_dump(make() === make());

RFC在实际应用模板上测得的性能提升约为 3%,但这个数字包含了另一项未被合并的优化。请将其视为一个方向,而非承诺。

14. 一些小东西


  • trim()移除换行符。"\f"连接默认字符trim(),ltrim()和rtrim()。
  • ARRAY_FILTER_USE_VALUE是 的默认模式的新名称array_filter(),在此可以显式地写出模式。
  • WeakMap引用。$ref = &$map[$object]当密钥尚未存在时有效。
  • ReflectionProperty::isReadable()并isWritable()告知当前作用域是否可以读取或写入属性,包括非对称可见性和钩子。序列化器和表单库需要此功能。
  • ReflectionParameter::getDocComment()读取参数的文档注释。
  • ReflectionAttribute::getShortName(),getNamespaceName()和inNamespace()。
  • mysqli::quote_string()一次调用即可对字符串进行引号和转义。
  • grapheme_strrev()按字素反转字符串,因此"👋🏽"始终保持一个表情符号。它在intl。
  • ini_get_all()builtin_default_value为每个指令提供一个。
  • 内置服务器(php -S)接受 HTTPQUERY方法,不再反映Host标头。
  • error_include_args新增的 INI 选项会在函数的错误信息中显示其参数。默认情况下此选项处于关闭状态。
  • OpenSSL为流获取 TLS 会话恢复、外部 PSK 和 TLS 1.3 早期数据。
  • Sodium获得了后量子 ML-KEM768 和 X-Wing KEM(使用 libsodium 1.0.22)、SHAKE 和 TurboSHAKE 以及 IP 地址加密。
  • Curl可以获取CURLOPT_SEEKFUNCTION,因此流式上传可以在重定向时回滚。
  • 国际价格IntlNumberRangeFormatter为“€3 – €5”,具体价格取决于当地语言环境。
  • Unicode 18.0在mbstring。


15. 什么会打破


升级生产环境前请阅读本节内容。

现在会话默认设置是安全的。`is`、session.use_strict_mode` is`和`is`都是安全的。

如果你的框架有自己的会话层或传递自己的会话选项,请检查其会话配置。如果应用程序使用默认设置从 JavaScript 读取会话 cookie,或者期望跨站请求会话 cookie ,则会出错。

session.cookie_httponlysession.cookie_samesite "Lax"session_start()

ValueError之前出现警告的地方现在会抛出异常。很多函数现在会因为传入的错误值而抛出异常:例如,传递给 `__init__`、`__init__` 或 `__init__` 的路径中包含空字节 (NUL)、file_exists()、is_file()、filesize()` stat()__init__` 、`__init__`或`__init__`sleep()的无效模式。

如果你的代码之前传递了无效值,现在会抛出异常。例如array_filter()、pathinfo()、scandir()、preg_grep()返回false错误,例如错误的 UTF-8 编码,而不是部分数组。

如果有意在字符串末尾保留换行符,请使用trim($value, " \n\r\t\v\0")。

SplFileObject更改了读取方式next(),fgets()并current()共享当前行。如果使用它逐行读取文件,请运行测试。

base_convert()当结果精度下降时,发出bindec()hexdec()octdec()通知。

session_encode()对于空会话,返回空字符串,而不是false。

内存严重不足时,程序会调用abort()并退出,退出代码为 134,而不是 1。你的进程监视器看到的是不同的代码。

16. 哪些内容已被弃用


弃用声明并非错误。你的代码仍可以运行,但会收到一条E_DEPRECATED通知,告知在 PHP 9 之前需要进行哪些更改。以下是一些示例:

Deprecated: Returning a value from a constructor is deprecatedDeprecated: Returning from a finally block is deprecatedDeprecated: Function is_integer() is deprecated since 8.6, use is_int() insteadDeprecated: Function is_double() is deprecated since 8.6, use is_float() insteadDeprecated: Function doubleval() is deprecated since 8.6, use floatval() insteadDeprecated: Function spl_object_hash() is deprecated since 8.6, consider using spl_object_id() instead

语言层面的一些更新:

  • __construct()__destruct()具有return值 ,以及一个生成器构造函数或析构函数。
  • let以及is作为类、接口、特性、枚举、函数或常量的名称。这两个词是为将来保留的,所以不要用它们来命名类名。
  • readonly可以用作函数名、namespace类常量名_或常量名。
  • is_a()当是is_subclass_of()字符串时。
  • ?array作为返回类型__debugInfo(),将写入array中。


函数家族:

  • is_integer(),is_long(),doubleval()和is_double()请使用
is_int(),is_float()和floatval()替换。
  • spl_object_hash()请使用spl_object_id()。
  • 增加mysqli_get_charset()和mysqli_stmt_init()。
  • php://filterURL中包含超过 16 个过滤器
  • ReflectionProperty::setValue()使用错误类的对象,以及ReflectionMethod::invoke()使用静态方法的对象。
  • create_sid()没有和的会话处理程序validateId()。


如何尝试PHP 8.6


无需等到11月19日,你可以自行编译分支。如下方法:


git clone --depth 1 --branch PHP-8.6 https://github.com/php/php-src.gitcd php-src./buildconf --force./configure --disable-all--enable-cli--enable-mbstring--disable-mbregex --enable-tokenizer --enable-ctype --enable-filtermake -j8 sapi/cli/php -r 'var_dump(clamp(150, 1,100));

这便是 PHP 8.6 版本。这是一个令人兴奋的版本,PHP 团队在这方面做得非常出色。

在过去一个月间,安全漏洞一直是人们关注的焦点,而且人们至今仍在讨论这些问题。我希望这些讨论不会掩盖这次的版本发布,它值得更多的人关注。

PHP 是一门很棒的语言。最近围绕它的一切热潮再次证明了这一点。我热爱 PHP,也热爱这个开源社区。

作者:手扶拖拉斯基

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信