PHP枚举与模式匹配实战:构建类型安全的订单状态机

2026-05-13 0 513

在传统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的枚举不支持构造参数,但我们可以结合matchenum方法实现复杂逻辑:

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。
  • 接口实现:枚举可以实现接口,适合依赖注入。
  • 序列化serializeunserialize原生支持。
  • 数据库映射:结合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进行静态分析。

PHP枚举与模式匹配实战:构建类型安全的订单状态机
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 php PHP枚举与模式匹配实战:构建类型安全的订单状态机 https://www.taomawang.com/server/php/1792.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务