CatchAdmin PHP 后台管理框架 Logo CatchAdmin

PHP 8.6 新特性一览

PHP 8.6 将于 2026 年 11 月 19 日发布,将带来部分函数应用、新的轮询 API、函数参数文档注释、一系列弃用项等新内容。

部分函数应用

部分函数应用(Partial Function Application,简称 PFA)允许创建一个闭包引用,其中部分参数已预先填充。一个简单的例子是:将字符串中所有空格替换为连字符的函数:

php
$makeSlug = str_replace(' ', '-', ?);

闭包创建完成后,可以像这样调用:

php
$makeSlug('Hello World');

// Hello-World

PFA 与管道运算符(pipe operator)结合时尤为有用,因为管道运算符始终要求可调用对象恰好接收一个参数。

php
$output = 'Hello World' 
    |> str_replace(' ', '-', ?)
    |> strtolower(...);

// hello-world

关于部分函数应用的全部细节,可以参阅专门介绍该特性的文章。

只读属性默认值

随着 PHP 8.4 引入属性钩子(property hooks),现在可以在接口上定义属性钩子:

php
interface MigratesUp
{
    public string $name { get; }

    public function up(): QueryStatement;
}

正因如此,在很多场景下,带默认值的只读属性(readonly properties)也显得合理:

php
final class CreateBooksTable implements MigratesUp
{
    public readonly string $name = '2026-01-01_create_books_table';
    
    public function up(): QueryStatement
    { /* … */ }
}

不过在 PHP 8.6 之前,无法为只读属性赋默认值。这是只读属性引入时有意为之的设计决策,因为带默认值的只读属性本质上就是一个常量。当然,那是在属性钩子可以定义在接口上之前;如今,当默认的、不可变的值是更大契约的一部分时,它确实是有意义的。

而这就是只读属性现在允许设置默认值的原因!

php
final class CreateBooksTable implements MigratesUp
{
    // ✅
    public readonly string $name = '2026-01-01_create_books_table';
    
    // …
}

轮询 API

新的轮询 API 首先是为了简化内部开发而创建的。PHP-FPM 以及 ZTS(Zend Thread Safety)模式下的信号处理等功能,都将受益于一个统一的基础平台。同时,新的轮询 API 也向用户空间(userland)开放,这可能会让 ReactPHP 或 Amp 等底层框架得以利用:

php
use Io\Poll\Context;
use Io\Poll\Event;
use Io\Poll\StreamPollHandle;
use Time\Duration;
 
// Create a poll context with automatic backend selection
$context = new Context();

// Create a non-blocking socket, just like before
$stream = stream_socket_server('tcp://0.0.0.0:8080', $errno, $errstr);
stream_set_blocking($stream, false);

// Wrap that stream in a new `StreamPollHandle` so that it can make use of the new API
$handle = new StreamPollHandle($stream);
 
// Add the handle to the context
$context->add($handle, [Event::Read], ['type' => 'server']);

while (true) {
    // Wait for one second, polling for new events
    $watchers = $context->wait(new Duration::fromSeconds(1));
    
    // …
}

需要特别注意的是,这个新的轮询 API 不会为 PHP 引入任何新的异步特性。它只是与异步特性交互的另一种(也是更简单的)方式——此前 PHP 在 I/O 多路复用方面只有 stream_select() 这一个选项。由于新 API 在可用时还能利用多种后端(如 epoll 或 WSAPoll),在达到一定规模后,其性能将优于 stream_select()。

新的轮询 API 也不附带内置的事件循环,因此将其封装为更高级别的抽象,仍然需要由用户空间库来完成。

新的 clamp 函数

clamp() 在许多框架中已经是相当常见的函数,现在它将随 PHP 8.6 内置发布。该函数确保给定值(数值或其他类型)处于给定边界之内;如果超出边界,则返回最近的边界值。

php
clamp(10, min: 0, max: 100); // Will return `10`
clamp(101, min: 0, max: 100); // Will return `100`
clamp(-1, min: 0, max: 100); // Will return `0`

clamp() 不只适用于整数。例如字符串:

php
clamp("y", "x", "z") // Will return "y"
clamp("a", "x", "z") // Will return "x"

或者 DateTime 对象:

php
clamp(
    value: new DateTimeImmutable('2025-01-01'), 
    min: new DateTimeImmutable('2026-01-01'), 
    max: new DateTimeImmutable('2026-12-31'),
); // Will return `DateTimeImmutable('2026-01-01')`

新的 Duration 类

新增了 \Time\Duration 类,用于表示持续时间:

php
use Time\Duration;

sleep(Duration::fromMilliseconds(500));

该类附带一系列方法,可对持续时间进行数学运算:

php
$baseDelay = Duration::fromMilliseconds(100);

$baseDelay->add(Duration::fromSeconds(2));

$attempt = 5;
$delay = $baseDelay->multiplyBy(2 ** $attempt);

也可以直接比较两个持续时间:

php
if ($durationA < $duractionB) {
    /* … */
}

并且可以将其用于新的轮询 API 等场景:

php
$watchers = $context->wait(new Duration::fromSeconds(1));

新的 isReadable 与 isWriteable 反射函数

新增了两个函数,用于指示反射属性是否为只读和/或可写。随着 PHP 8.4 引入属性钩子,在 PHP 的反射 API 中提供这些方法是有意义的:

php
final class Book
{
    private(set) string $title;
}

$property = new ReflectionProperty(Book::class, 'title');

$property->isReadable(scope: Book::class);
$property->isWriteable(scope: null);

最重要的是 $scope 变量,因为它决定了从哪个作用域可以读取或写入该属性。例如:私有属性可以在类内部读取,但不能从外部读取:

php
$property->isWriteable(scope: Book::class); // true
$property->isWriteable(scope: null); // false

可选地,还可以传入第二个 $object 参数。如果传入,反射 API 就能判断只读属性是否已被赋值,这也会决定它是否可写。

php
final class Book
{
    public readonly string $title;
}

$book = new Book();

$property = new ReflectionProperty(Book::class, 'title');

$property->isWriteable(scope: null, object: $book); // true

$book->title = 'Timeline Taxi';

$property->isWriteable(scope: null, object: $book); // false

函数参数文档注释

PHP 的反射 API 在 ReflectionParameter 上新增了 getDocComment() 方法。实际上,这意味着可以把这种写法:

php
/** @param Book[] $books */
function store(array $books): void { /* … */ }

改写成这种:

php
function store(
    /** @param Book[] */
    array $books,
): void { /* … */ }

当然,也可以把文档块放在参数的任一侧,写出更短的单行版本:

php
function store(/** @param Book[] */ array $books): void { /* … */ }
function store(array $books /** @param Book[] */): void { /* … */ }

注意,如果文档注释放在参数后面,它应该放在将该参数与下一个参数分隔开的逗号之前:

php
function store(
    array $books /** valid placement to link to $books */, 
    array $other, /** this one won't be detected */
): void { /* … */ }

虽然这个特性很可能对静态分析器最有用,但当然也可以通过反射来读取这些参数文档注释:

php
$reflection = /* … a method or function reflector */

$parameters = $reflection->getParameters;

$parameters[0]->getDocComment();

新的 SortDirection 枚举

PHP 8.6 带来一个新的内置枚举,用于表示排序方向:

php
enum SortDirection {
    case Ascending;
    case Descending;
}

框架和库可以自由选择在任何需要排序的地方支持该枚举,然后将 SortDirection 映射到该上下文所需的内容。注意,array_multisort() 或 scandir() 等内置 PHP 函数目前还不支持该枚举,但计划在未来支持。

会话默认值的安全性改进

PHP 修改了几个默认 ini 设置值,使会话在默认配置下更加安全:session.use_strict_mode 和 session.cookie_httponly 都将从默认 0 改为默认 1;session.cookie_samesite 也将设置为 Lax,而不再是完全不设置。RFC 概述了这些更改可能对项目产生的影响,这里也一并列出:

session.use_strict_mode

有意提供外部控制的会话 ID 的应用程序——例如,使用文件保存处理程序在子域之间进行共享密钥交接——其 ID 将被拒绝,因为首次请求时不存在匹配的会话文件。正确的做法是在发起方一侧使用 session_write_close() 写入并关闭会话,然后再将 ID 呈现给接收方。

使用自定义会话处理程序、且其 validateId() 方法无条件返回 true 的应用程序不受影响。这包括 Redis 和 Memcached 等常见后端——使用默认的 php-memcached 或 phpredis 会话处理程序时即是如此,它们不实现 validateId(),因此回退为返回 true。

HttpOnly 标志由浏览器强制执行。它阻止通过 document.cookie 读取会话 cookie 的值;它不影响浏览器是否随请求发送该 cookie。

在 JavaScript 中通过 document.cookie 读取会话 cookie 值的应用程序(例如,将 ID 嵌入自定义请求头)将无法再这样做。会话 ID 是服务端凭据,不应被客户端代码消费。需要通过客户端可访问的令牌进行请求关联的应用程序,应使用单独的、明确非 HttpOnly 的 CSRF 令牌,而不是会话 cookie 本身。

使用 SameSite=Lax 时,浏览器会在同站请求以及使用安全 HTTP 方法(GET 和 HEAD)的顶级跨站导航中发送会话 cookie;不会在跨站子资源请求或跨站 POST 请求中发送该 cookie。

依赖跨站 POST 携带会话 cookie 的应用程序——例如,SP 发起的 SAML SSO 流程或遗留的跨源表单提交——必须为这些端点显式设置 SameSite=None; Secure,或者迁移到基于令牌的流程。

如上所述,Chrome 和 Firefox 已经将 Lax 作为未携带 SameSite 属性的 cookie 的隐式默认值。在这些浏览器上运行的应用程序已经受此行为约束;这一更改使其在所有浏览器和 PHP 版本中变得明确且一致。

可调试的枚举

枚举现在可以实现 __debugInfo() 魔术方法,这意味着可以为枚举提供自定义的调试实现:

php
enum Status: int
{
    case OK = 200;
    case NOT_FOUND = 404;
    case FOUND = 302;
    case INTERNAL_SERVER_ERROR = 500;
    
    public function __debugInfo() {
		return [__CLASS__ . '::' . $this->name . ' = ' . $this->value];
	}
}
 
var_dump(Status::OK);

// enum(Status::OK) (1) {
//  [0]=>
//   string(16) "Status::OK = 200"
// }

弃用项

与往常一样,PHP 的任何次要版本都会带来一批弃用项:警告某些内容将在未来的 PHP 主版本中更改或移除。最好现在就开始修复弃用项,以免将来成为实际问题。

从 finally 块中返回

从 finally 中返回一直都非常令人困惑,也是微妙 bug 的根源。现在,这一行为已被弃用:

php
function getConfig(): array {
    try {
        return loadConfig();
    } finally {
        return [];
    }
}

弃用使用 let 作为标识符

此弃用的动机是为了将来能够将 let 用作关键字:

php
class let { /* … */ }

function let() {}

弃用使用 is 作为标识符

此弃用的动机是为了将来能够将 is 与 match 运算符结合使用:

php
class is { /* … */ }

function is() {}

弃用使用 namespace 作为类常量名

动机是为 namespace 保留未来的语法空间,例如,将来可以允许 ::namespace 伪常量,类似于 ::class:

php
class Foo {
  const NAMESPACE = '';
}

弃用将函数命名为 readonly

readonly 最初被允许用作函数名,是因为 WordPress 有一个同名函数。这是通过 PHP 词法分析器中的一个 hack 实现的,该 hack 现在已被弃用:

php
function readonly { /* … */ }

弃用使用 _ 作为常量及编译时别名

这延续了 PHP 8.4 中的一项弃用,当时单个下划线作为类名已被弃用。

php
const _ = 1;

传递被解释为数组的对象

array_walk() 或 inflate_init() 等一些函数在接收数组的同时也接受对象。其内部实现方式可能导致微妙的 bug,甚至造成内存损坏。这就是该行为在 PHP 8.6 中被弃用的原因:

php
$objectInsteadOfArray = new /* … */;

array_walk($objectInsteadOfArray, fn () => /* … */)

弃用 is_double

应使用 is_float() 代替 is_double():

php
if (is_double($value)) {
    /* … */
}

弃用 is_integer

应使用 is_int() 代替 is_integer():

php
if (is_integer($value)) {
    /* … */
}

弃用 is_long

应使用 is_int() 代替 is_long():

php
if (is_long($value)) {
    /* … */
}

弃用 doubleval

应使用 floatval() 代替 doubleval():

php
if (doubleval($value)) {
    /* … */
}

弃用 define 函数的 $case_insensitive 参数

对大小写不敏感常量的支持早已被移除。

php
define('MY_CONSTANT', 'value', case_insensitive: true);

弃用 $allow_string 为 false 时的 is_subclass_of 和 is_a

is_subclass_of() 用于检查某个对象或类名是否是其他指定类/接口的子类。第一个参数可以是待检查的对象,也可以是类名。第三个参数 $allow_string 控制第一个参数是否可以传字符串。该参数之所以有用,是因为当设为 false 时,字符串不会触发自动加载。然而,当 $allow_string 为 false 时传入字符串,函数将始终返回 false,即使该类根本不需要自动加载也是如此。

如果开发者明确指定不允许传字符串(默认是允许的),那么传入字符串就表明存在 bug,这正是它被弃用的原因。

php
is_subclass_of(Foo::class, Bar::class, allow_string: false);
is_a(Foo::class, Bar::class, allow_string: false);

弃用 strcoll

该函数现已弃用:

php
strcoll($a, $b);

弃用排序函数的 SORT_LOCALE_STRING 标志

php
sort($array, flags: SORT_LOCALE_STRING);

弃用 metaphone 函数

