PHP 8.x 新特性实战:Enum、只读类、Fibers 完整开发教程

为什么要升级到 PHP 8.x?
PHP 8.x 系列(8.1、8.2、8.3)带来了大量改变游戏规则的新特性,很多特性能直接减少代码量、提升类型安全、增强运行性能。然而不少团队仍在跑 PHP 7.4,原因是"没时间研究新特性"或"不知道升了有什么用"。
本文不讲理论,全程用真实项目场景演示以下核心特性:
Fibers(协程纤程):实现轻量级并发,告别 ReactPHP 的复杂度
枚举(Enum):替代常量类,消灭魔法字符串
只读属性与只读类:构建不可变值对象,防止意外修改
Intersection Types(交叉类型):更精确的类型约束
命名参数(Named Arguments):让函数调用自文档化
match 表达式:switch 的强类型升级版
每个特性都会给出:用途说明 → 旧写法对比 → 新写法实战 → 踩坑点提醒。
Fibers:PHP 的协程支持终于来了
PHP 8.1 引入了 Fibers,这是 PHP 原生协程的基础。它允许暂停和恢复一段函数执行,非常适合实现异步任务调度器、流式响应等场景。
理解 Fiber 的关键:它不是多线程,而是协作式多任务——你主动让出控制权,主程序继续执行其他任务,之后再恢复你。
基本用法示例:
<?php
// 创建一个 Fiber
$fiber = new Fiber(function(): void {
echo "Fiber 开始执行\n";
$value = Fiber::suspend('第一次挂起'); // 挂起,传值给外部
echo "Fiber 恢复,收到: {$value}\n";
Fiber::suspend('第二次挂起');
echo "Fiber 结束\n";
});
// 启动 Fiber,直到第一次 suspend
$result1 = $fiber->start();
echo "外部收到: {$result1}\n"; // 输出: 外部收到: 第一次挂起
// 恢复 Fiber,传入一个值
$result2 = $fiber->resume('外部传入的数据');
echo "外部收到: {$result2}\n"; // 输出: 外部收到: 第二次挂起
$fiber->resume(); // 最后一次恢复,Fiber 结束实战场景:简易任务调度器
<?php
class SimpleScheduler {
private array $fibers = [];
public function addTask(callable $callback): void {
$this->fibers[] = new Fiber($callback);
}
public function run(): void {
// 初始化所有 Fiber
foreach ($this->fibers as $fiber) {
$fiber->start();
}
// 循环调度,直到所有 Fiber 完成
while (!empty($this->fibers)) {
foreach ($this->fibers as $key => $fiber) {
if ($fiber->isSuspended()) {
$fiber->resume();
} elseif ($fiber->isTerminated()) {
unset($this->fibers[$key]);
}
}
}
}
}
$scheduler = new SimpleScheduler();
$scheduler->addTask(function() {
echo "任务A: 第一步\n";
Fiber::suspend();
echo "任务A: 第二步\n";
Fiber::suspend();
echo "任务A: 完成\n";
});
$scheduler->addTask(function() {
echo "任务B: 第一步\n";
Fiber::suspend();
echo "任务B: 完成\n";
});
$scheduler->run();
// 输出顺序:任务A第一步 → 任务B第一步 → 任务A第二步 → 任务B完成 → 任务A完成⚠️ 踩坑:Fiber 不能跨 Fiber 调用 Fiber::suspend(),必须在当前 Fiber 内部调用。另外,Fiber 异常不会自动冒泡到外部,需要用 try/catch 在 Fiber 内部处理,或通过 $fiber->getReturn() 检查结果。
枚举(Enum):消灭魔法字符串和常量类
PHP 8.1 之前,我们用常量类或 const 数组来表示有限状态集,代码重复且不类型安全。现在 Enum 原生支持了。
旧写法(常量类,PHP 7.x):
<?php
class OrderStatus {
const PENDING = 'pending';
const PAID = 'paid';
const SHIPPED = 'shipped';
const CANCELLED = 'cancelled';
}
// 调用时没有类型约束,传什么字符串都行,IDE 也不会报错
function processOrder(string $status): void {
if ($status === OrderStatus::PAID) {
// ...
}
}
processOrder('piad'); // 拼写错误,运行时才发现新写法(PHP 8.1+ Enum):
<?php
enum OrderStatus: string {
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
// Enum 可以有方法!
public function label(): string {
return match($this) {
OrderStatus::Pending => '待支付',
OrderStatus::Paid => '已支付',
OrderStatus::Shipped => '已发货',
OrderStatus::Cancelled => '已取消',
};
}
public function canTransitionTo(OrderStatus $next): bool {
return match($this) {
OrderStatus::Pending => $next === OrderStatus::Paid || $next === OrderStatus::Cancelled,
OrderStatus::Paid => $next === OrderStatus::Shipped,
OrderStatus::Shipped => false,
OrderStatus::Cancelled => false,
};
}
}
// 使用
function processOrder(OrderStatus $status): void {
echo "当前状态: " . $status->label() . "\n";
if ($status->canTransitionTo(OrderStatus::Shipped)) {
echo "可以发货\n";
}
}
processOrder(OrderStatus::Paid); // ✅
// processOrder('piad'); // ❌ 直接类型错误,IDE 实时提示
// 从数据库值恢复 Enum
$dbValue = 'paid';
$status = OrderStatus::from($dbValue); // 找不到会抛异常
$status2 = OrderStatus::tryFrom('unknown'); // 找不到返回 null,更安全Enum 实现接口(高级用法):
<?php
interface HasColor {
public function color(): string;
}
enum Suit: string implements HasColor {
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
public function color(): string {
return match($this) {
Suit::Hearts, Suit::Diamonds => 'red',
Suit::Clubs, Suit::Spades => 'black',
};
}
}
echo Suit::Hearts->color(); // 输出: red⚠️ 踩坑:纯 Enum(不带类型声明)的 case 没有 value,不能用 ->value;带类型(enum X: string)才有。from() 和 tryFrom() 只对 backed enum 有效。Enum 不支持 new 实例化,也不支持 extends 继承其他类。
只读属性与只读类:构建不可变值对象
PHP 8.1 引入了只读属性(readonly),PHP 8.2 进一步引入了只读类(readonly class)。这对于领域驱动设计(DDD)中的值对象(Value Object)来说是天作之合。
场景:构建一个金额值对象
<?php
// PHP 8.2+ 只读类:所有属性自动变为 readonly
readonly class Money {
public function __construct(
public int $amount, // 金额(分),防止浮点数精度问题
public string $currency, // 货币代码,如 CNY
) {
if ($amount < 0) {
throw new \InvalidArgumentException("金额不能为负数: {$amount}");
}
if (strlen($currency) !== 3) {
throw new \InvalidArgumentException("货币代码必须为3位: {$currency}");
}
}
public function add(Money $other): self {
if ($this->currency !== $other->currency) {
throw new \DomainException("不同币种不能相加");
}
return new self($this->amount + $other->amount, $this->currency);
}
public function format(): string {
return number_format($this->amount / 100, 2) . ' ' . $this->currency;
}
public function equals(Money $other): bool {
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
$price = new Money(9900, 'CNY'); // 99.00 CNY
$tax = new Money(891, 'CNY'); // 8.91 CNY
$total = $price->add($tax);
echo $total->format(); // 输出: 108.91 CNY
// $price->amount = 100; // ❌ 报错:Cannot modify readonly propertyPHP 8.1 单个属性只读(更细粒度控制):
<?php
class User {
public readonly string $id;
public string $name; // 可修改
public readonly string $email; // 注册后不可修改
public function __construct(string $id, string $name, string $email) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public function rename(string $newName): void {
$this->name = $newName; // ✅ 可以修改
// $this->email = '...'; // ❌ 报错
}
}⚠️ 踩坑:只读属性只能赋值一次,且只能在声明它的类内部赋值(构造函数内)。clone 操作不会重置只读属性,所以无法用 clone $obj; $clone->readonly = newVal; 来"修改"。PHP 8.3 引入了 clone with 语法(尚在 RFC 阶段),目前可用工厂方法代替。
match 表达式与命名参数:日常必用的提效利器
这两个特性虽然不如 Fibers 那么炫酷,但在日常开发中出现频率极高,值得专门介绍。
match 表达式(PHP 8.0+)
match 是 switch 的严格类型版本:使用 === 比较,必须有返回值,不会 fall-through,未匹配时抛出 UnhandledMatchError。
<?php
// 旧写法 switch(PHP 7.x)
function getDiscount_old(string $userType): int {
switch ($userType) {
case 'vip':
return 20;
case 'member':
return 10;
default:
return 0;
}
}
// 新写法 match(PHP 8.0+)
function getDiscount(string $userType): int {
return match($userType) {
'vip' => 20,
'member' => 10,
default => 0,
};
}
// match 可以匹配多个条件
$httpCode = 404;
$message = match(true) {
$httpCode >= 500 => '服务器错误',
$httpCode === 404 => '页面不存在',
$httpCode === 401, $httpCode === 403 => '无权访问',
$httpCode >= 200 && $httpCode < 300 => '成功',
default => '未知状态',
};
echo $message; // 输出: 页面不存在命名参数(PHP 8.0+)
命名参数让函数调用更清晰,尤其是有多个可选参数时,无需记忆参数顺序。
<?php
// 旧写法:必须记住参数顺序,第 3、4、5 个参数是什么鬼?
$result = array_slice($array, 0, 5, true);
// 新写法:一目了然
$result = array_slice(array: $array, offset: 0, length: 5, preserve_keys: true);
// 在构造函数中特别好用(和 Constructor Promotion 组合)
class PaginatedQuery {
public function __construct(
public readonly string $table,
public readonly int $page = 1,
public readonly int $perPage = 20,
public readonly string $orderBy = 'created_at',
public readonly string $order = 'desc',
) {}
}
// 只传你关心的参数
$query = new PaginatedQuery(
table: 'orders',
perPage: 50,
orderBy: 'total_amount',
);Intersection Types:更精确的接口约束
PHP 8.1 引入交叉类型(Intersection Types),允许一个参数同时满足多个接口约束,不再需要为此创建空的组合接口。
<?php
interface Serializable {
public function serialize(): string;
}
interface Loggable {
public function toLog(): array;
}
interface Cacheable {
public function cacheKey(): string;
public function ttl(): int;
}
// 旧写法(PHP 8.0 以前):必须创建一个组合接口
interface CacheableAndLoggable extends Cacheable, Loggable {}
function saveToCache(CacheableAndLoggable $entity): void {
// ...
}
// 新写法(PHP 8.1+ 交叉类型):直接用 & 组合
function saveToCache(Cacheable&Loggable $entity): void {
$key = $entity->cacheKey();
$ttl = $entity->ttl();
$log = $entity->toLog();
// 存缓存 + 记日志
echo "缓存 {$key},TTL={$ttl}s\n";
}
// 实际类只需实现对应接口即可
class Order implements Cacheable, Loggable, Serializable {
public function __construct(private string $id) {}
public function cacheKey(): string { return "order:{$this->id}"; }
public function ttl(): int { return 300; }
public function toLog(): array { return ['order_id' => $this->id]; }
public function serialize(): string { return json_encode(['id' => $this->id]); }
}
saveToCache(new Order('ORD-001')); // ✅ Order 实现了 Cacheable 和 Loggable⚠️ 踩坑:交叉类型不能和 Union 类型混用(如 A&B|C 会报错),也不能包含 null。返回类型同样支持交叉类型声明。
升级清单与兼容性检查
升级前必须做的几件事:
运行 php-compatibility-checker:
composer require --dev phpcompatibility/php-compatibility,扫描项目中不兼容的写法检查第三方库:
composer require --dev roave/backward-compatibility-check注意废弃函数:PHP 8.x 废弃了
utf8_encode/decode、部分 PCRE 函数,需要替换错误处理变化:PHP 8.0 将很多 Warning 升级为了 TypeError 或 ValueError,需要修复类型不匹配问题
# 检查兼容性(以 PHP 8.2 为目标) ./vendor/bin/phpcs --standard=PHPCompatibility \ --runtime-set testVersion 8.2 \ --extensions=php \ ./src/ # 使用 Rector 自动迁移旧代码 composer require --dev rector/rector ./vendor/bin/rector process src --set-list php82
升级 PHP 8.x 是一次值得投资的技术债偿还。Enum 减少了 30% 的状态相关 bug,只读属性让值对象的设计更加清晰,Fibers 则为将来拥抱异步编程打下基础。建议从 PHP 8.1 开始,把 Enum 和只读属性用起来,这两个特性学习成本最低、收益最高。
发布评论
热门评论区: