PHP 8 新特性
这一课你将能做到
- 用枚举(enum)替代魔法字符串/类常量,给状态字段立起类型护栏
- 用只读属性、命名参数、match、nullsafe 运算符写出紧凑且不可变的现代代码
- 说清 Fiber 协程与线程的区别、JIT 与 OPcache 的关系,并能动手开启 JIT
为什么要学
PHP 8.0(2020)、8.1(2021)、8.2(2022)三连击彻底改变了这门语言的气质:构造器属性提升、枚举、纤程、只读类——把 Laravel 9+、Symfony 6+ 的源码风格推向了「像 Java 又比 Java 省事」。面试官问 PHP 现状,考的就是这些特性;读新框架源码,满屏都是 enum、readonly、fn。停留在 PHP 5 时代的认知,等于看不懂现代 PHP。
人话:这一课是「补差价」——把前几课刻意没展开的高级语法一次讲透,让你写的 PHP 从「能跑」升级到「现代」。
先建立图像
老写法 vs 新写法的直观对比:
// 2010 年代的 PHP // PHP 8.2
class Order { class Order {
const STATUS_PENDING = 1; public function __construct(
public function __construct( public readonly int $id,
private int $status = public readonly OrderStatus $status,
self::STATUS_PENDING ) {}
) {}
} }
enum OrderStatus: string {
case Pending = 'pending';
case Paid = 'paid';
}
魔法值从「常量约定」变成「类型约束」:传错状态直接 TypeError,IDE 自动补全所有合法值。
跟着做
步骤 1:枚举——带方法的类型安全常量
<?php
declare(strict_types=1);
enum OrderStatus: string // backed enum:每个 case 绑定一个标量值
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
public function canCancel(): bool // 枚举可以有方法
{
return in_array($this, [self::Pending, self::Paid]);
}
public static function fromDb(?string $v): self // 数据库值 → 枚举
{
return self::tryFrom($v ?? '') ?? self::Pending;
}
}
$status = OrderStatus::Paid;
echo $status->value, PHP_EOL; // paid(取出绑定值)
var_dump($status->canCancel()); // bool(true)
var_dump(OrderStatus::tryFrom('x')); // NULL(非法值不炸)
var_dump(OrderStatus::from('x')); // 抛 ValueError
// 枚举是天然的合法值清单
foreach (OrderStatus::cases() as $case) {
echo $case->name, ' => ', $case->value, PHP_EOL;
}
函数签名写 OrderStatus $status 后,魔法字符串 'paid' 从参数层就绝迹了。
步骤 2:只读属性与只读类
<?php
declare(strict_types=1);
// 8.1:属性级 readonly
class Money
{
public function __construct(
public readonly int $amount, // 初始化后不可修改
public readonly string $currency,
) {}
}
$m = new Money(100, 'CNY');
// $m->amount = 200; // Error: cannot modify readonly property
// 8.2:类级 readonly,所有声明属性自动只读
readonly class Point
{
public function __construct(
public float $x = 0.0,
public float $y = 0.0,
) {}
}
$p = new Point(1.5, 2.5);
echo $p->x + $p->y, PHP_EOL; // 4
readonly 对象是不可变数据的载体——传参传出去,调用方改不了内部状态,天然线程安全(对 Fiber 也是)。
步骤 3:命名参数、match、nullsafe 组合拳
<?php
declare(strict_types=1);
class Customer
{
public function __construct(
private ?Address $address = null, // 可空属性
) {}
public function address(): ?Address { return $this->address; }
}
class Address
{
public function __construct(public string $city) {}
}
function shipTo(string $city, bool $express = false, string $note = ''): string {
$mode = match (true) { // match 表达式有返回值
$express => '特快',
$note !== '' => '备注件',
default => '普通',
};
return "配送至 {$city}({$mode})";
}
$c = new Customer(new Address('杭州'));
// nullsafe 链:中间为 null 整条表达式短路为 null,不再抛错
echo shipTo(city: $c?->address()?->city ?? '未知城市', express: true), PHP_EOL;
// 配送至 杭州(特快)
$empty = new Customer();
var_dump($empty?->address()?->city); // NULL,不抛 Error
命名参数让布尔参数自解释(express: true 一眼懂);?-> 消灭了层层 if ($x !== null)。
步骤 4:Fiber 协程
<?php
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('第一步挂起'); // 让出控制权,返回给 resume
echo "恢复,收到:{$value}\n";
Fiber::suspend('第二步挂起');
});
$first = $fiber->start(); // 跑到第一个 suspend
echo $first, PHP_EOL; // 第一步挂起
echo $fiber->resume('参数回传'), PHP_EOL; // 恢复并传值,再挂起
// 恢复,收到:参数回传 / 第二步挂起
$fiber->resume(); // 跑完函数体
var_dump($fiber->isTerminated()); // bool(true)
Fiber 是「可暂停/恢复的函数」:在单个线程内做协作式调度,不涉及并行,没有锁竞争——它是 Swoole、Revolt、amphp 等异步框架的地基,普通 Web 业务代码不直接写 Fiber。
步骤 5:开启 JIT
php -i | findstr jit # 查看当前 JIT 状态(Windows)
php -i | grep jit # macOS/Linux
php.ini 配置:
opcache.enable=1
opcache.jit_buffer_size=64M # JIT 代码缓存区,0 表示关闭
opcache.jit=tracing # tracing 模式:默认推荐
重启后 php -r "var_dump(opcache_get_status()['jit']['enabled']);" 输出 bool(true)。CPU 密集型脚本(图像处理、数学运算)收益明显;典型 Web 业务瓶颈在 IO(数据库/网络),JIT 提升感知有限。
讲清楚要点
- 枚举分纯枚举与 backed 枚举:纯枚举 case 无值(
enum Suit { case Hearts; });backed 枚举绑定 int/string(一个枚举只能选一种类型),提供from/tryFrom与->value。枚举可实现接口、可含方法与常量,但不能有属性、不能实例化、天然单例。 - readonly 三条铁律:只能在声明处或构造器内赋值;作用域内赋值次数只有一次;不可变的是「引用」(对象内部若持有可变对象,其内部状态仍可变)。8.2 的 readonly class 里所有声明属性自动只读,但不能有无类型的属性。
- Fiber ≠ 线程:Fiber 是单线程内的协作式多任务,调度权由代码显式
suspend/resume,不存在抢占与锁;线程是内核级并行,有真实并发开销。PHP 的并发执行本质仍是 FPM 多进程,Fiber 解决的是 IO 等待期间的「闲置复用」。 - JIT 建立在 OPcache 之上:OPcache 缓存 opcode 省编译,JIT 进一步把热点 opcode 编译成机器码。两层都要先开
opcache.enable=1;Web 场景优先保证 OPcache,JIT 属于锦上添花。
你可能会懵的地方
- 枚举和「类常量 + 类型提示」比好在哪?
类常量是 int/string,参数提示只能写标量,非法值运行时才发现;枚举是独立类型,非法值在调用瞬间 TypeError,IDE 补全、静态分析、
match穷尽性检查全覆盖。数据库存的是 backed 值,取出来tryFrom还原即可。 - readonly 和 const 是一回事吗?
不是。const 属于类/文件级别、编译期确定;readonly 是实例属性,每个对象有自己的值,运行时构造器里赋值。
readonly让「每个实例不同但创建后不可改」成为可能——Java 里对应的是 final 字段。 - 既然有 Fiber,PHP 能像 Go 一样高并发吗? 有限度。Fiber 提供了用户态调度原语,配合 Revolt/amphp/Swoole 事件循环可在单进程内并发数万 IO 任务;但 CPU 并行仍靠多进程,生态与 Go 的 goroutine 运行时不在一个量级。Web 业务的主流形态仍是 FPM + 水平扩展。
- match 和 switch 除了语法还有什么区别?
match是表达式有返回值、===严格比较、不隐式穿透、无匹配抛UnhandledMatchError;switch语句无返回值、==宽松比较、忘 break 会穿透。语义上 match 更接近 Java 的 switch 表达式。 - 8.2 废弃动态属性是什么意思?
给未声明的属性赋值(
$obj->foo = 1而 foo 未在类中声明)触发弃用警告,8.3 起报错。这是 PHP 向强纪律演进的信号:动态行为请显式用#[\AllowDynamicProperties]或数组/集合。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
用字符串 'paid' 在各层传递状态 | 拼写错误无提示,比对脆弱 | backed enum + 类型提示 |
OrderStatus::from($input) 接用户输入 | 非法值直接 ValueError | tryFrom + 默认值兜底 |
| 给 readonly 属性提供 setter | 等于放弃不可变性 | 需要变更时新建实例(with 模式) |
| 认为开 JIT 就变快一倍 | Web 瓶颈在 IO,JIT 收益有限 | 先开 OPcache,压测后再决定 JIT |
| 在 Fiber 里做 CPU 死循环 | 单线程内阻塞所有协程 | CPU 任务拆分或交给队列/多进程 |
| 匿名类/闭包里随便挂动态属性 | 8.2 弃用警告 | 显式声明属性或改用数组 |
卡点急救
Call to undefined function Fiber\fiber之类:版本不够。php -v确认 ≥8.1;Windows 集成环境注意选 PHP 8.2 构建。- JIT 配了不生效:检查
opcache.enable_cli(CLI 需单独开)、jit_buffer_size > 0、opcache.jit不是disable;用opcache_get_status()['jit']验证。 - 枚举序列化/落库报错:枚举不能直接 json_encode 成值(要
->value)、不能隐式转字符串(实现Stringable或用->name)。Eloquent 的 cast 配置可自动双向转换。
面试常问
- PHP 枚举相比类常量解决什么问题?
类型安全与行为内聚:枚举是可作参数/返回值类型的独立类型,非法值调用即报错;case 可绑定标量值(backed enum)方便落库,可携带方法表达状态行为;
tryFrom提供安全转换,cases()提供合法值全集,配合 match 实现穷尽性分支。 - Fiber 与线程/进程的区别? Fiber 是用户态协作式协程:单线程内由代码显式挂起/恢复,无抢占、无锁、切换成本纳秒级;线程内核调度、可并行、需同步原语;进程隔离最强但开销最大。Fiber 让 PHP 在不改变多进程模型的前提下具备 IO 并发复用能力。
- JIT 与 OPcache 的关系?PHP 开 JIT 收益如何? OPcache 缓存编译后的 opcode 避免重复解析;JIT 是 OPcache 的可选组件,把热点 opcode 进一步编译为机器码。CPU 密集型负载(数学计算、渲染)可显著提升,典型 Web 业务瓶颈在数据库与网络 IO,JIT 提升通常个位数百分比——先保证 OPcache,再按压测数据决定 JIT。
自测三问
- backed enum 的
from()与tryFrom()失败时行为有什么不同? - readonly 类的属性还能在哪些位置赋值?外部 setter 会发生什么?
- 为什么说「开 JIT 之前先开 OPcache」?两者分别省掉哪部分开销?
小结一句话
PHP 8 用枚举立类型、readonly 立不变性、match/nullsafe 立简洁、Fiber 立并发原语、JIT 立性能上限——语言形态已与 Java 同代,写法却更省。
延伸
- PHP 官方手册:枚举
- 下一课:Docker 运行 PHP——php-fpm + nginx 容器化实战。