此函数可用于判断发音相似的单词。然而,它基于非常古老的算法,并且只支持英语。取而代之的是,建议依赖更现代的用户空间实现,例如 noodlesnz/double-metaphone。

弃用使用错误类型设置 ReflectionProperty 值

使用一个类的反射属性在另一个类上设置属性值的能力已被弃用:

php
class Original 
{
    private $property;
}
 
$property = new ReflectionProperty(Original::class, 'property');

注意,$property 引用的是 Original::$property。然而,以前可以使用同一个反射属性在其他对象上设置值:

php
class Other {}

$other = new Other();

$property->setValue($other, true);

弃用通过反射在真实对象上进行静态调用

ReflectionMethod::invoke() 和 ReflectionMethod::invokeArgs() 在调用实例方法时都接受一个可选对象。如果调用的是静态方法,则应传入 null:

php
class Book
{
    public static function create() {}
}

$method = new ReflectionMethod(Book::class, 'create');

$method->invoke(null);

然而,即使调用的是静态方法,仍然可以传入对象,尽管它不会起任何作用。该行为已被弃用:

php
$book = new Book();

$method = new ReflectionMethod(Book::class, 'create');

$method->invoke($book); // Only if the method is static

弃用的 ArrayIterator 方法

RFC 中提到:

各种 ArrayIterator 方法之所以存在,只是因为很长一段时间里它与 ArrayObject 共享同一个实现。这些方法大多意义不大,并且阻碍了对 ArrayIterator 实现的优化。

以下方法已被弃用:

php
ArrayIterator::getFlags()
ArrayIterator::setFlags()
ArrayIterator::asort()
ArrayIterator::ksort()
ArrayIterator::uasort()
ArrayIterator::uksort()
ArrayIterator::natsort()
ArrayIterator::natcasesort()
ArrayIterator::unserialize()
ArrayIterator::serialize()

弃用 spl_classes

spl_classes() 长期以来就有更好的替代方案 ReflectionExtension::getClassNames()。因此,该函数现在已被弃用:

php
spl_classes();

// Use this one instead:
new ReflectionExtension('spl')->getClassNames();

弃用 spl_object_hash

应使用 spl_object_id() 代替 spl_object_hash():

php
spl_object_hash($object);

如果仍然需要真正的哈希值而不是对象 ID,可以像这样生成:

php
$hash = $obj
    |> spl_object_id(...)
    |> dechex(...)
    |> str_pad(?, 16, '0', STR_PAD_LEFT)
    |> (fn ($x) => $x . '0000000000000000');

弃用 SplFileObject 中与 CSV 相关的方法

以下方法已被弃用:

php
SplFileObject::fgetcsv()
SplFileObject::fputcsv()
SplFileObject::setCsvControl()
SplFileObject::getCsvControl()

RFC 列出了以下原因:

这些 API 由于历史设计问题和不一致性而变得越来越难以维护。命名参数的引入进一步暴露了 API 设计和行为中的问题。

CSV 处理功能本质上不属于 SplFileObject,未来的工作最好由专门的 CSV 扩展来承担,以提供更干净、更易于维护的 API。

弃用 mysqli::stmt_init

php
$statement = $mysqli->stmt_init();
$statement->prepare($sql);

// Instead do this:
$statement = $mysqli->prepare($sql);

弃用 mysqli_get_charset

RFC 中提到:

此函数可用于检索当前所选字符集的内部实现细节。当 mysqli 针对 libmysql 编译时,它能提供更有意义的值,但自 PHP 8.2 起情况已不再如此。

提议弃用并移除该函数及其面向对象风格的别名 mysqli::get_charset(),且将来移除时不会提供替代品。任何 PHP 项目仍然使用此函数的可能性很小;即便在用,他们很可能也不知道它返回的是虚假的信息。

弃用向 session_set_save_handler 传递无效的会话处理程序

RFC 中提到:

用户自定义的会话处理程序并未被强制要求实现那些能够确保会话扩展在 session.use_strict_mode 这一 INI 设置下行为正确的方法。这一点很重要,因为该设置的默认值已在"安全会话配置默认值"RFC 中更改,并且绝不应被禁用。

要确保该行为有明确定义,需要实现的方法是 create_sid()validateId()

弃用从构造函数和析构函数中返回

虽然这在技术上曾经是被允许的,但在构造函数或析构函数中编写返回语句从来没有任何意义,因为这两个函数都只能在无法存储返回值的情况下被调用。这就是该行为现在被弃用的原因:

php
class Book
{
    public function __construct() 
    {
        return 1;
    }
    
    public function __destruct() 
    {
        return 0;
    }
}
本作品采用《CC 协议》,转载必须注明作者和本文链接