CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
PHP Web 入门/课时/PHP 8 新特性
RawBlame
课时目录12 节

PHP Web 入门

  1. 01PHP 是什么
  2. 02基础语法
  3. 03函数与命名空间
  4. 04面向对象
  5. 05Composer 依赖
  6. 06Web 请求处理
  7. 07PDO 与 MySQL
  8. 08REST API 风格
  9. 09PHP 8 新特性
  10. 10Docker 运行 PHP
  11. 11PHP vs Java 选型
  12. 12综合练习

PHP 8 新特性

属性、联合类型、readonly。

25 分钟

PHP 8 新特性

这一课你将能做到

  1. 用枚举(enum)替代魔法字符串/类常量,给状态字段立起类型护栏
  2. 用只读属性、命名参数、match、nullsafe 运算符写出紧凑且不可变的现代代码
  3. 说清 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 提升感知有限。

讲清楚要点

  1. 枚举分纯枚举与 backed 枚举:纯枚举 case 无值(enum Suit { case Hearts; });backed 枚举绑定 int/string(一个枚举只能选一种类型),提供 from/tryFrom 与 ->value。枚举可实现接口、可含方法与常量,但不能有属性、不能实例化、天然单例。
  2. readonly 三条铁律:只能在声明处或构造器内赋值;作用域内赋值次数只有一次;不可变的是「引用」(对象内部若持有可变对象,其内部状态仍可变)。8.2 的 readonly class 里所有声明属性自动只读,但不能有无类型的属性。
  3. Fiber ≠ 线程:Fiber 是单线程内的协作式多任务,调度权由代码显式 suspend/resume,不存在抢占与锁;线程是内核级并行,有真实并发开销。PHP 的并发执行本质仍是 FPM 多进程,Fiber 解决的是 IO 等待期间的「闲置复用」。
  4. 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) 接用户输入非法值直接 ValueErrortryFrom + 默认值兜底
给 readonly 属性提供 setter等于放弃不可变性需要变更时新建实例(with 模式)
认为开 JIT 就变快一倍Web 瓶颈在 IO,JIT 收益有限先开 OPcache,压测后再决定 JIT
在 Fiber 里做 CPU 死循环单线程内阻塞所有协程CPU 任务拆分或交给队列/多进程
匿名类/闭包里随便挂动态属性8.2 弃用警告显式声明属性或改用数组

卡点急救

  1. Call to undefined function Fiber\fiber 之类:版本不够。php -v 确认 ≥8.1;Windows 集成环境注意选 PHP 8.2 构建。
  2. JIT 配了不生效:检查 opcache.enable_cli(CLI 需单独开)、jit_buffer_size > 0、opcache.jit 不是 disable;用 opcache_get_status()['jit'] 验证。
  3. 枚举序列化/落库报错:枚举不能直接 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。

自测三问

  1. backed enum 的 from() 与 tryFrom() 失败时行为有什么不同?
  2. readonly 类的属性还能在哪些位置赋值?外部 setter 会发生什么?
  3. 为什么说「开 JIT 之前先开 OPcache」?两者分别省掉哪部分开销?

小结一句话

PHP 8 用枚举立类型、readonly 立不变性、match/nullsafe 立简洁、Fiber 立并发原语、JIT 立性能上限——语言形态已与 Java 同代,写法却更省。

延伸

  • PHP 官方手册:枚举
  • 下一课:Docker 运行 PHP——php-fpm + nginx 容器化实战。
加载本节练习…
← 上一课REST API 风格下一课 →Docker 运行 PHP