在传统PHP项目中,状态管理通常使用常量或字符串,导致类型不安全、可维护性差。PHP 8.1引入的枚举(Enum)和模式匹配(Pattern Matching)彻底改变了这一局面。本文通过构建一个订单状态机,展示如何用枚举定义有限状态,用match表达式处理状态转换逻辑,实现类型安全且易于扩展的代码。
一、为什么需要枚举?
传统状态定义方式:
define('STATUS_PENDING', 'pending');
define('STATUS_PAID', 'paid');
define('STATUS_SHIPPED', 'shipped');
// 或者使用类常量
class OrderStatus {
const PENDING = 'pending';
const PAID = 'paid';
}
问题在于:任何字符串都可以被传入,无法在编译时检查错误。枚举提供了真正的类型安全:
enum OrderStatus: string {
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
枚举值现在是OrderStatus类型,函数参数可以强制类型约束,IDE也能提供自动补全。
二、模式匹配:match表达式的威力
PHP 8.0引入的match表达式比switch更强大:它是表达式(有返回值),支持严格比较,且无需break。
function getStatusLabel(OrderStatus $status): string {
return match ($status) {
OrderStatus::Pending => '待支付',
OrderStatus::Paid => '已付款',
OrderStatus::Shipped => '已发货',
OrderStatus::Delivered => '已送达',
OrderStatus::Cancelled => '已取消',
};
}
如果遗漏某个枚举值,PHP会抛出UnhandledMatchError,强迫开发者处理所有情况。
三、完整案例:订单状态机
我们将实现一个订单状态机,包含:
- 状态定义(枚举)
- 状态转换规则(允许/不允许的转换)
- 状态转换方法(返回新状态或抛出异常)
- 状态相关行为(如发货时发送通知)
1. 定义枚举
<?php
enum OrderStatus: string {
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
// 枚举方法:判断是否允许转换到目标状态
public function canTransitionTo(self $target): bool {
return match ($this) {
self::Pending => in_array($target, [self::Paid, self::Cancelled], true),
self::Paid => in_array($target, [self::Shipped, self::Cancelled], true),
self::Shipped => in_array($target, [self::Delivered], true),
self::Delivered => false, // 终态
self::Cancelled => false, // 终态
};
}
// 获取人类可读标签
public function label(): string {
return match ($this) {
self::Pending => '待支付',
self::Paid => '已付款',
self::Shipped => '已发货',
self::Delivered => '已送达',
self::Cancelled => '已取消',
};
}
}
2. 订单实体
class Order {
public function __construct(
private int $id,
private OrderStatus $status = OrderStatus::Pending,
private ?DateTimeImmutable $paidAt = null,
private ?DateTimeImmutable $shippedAt = null,
) {}
public function getId(): int { return $this->id; }
public function getStatus(): OrderStatus { return $this->status; }
// 状态转换方法
public function transitionTo(OrderStatus $newStatus): void {
if (!$this->status->canTransitionTo($newStatus)) {
throw new InvalidTransitionException(
sprintf(
'不能从 %s 转换到 %s',
$this->status->label(),
$newStatus->label()
)
);
}
// 执行转换前钩子
$this->beforeTransition($newStatus);
// 更新状态
$this->status = $newStatus;
// 执行转换后钩子
$this->afterTransition($newStatus);
}
private function beforeTransition(OrderStatus $newStatus): void {
// 记录日志
echo sprintf(
"订单 %d: 从 %s 转换到 %sn",
$this->id,
$this->status->label(),
$newStatus->label()
);
}
private function afterTransition(OrderStatus $newStatus): void {
match ($newStatus) {
OrderStatus::Paid => $this->onPaid(),
OrderStatus::Shipped => $this->onShipped(),
OrderStatus::Cancelled => $this->onCancelled(),
default => null,
};
}
private function onPaid(): void {
$this->paidAt = new DateTimeImmutable();
echo "订单 {$this->id} 已支付n";
// 发送支付确认邮件等
}
private function onShipped(): void {
$this->shippedAt = new DateTimeImmutable();
echo "订单 {$this->id} 已发货n";
// 调用物流API
}
private function onCancelled(): void {
echo "订单 {$this->id} 已取消n";
// 执行退款逻辑
}
}
3. 自定义异常
class InvalidTransitionException extends RuntimeException {
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
4. 使用示例
// 创建订单
$order = new Order(1001);
echo "初始状态: " . $order->getStatus()->label() . "n"; // 待支付
// 支付
$order->transitionTo(OrderStatus::Paid);
echo "当前状态: " . $order->getStatus()->label() . "n"; // 已付款
// 发货
$order->transitionTo(OrderStatus::Shipped);
echo "当前状态: " . $order->getStatus()->label() . "n"; // 已发货
// 送达
$order->transitionTo(OrderStatus::Delivered);
echo "当前状态: " . $order->getStatus()->label() . "n"; // 已送达
// 尝试取消已送达订单(会抛出异常)
try {
$order->transitionTo(OrderStatus::Cancelled);
} catch (InvalidTransitionException $e) {
echo "错误: " . $e->getMessage() . "n";
}
// 输出:
// 初始状态: 待支付
// 订单 1001: 从 待支付 转换到 已付款
// 订单 1001 已支付
// 当前状态: 已付款
// 订单 1001: 从 已付款 转换到 已发货
// 订单 1001 已发货
// 当前状态: 已发货
// 订单 1001: 从 已发货 转换到 已送达
// 当前状态: 已送达
// 错误: 不能从 已送达 转换到 已取消
四、模式匹配的高级用法
1. 多条件匹配
function getShippingEstimate(OrderStatus $status): string {
return match (true) {
$status === OrderStatus::Pending || $status === OrderStatus::Paid => '待发货',
$status === OrderStatus::Shipped => '运输中',
$status === OrderStatus::Delivered => '已签收',
$status === OrderStatus::Cancelled => '已取消,无物流',
};
}
2. 匹配枚举值并解构
PHP 8.1的枚举不支持构造参数,但我们可以结合match与enum方法实现复杂逻辑:
enum HttpStatus: int {
case Ok = 200;
case NotFound = 404;
case InternalServerError = 500;
public function isError(): bool {
return match ($this) {
self::Ok => false,
self::NotFound, self::InternalServerError => true,
};
}
}
五、枚举的其他实用特性
- 静态方法:枚举可以定义静态方法,如
OrderStatus::cases()返回所有case。 - 接口实现:枚举可以实现接口,适合依赖注入。
- 序列化:
serialize和unserialize原生支持。 - 数据库映射:结合Eloquent或Doctrine,枚举字段可以自动映射。
// 枚举实现接口
interface Labelable {
public function label(): string;
}
enum OrderStatus: string implements Labelable {
// ...
}
六、测试状态机
使用PHPUnit编写测试:
class OrderStatusTest extends PHPUnitFrameworkTestCase {
public function testValidTransitions(): void {
$order = new Order(1);
$this->assertEquals(OrderStatus::Pending, $order->getStatus());
$order->transitionTo(OrderStatus::Paid);
$this->assertEquals(OrderStatus::Paid, $order->getStatus());
$order->transitionTo(OrderStatus::Shipped);
$this->assertEquals(OrderStatus::Shipped, $order->getStatus());
}
public function testInvalidTransitionThrowsException(): void {
$this->expectException(InvalidTransitionException::class);
$order = new Order(2);
$order->transitionTo(OrderStatus::Delivered); // 不能从Pending直接到Delivered
}
}
七、常见陷阱与最佳实践
- 避免在枚举中存储动态数据:枚举case是单例,不应包含可变状态。
- 使用backed枚举(有值枚举)与数据库交互:
enum Status: string方便存储和读取。 - match表达式必须穷举:如果枚举新增case,match会抛出
UnhandledMatchError,强迫更新代码。 - 状态转换逻辑集中管理:将
canTransitionTo放在枚举中,避免散落在各处。
八、总结
通过订单状态机案例,我们看到了PHP枚举和模式匹配如何提升代码的类型安全性与可读性。枚举替代了魔术字符串,match替代了冗长的if-else或switch。结合领域驱动设计思想,可以构建出健壮、自文档化的业务逻辑。
现在,开始在你的项目中用枚举取代常量,用match替代switch吧——你的代码会感谢你。
本文为原创技术教程,代码基于PHP 8.1测试通过。建议在实际项目中结合PHPStan或Psalm进行静态分析。

