/ PHP  PHP8  Enum  Fibers  只读类  新特性  后端开发  类型系统 

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 property

PHP 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-checkercomposer 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 和只读属性用起来,这两个特性学习成本最低、收益最高。

发布评论

热门评论区: