简介:
自 PHP 8.0 发布以来,该语言经历了一系列重要的功能迭代与性能改进。对于开发者而言,系统理解这些新特性不仅是技术更新的需要,更是优化代码设计、降低维护成本的有效途径。本分类将按版本梳理 PHP 8.x 的新特性。
8.1官方手册参考指南(含其他特性):官方链接
一、枚举
PHP8.1 正式引入枚举(Enumeration,简称 Enum)特性,解决了传统开发中用「类常量」「数组」管理固定值集合时的类型不安全「取值范围无法约束」「语义不清晰」等问题。
核心概念:枚举是一种特殊的自定义数据类型,用于定义一组命名且不可变的常量集合,强制变量只能取集合内的某个值。PHP 枚举分为两种核心类型(纯枚举、带值枚举),且所有枚举本质上是「final 类」,无法继承、无法实例化(单例设计)。
纯枚举示例:
php
<?php
declare(strict_types=1);
/**
* 纯枚举类
* 无关联值,仅表示「命名常量」
* 标识状态(如开关)
*/
enum EnumExample{
case Active; // 激活
case Inactive; // 未激活
case Banned; // 封禁
}
$status = EnumExample::Active;
var_dump($status->name); // string(6) "Active"
带值枚举示例:
php
<?php
declare(strict_types=1);
/**
* 带值枚举
* 关联标量值(仅 int/string),且值类型统一
* 需要状态 + 对应值(如编码)
*/
enum HttpEnumExample : int {
// 通常用于 GET 和 POST请求成功, 服务器已成功处理同时响应体中包含所请求的数据
case OK = 200;
// 临时重定向, 常用于临时活动页或登录后跳转
case FOUND = 302;
// 请求参数错误
case BAD_REQUEST = 400;
// 未认证。请求要求用户的身份验证
case UNAUTHORIZED = 401;
// 服务器内部错误
case INTERNAL_SERVER_ERROR = 500;
}
$http = HttpEnumExample::OK;
var_dump($http->name); // string(2) "OK"
var_dump($http->value); // int(200) 注:纯枚举无此属性
枚举的核心特性:
-
内置默认方法
方法 适用类型 作用 cases(): array所有枚举 返回枚举所有案例的数组(按定义顺序) from(mixed $value)带值枚举 根据值查找枚举案例,值不存在则抛出 ValueError异常tryFrom(mixed $value)带值枚举 根据值查找枚举案例,值不存在则返回 null(推荐替代from)示例:
phpecho '<pre>'; $httpArray = HttpEnumExample::cases(); foreach($httpArray as $http){ var_dump($http->name.'->'.$http->value); // string(7) "OK->200" ... } // from():值存在返回案例,不存在抛异常 $isOk = HttpEnumExample::from(200); // $isFound = TestHttpEnum::from(303); //抛出异常 var_dump($isOk); // 枚举案例 enum(HttpEnumExample::OK) // tryFrom():值不存在返回null(更安全) $isOk = HttpEnumExample::tryFrom(200); $isFound = HttpEnumExample::tryFrom(303); var_dump($isOk); // 枚举案例 enum(HttpEnumExample::OK) var_dump($isFound); // NULL -
自定义方法与构造函数,枚举支持定义普通方法「静态方法」。
甚至私有构造函数(禁止外部实例化),但构造函数必须是private(枚举默认单例)注:PHP8.4版本已经禁用示例:
php<?php declare(strict_types=1); enum HttpEnumExample : int { case OK = 200; case FOUND = 302; case BAD_REQUEST = 400; case UNAUTHORIZED = 401; case INTERNAL_SERVER_ERROR = 500; /** * 自定义方法:获取状态的中文描述 * @return string */ public function getLabel() : string{ return match($this){ self::OK => "请求成功", self::FOUND => "临时重定向", self::BAD_REQUEST => "请求参数错误", self::UNAUTHORIZED => "未认证", self::INTERNAL_SERVER_ERROR => "服务器内部错误" }; } /** * 静态方法:批量获取「值-标签」映射 * @return array */ public static function getLabelMap(): array{ $map = []; foreach(self::cases() as $case){ $map[$case->value] = $case->getLabel(); } return $map; } } $status = HttpEnumExample::OK; echo '<pre>'; var_dump($status->getLabel()); // 请求成功 var_dump($status::getLabelMap()); // 数组 [200 => "请求成功",...] -
类型提示与类型安全。枚举可作为参数「返回值」「属性」的类型提示,强制传入 / 返回的值必须是枚举案例,杜绝非法值。
-
枚举的比较,枚举案例是单例对象,推荐使用「严格比较(===)」,松散比较(==)也可但不推荐
-
反射(ReflectionEnum),PHP 提供
ReflectionEnum类(继承自ReflectionClass),用于动态获取枚举信息 -
序列化与反序列化,枚举支持
serialize()/unserialize(),且反序列化后仍指向原单例对象,保证一致性
补充:注意事项与最佳实践
核心限制:
枚举是 final 类,无法继承;
枚举不能实例化(new OrderStatus() 抛 Error);
带值枚举的值必须唯一(重复值抛 Fatal Error);
枚举不能定义属性(仅能通过 name/value 访问固定值)
最佳实践:
优先使用 tryFrom() 而非 from():避免未捕获的 ValueError,提升代码健壮性;
枚举方法保持单一职责:仅处理与枚举本身相关的逻辑(如标签转换),不写复杂业务逻辑;
结合接口统一契约:多个枚举需实现相同逻辑时,通过接口约束;
用 match 替代 switch:穷尽检查确保覆盖所有案例,避免遗漏;
带值枚举优先用 int 类型:便于扩展(如后续加状态时,int 比 string 更灵活)
二、只读属性
PHP8.1 引入的只读属性是对对象属性不可变性的语法级 增强。只读属性是通过 readonly 关键字修饰的实例属性,仅能在对象初始化阶段(构造函数内)赋值一次,赋值后任何修改操作都会触发 Error 异常,从语法层面强制保障属性的不可变性。
语法规则:
1. readonly 关键字必须位于访问修饰符(public/protected/private)之后、类型声明之前;
2. PHP8.1 中,只读属性必须显式声明类型(不能省略类型,也不能用 mixed,PHP8.2 开始支持 mixed);
3. 只读属性仅支持实例属性,不能修饰静态属性(static);
4. 支持所有合法的类型声明(标量、对象、联合类型、Nullable 类型等)
注意规避「浅只读」坑点。若属性类型为「标量(int/string/bool/float)」:完全不可修改;若属性类型为「数组 / 对象」:属性本身的引用不可改,但数组元素 / 对象属性可自由修改。
示例:
php
<?php
declare(strict_types=1);
class Example
{
public readonly ?string $msg;
public function __construct(
?string $msg = '信息',
public readonly string $otherMsg = '另一个信息'
) {
$this->msg = $msg === '信息' ? '第一次改变信息' : $msg;
// 使用属性构造器提升,其属性赋值在进入构造函数函数体之前就已经赋值完成了。
// $this->otherMsg = '第一次改变另一个信息'; // 这行代码会抛出错误,因为 otherMsg 已经被赋值了,并且是只读的。
}
public function getChangeMsg(): string
{
$this->msg = '尝试第二次改变信息';
return $this->msg;
}
}
$exampleClass = new Example();
echo '<pre>';
try {
var_dump($exampleClass->msg); // 输出: string(9) "信息"
var_dump($exampleClass->otherMsg); // 输出: string(12) "另一个信息"
var_dump($exampleClass->getChangeMsg()); // 抛出异常
} catch (Error $e) {
echo $e->getMessage(); // Cannot modify readonly property Example::$msg
}
最佳实践:
按需使用:仅对「赋值后无需修改」的属性使用 readonly,避免滥用(如业务属性需要动态修改的,不要用);
结合构造函数参数提升:简化代码,提升可读性(推荐写法);
初始化校验:在构造函数内对只读属性做合法性校验(如金额非负、邮箱格式正确);
三、一阶可调用语法(First-class)
一阶可调用语法是 PHP 8.1 推出的语法糖级核心特性,官方定义:Callable 表达式语法,用于极简、安全、优雅地创建可调用对象(Callable),彻底解决了传统 PHP 中回调写法丑陋、无类型检查、IDE 不识别、易写错的痛点。
语法:用 函数/方法名(...) 的语法,直接将任意可调用元素转换为标准闭包(Closure),... 是固定语法标记(非可变参数、非参数展开)
示例:
php
<?php
declare(strict_types=1);
class Example
{
public function getMsg(string $msg = '')
{
return $msg ? $msg : '执行了';
}
public static function getStaticMsg(string $msg = '')
{
return $msg ? $msg : '执行了';
}
}
/** 传统写法 */
echo '<pre>';
// 字符串函数名
$func = 'strlen';
var_dump($func('把"strlen"这个函数当做一个变量来传递,$func("hello") 相当于调用 strlen("hello")')); // 99
// 数组静态方法
$staticMethod = [Example::class, 'getStaticMsg'];
var_dump($staticMethod()); // 执行了
// 数组对象方法
$test = new Example();
$objectMethod = [$test, 'getMsg'];
var_dump($objectMethod()); // 执行了
/** 一级可调用语法 */
$func1 = strlen(...);
var_dump($func1('普通函数:类型安全、IDE 识别')); // 40
$staticMethod1 = Example::getStaticMsg(...);
var_dump($staticMethod1('静态方法:语法简洁、无数组冗余')); // 静态方法:语法简洁、无数组冗余
$objectMethod1 = $test->getMsg(...);
var_dump($objectMethod1('对象方法:语义直观、自动绑定作用域')); // 对象方法:语义直观、自动绑定作用域
// 闭包 / 匿名函数
$add = fn($a, $b) => $a + $b;
$callable = $add(...);
var_dump($callable(1, 2)); // 3
$arr = ['apple', 'banana', 'cherry'];
var_dump(array_map('strlen', $arr)); // 旧写法 数据[0=>5, 1=>6, 2=>6]
var_dump(array_map(strlen(...), $arr)); // 新写法 数据[0=>5, 1=>6, 2=>6]
注意:不支持通过此语法(例如 new Foo(...))创建对象,因为不会视 new Foo() 语法为调用;一级可调用语法不能与 nullsafe 运算符结合使用
四、新的初始化器
PHP 8.1 核心的语法升级之一,允许在「编译期可解析的上下文」中,直接使用 new 类名(参数) 作为默认值 / 初始值,但需要满足"常量表达式"的要求,禁止运行时变量、函数、动态值。
这一特性主要涵盖四个场景:
-
函数/方法参数默认值(支持箭头函数)
示例:
php<?php declare(strict_types=1); class Example { public function __construct( public DateTime $createTime = new DateTime() ){} } $ExampleClass = new Example(); var_dump($ExampleClass->createTime->format('Y-m-d H:i:s')); // 输出当前时间 -
静态变量初始化器
php<?php declare(strict_types=1); function getDateTimeStr() { static $date = new DateTime(); // 低版本会触发error return $date->format('Y-m-d H:i:s'); } var_dump(getDateTimeStr()); // 当前时间 -
属性参数(支持嵌套属性)
php<?php declare(strict_types=1); // 普通类 class Role { public function __construct(public string $name) {} } // 注解(Attribute) 类 #[Attribute] class Auth { public function __construct(public Role $role = new Role("默认管理员")) {} } // 用法1:不传参 → 自动使用 new 初始化的默认对象 #[Auth] class Admin {} // 用法2:手动传 new 对象 → 注解参数直接传实例 #[Auth(new Role("普通用户"))] class User {} echo "<pre>"; // 获取Admin的注解 $adminAttr = new ReflectionClass(Admin::class)->getAttributes(Auth::class)[0]->newInstance(); echo "Admin 权限:" . $adminAttr->role->name; // Admin 权限:默认管理员 // 获取User的注解 $userAttr = new ReflectionClass(User::class)->getAttributes(Auth::class)[0]->newInstance(); echo "User 权限:" . $userAttr->role->name; // User 权限:普通用户 -
全局常量(类常量存在限制)
php<?php declare(strict_types=1); class Example { // ❌ 非法:类常量不支持 new // public const OBJ = new stdClass(); // Fatal error // ❌ 非法:类属性(静态)不支持 new // public $obj = new stdClass(); // Fatal error public function __construct(public string $name = 'admin') {} } // ✅ 合法:全局常量支持 new const DEFAULT_ROLE = new Example(); print_r(DEFAULT_ROLE->name);补充(明确禁止的语法):
php<?php declare(strict_types=1); class Example { // ❌ 类属性、类常量的明确排除 均不合法 public static $classOne = new stdClass(); public $classTwo = new stdClass(); public const CLASS_THREE = new stdClass(); } // ❌ 禁止:动态类名 function funOne($a = new (CLASS_NAME_CONSTANT)()){} // ❌ 禁止:匿名类 function funTwo($a = new class {}){} // ❌ 禁止:参数解包 function funThree($a = new A(...[])){} // ❌ 禁止:非常量表达式作为参数 function funFour($a = new B($someVariable)){}
五、纯交集类型
PHP 8.1 引入了 纯交集类型(Pure Intersection Types),允许开发者使用 & 符号将多个类/接口类型 组合起来,表示一个值必须同时是所有这些类型的实例。"纯"的含义:目前 PHP 的交集类型只能包含类类型(类名、接口名),不能包含标量类型(如 int、string)。这与联合类型(可包含标量)形成对比。
可用于:参数类型、返回类型、属性类型、instanceof 操作符右侧
示例:
php
<?php
declare(strict_types=1);
function process(Iterator & Countable $value): int {
// $value 必须同时实现 Iterator 和 Countable
foreach ($value as $item) { /* ... */ }
return count($value);
}
$data = ['apple', 'banana', 'cherry'];
// ArrayIterator 既实现 Iterator,也实现 Countable
$iterator = new ArrayIterator($data);
var_dump(process($iterator)); // 输出: int(3)
补充:交集类型可以与联合类型嵌套,但不能直接在同一个类型声明中混用 & 和 |(除非使用括号)。其中,& 的优先级高于 |,但直接书写 A&B|C 会被解释为 (A&B)|C,然而由于语法限制,这样的写法会被拒绝,必须显式使用括号。
php
<?php
declare(strict_types=1);
// ✅ 合法:表示 $value 要么是 (A 且 B),要么是 C
function fun1((A & B) | C $value): void {}
// ❌ 错误!会解析为 (A&B)|C 吗?实际上会报错,因为优先级不明确
function fun2( A & B | C $value): void {}
六、Never 返回类型
在 PHP 8.1 之前,函数可以声明返回类型为 void,表示该函数不返回任何值(即没有 return 语句,或者 return; 不带值)。但有些函数不仅不返回值,它们根本不会正常返回------要么抛出异常,要么调用 exit/die 终止脚本执行。PHP 8.1 引入了 never 类型来明确标记这种"永不返回"的函数,让开发者和静态分析工具能够更准确地理解程序的控制流。
范围:never 作为返回类型声明,用于函数或方法定义中,也可以用于闭包和箭头函数。
规则:never 类型只能在返回类型位置使用;声明为 never 的函数或方法绝对不能正常返回(即不能有 return; 或 return $value; 语句,除非该语句在异常或 exit 之后且不可达);如果函数声明为 never 却正常返回,PHP 会抛出一个编译时错误(或运行时 TypeError)。
示例:
php
<?php
declare(strict_types=1);
function redirect(string $url): never {
header("Location: $url");
exit();
}
function alwaysThrows(): never {
throw new RuntimeException("Something went wrong");
}
$handler = function(): never {
throw new Exception("Abort");
};
$arrow = fn(): never => throw new Exception("Abort");
七、Final 类常量
PHP 8.1 之前,类常量可以被任何继承它的子类重新定义,即使是在 final 类中定义的常量也是如此。这种行为虽然灵活,但也带来了问题:父类常量的语义可能被子类无意或有意地改变,破坏了封装性。PHP 8.1 引入了 final 修饰符用于类常量,声明后该常量 不能被子类重写。
语法:final 可以与其他可见性修饰符(public、protected、private)一起使用。它的位置在可见性修饰符之前/后,常量名之前。
示例:
php
<?php
declare(strict_types=1);
class A {
// 两种均可用,官方示例为:final public
final public const X = 1;
public final const Y = 2;
public const Z = 3;
}
class B extends A {
public const Z = 4; // 会重写
public const X = 2; // 不能被重写,抛出 Fatal error: Cannot override final constant A::X
}
八、对字符串键控数组的数组解包支持
在 PHP 8.1 之前,数组解包(...)仅支持数字索引数组,遇到字符串键名时会直接抛出错误。PHP 8.1 通过 RFC 投票正式打破了这个限制,采用 array_merge 的语义实现了对字符串键数组的解包支持。
- 采用 array_merge 语义,后者覆盖前者。
这是理解该特性的关键。当使用 ... 展开数组时,其行为与 array_merge 函数保持一致:对于字符串键,如果键名重复,后面出现的值会覆盖前面的值。
示例:
php
<?php
declare(strict_types=1);
$array1 = ["a" => 1];
$array2 = ["a" => 2, "b" => 3];
$result = ["a" => 0, ...$array1, ...$array2];
var_dump($result); // 输出:Array ( [a] => 2 [b] => 3 )
- 数字键的例外:保持"追加"语义。
对于数字索引(或数字字符串键),PHP 8.1 依然保留了原有的"重编号(Renumbering)"行为,不会保留原有的数字键名,而是像列表一样追加。
示例:
php
<?php
declare(strict_types=1);
$arr1 = [1 => "one", 2 => "two"];
$arr2 = ["3" => "three"]; // 字符串数字键,会被转换为整数 3
$result = [...$arr1, ...$arr2];
var_dump($result); // 输出:Array ( [0] => one [1] => two [2] => three )
- 与 + 运算符的核心区别:前者"后覆盖",后者"前保留"。
数组解包 (...) => 行为逻辑:后者覆盖前者 (类似 array_merge);数字键:重新编号,不保留原键;关联键:后面覆盖前面。
+ 运算符 => 行为逻辑:前者保留,后者忽略 (类似"First Wins");数字键:保留原数字键,但仅当键不存在时才追加;关联键:前面优先,后面被丢弃。
示例:
php
<?php
declare(strict_types=1);
$arr1 = ["a" => 1, 1 => "one"];
$arr2 = ["a" => 2, 2 => "two"];
// 使用 ... 解包 字符串键覆盖,数字键重排
$unpack = [...$arr1, ...$arr2];
print_r($unpack); // 输出:Array ( [a] => 2 [0] => one [1] => two )
// 使用 + 运算符 字符串键保留第一个,数字键保留原样且不冲突的追加
$plus = $arr1 + $arr2;
print_r($plus); // 输出:Array ( [a] => 1 [1] => one [2] => two )
- 为什么是 array_merge 而非"直接丢弃"?
在开发该特性时,社区曾讨论过另一种方案:直接丢弃字符串键,将其视为普通值重排(类似 [...$arr1] 仅提取值)。但最终选择 array_merge 语义,主要有两个原因:
直觉一致性:解包操作符 ... 在字面量中可以被理解为把数组内容"写"进去。例如 ["a" => 1, ...["a" => 2]],就像手动写入了两次 "a" 键,后写的值(2)理应生效。
与参数解包对齐:在 PHP 8.0 中,参数解包(func(...$args))已经支持了字符串键(映射为命名参数)。为了保持语法统一,数组解包也不应简单丢弃这些键。
九、纤程(Fiber)
PHP 8.1 正式引入了纤程(Fiber) 特性,这是 PHP 在语言层面首次原生支持协作式多任务(cooperative multitasking)的轻量级并发机制。在此之前,PHP 开发者主要依靠生成器(Generator)或第三方扩展(如 Swoole、ReactPHP)来实现异步非阻塞编程。纤程的加入,使得 PHP 能够以更优雅、更灵活的方式处理 I/O 密集型任务,并为构建高并发应用提供了新的可能性。
定义:纤程是一种协程(coroutine)的实现,它允许在单线程内实现多个执行流的切换。与线程不同,纤程的切换完全由开发者显式控制(协作式),而非操作系统抢占式调度。每个纤程拥有自己的调用栈,可以在任意位置挂起(suspend),并将控制权交还给主调用者,稍后再从挂起点恢复(resume) 继续执行。
部分应用场景示例:
1、并发调用第三方 API :同时发起 3 个请求,挂起等待,所有响应返回后自动恢复,总耗时 = 最慢的那个接口的耗时
2、并行数据库查询:避免 "查完 A 表再查 B 表" 的串行等待,数据库查询时间从 T1+T2+T3 缩短为 max(T1,T2,T3)。
3、非阻塞缓存操作:缓存操作是典型的网络 IO,纤程能让等待缓存响应的时间不浪费。例如:同时从 Redis/Memcached 获取多个 key 的值(如get('user:1') + get('product:100'))
4、文件上传与处理:文件 IO 是慢操作,纤程能避免阻塞进程,同时处理多个文件任务。
5、后台任务队列:相比传统的 "多进程消费队列",纤程能在一个进程内同时处理成百上千个任务,内存占用极低。
6、定时任务调度:避免 "一个定时任务阻塞其他任务",无需为每个任务开一个进程 / 线程。
7、日志异步写入:日志写入是高频 IO 操作,纤程能让主业务逻辑不受日志写入速度影响
8、WebSocket 实时通信:比多进程,纤程能在一个进程内维护数万 WebSocket 连接,内存占用仅为多进程的几十分之一。
9、批量数据转换:据处理中往往夹杂着大量文件 / 数据库 IO,纤程能让 CPU 和 IO 设备同时忙碌
示例:
php
<?php
declare(strict_types=1);
/**
* 定义 Task 类,用于封装纤程任务及其当前结果
*/
class Task {
public function __construct(
public Fiber $fiber, // 存储纤程对象
public mixed $result = null // 存储每次恢复时传入/返回的值
) {}
}
/**
* 定义调度器类,管理多个任务的轮询执行
*/
class Scheduler {
// 存储所有 Task 对象的数组
private array $tasks = [];
/**
* 添加一个可调用任务到调度器
* @param callable $fn 要执行的任务函数
* @param mixed ...$args 传递给任务函数的参数
*/
public function add(callable $fn, mixed ...$args): void {
// 创建一个新的纤程,其回调是一个闭包,用于调用 $fn 并传递参数
$fiber = new Fiber(function () use ($fn, $args) {
// 执行用户任务并返回结果
return $fn(...$args);
});
// 将任务封装为 Task 对象,存入任务数组
$this->tasks[] = new Task($fiber);
}
/**
* 运行调度器,轮询执行所有任务直到完成
*/
public function run(): void {
// 当还有未完成的任务时持续循环
while (!empty($this->tasks)) {
// 遍历当前所有任务
foreach ($this->tasks as $i => $task) {
// 获取当前任务关联的纤程对象
$fiber = $task->fiber;
// 情况1:纤程已启动且尚未终止(即处于挂起状态) -> 恢复执行
if ($fiber->isStarted() && !$fiber->isTerminated()) {
// 调用 resume() 恢复纤程,并将上次挂起时保存的 result 作为参数传入
// resume() 返回纤程下一次挂起时传出的值(或 null 如果纤程结束)
$task->result = $fiber->resume($task->result);
}
// 情况2:纤程尚未启动 -> 首次启动
elseif (!$fiber->isStarted()) {
// 调用 start() 启动纤程,执行到第一个挂起点
// start() 返回第一个 Fiber::suspend() 传出的值
$task->result = $fiber->start();
}
// 如果纤程已经终止(执行完毕或抛出异常),则输出返回值并从任务列表中移除
if ($fiber->isTerminated()) {
// 获取纤程的返回值(即任务函数的 return 值)
echo "Task finished with return: ", $fiber->getReturn(), "\n";
// 从任务数组中删除当前任务
unset($this->tasks[$i]);
}
}
// 一轮遍历结束后,继续 while 循环,处理可能剩余的任务
}
}
}
// 使用示例:创建调度器
$scheduler = new Scheduler();
echo '<pre>';
// 添加任务 A:先输出 step1,挂起,然后输出 step2,返回字符串
$scheduler->add(function () {
echo "Task A: step 1\n";
Fiber::suspend(); // 挂起,控制权交回调度器
echo "Task A: step 2\n";
return "A done";
});
// 添加任务 B:结构与任务 A 相同
$scheduler->add(function () {
echo "Task B: step 1\n";
Fiber::suspend();
echo "Task B: step 2\n";
return "B done";
});
// 启动调度器,开始轮询执行
$scheduler->run();
// 执行结果:
// Task A: step 1
// Task B: step 1
// Task A: step 2
// Task finished with return: A done
// Task B: step 2
// Task finished with return: B done