PHP 8.1 Enums与Match表达式结合:构建类型安全且简洁的业务状态判断逻辑

PHP 8.1 Enums 与 Match 表达式:构建类型安全且简洁的业务状态判断逻辑

大家好,今天我们来聊聊 PHP 8.1 中 Enum(枚举)类型与 Match 表达式的结合使用。这两种特性的组合,可以帮助我们构建类型安全、可读性强、且维护成本更低的业务状态判断逻辑。在传统的 PHP 开发中,我们经常使用字符串常量或整数常量来表示业务状态,这样做存在诸多问题,例如类型错误难以发现,代码可读性差,以及维护困难。而 Enum 和 Match 表达式的引入,为我们提供了一种更优雅、更强大的解决方案。

一、传统业务状态判断的痛点

在没有 Enum 之前,我们通常会这样定义业务状态:

<?php

const ORDER_STATUS_PENDING = 1;
const ORDER_STATUS_PROCESSING = 2;
const ORDER_STATUS_SHIPPED = 3;
const ORDER_STATUS_DELIVERED = 4;
const ORDER_STATUS_CANCELLED = 5;

function processOrder(int $status) {
    switch ($status) {
        case ORDER_STATUS_PENDING:
            echo "订单待处理n";
            break;
        case ORDER_STATUS_PROCESSING:
            echo "订单处理中n";
            break;
        case ORDER_STATUS_SHIPPED:
            echo "订单已发货n";
            break;
        case ORDER_STATUS_DELIVERED:
            echo "订单已送达n";
            break;
        case ORDER_STATUS_CANCELLED:
            echo "订单已取消n";
            break;
        default:
            echo "未知订单状态n";
    }
}

processOrder(ORDER_STATUS_PROCESSING); // 输出: 订单处理中
processOrder(6); // 输出: 未知订单状态

或者使用字符串常量:

<?php

const ORDER_STATUS_PENDING = 'pending';
const ORDER_STATUS_PROCESSING = 'processing';
const ORDER_STATUS_SHIPPED = 'shipped';
const ORDER_STATUS_DELIVERED = 'delivered';
const ORDER_STATUS_CANCELLED = 'cancelled';

function processOrder(string $status) {
    switch ($status) {
        case ORDER_STATUS_PENDING:
            echo "订单待处理n";
            break;
        case ORDER_STATUS_PROCESSING:
            echo "订单处理中n";
            break;
        case ORDER_STATUS_SHIPPED:
            echo "订单已发货n";
            break;
        case ORDER_STATUS_DELIVERED:
            echo "订单已送达n";
            break;
        case ORDER_STATUS_CANCELLED:
            echo "订单已取消n";
            break;
        default:
            echo "未知订单状态n";
    }
}

processOrder(ORDER_STATUS_PROCESSING); // 输出: 订单处理中
processOrder('processing'); // 输出: 订单处理中
processOrder('wrong_status'); // 输出: 未知订单状态

这种方式存在以下问题:

  • 类型安全问题: 函数参数类型约束只能保证传入的是 int 或 string,但无法保证传入的是有效的状态值。例如,processOrder(6) 依然能够执行,只是进入了 default 分支。
  • 可读性差: 常量名称和值之间存在关联,但这种关联是隐式的,需要查阅常量定义才能理解其含义。
  • 维护困难: 如果需要新增或修改状态,需要在多个地方进行修改,容易出错。常量名称容易冲突。
  • 字符串常量容易产生拼写错误: 即使类型检查正确,拼写错误也可能导致逻辑错误。

二、Enum 的优势:类型安全和代码可读性

PHP 8.1 引入的 Enum 类型,允许我们定义一组具名的、类型安全的常量。Enum 本身也是一种类型,可以作为函数参数的类型约束,从而保证传入的值必须是 Enum 中定义的成员之一。

我们使用 Enum 来改写上面的例子:

<?php

enum OrderStatus {
    case Pending;
    case Processing;
    case Shipped;
    case Delivered;
    case Cancelled;
}

function processOrder(OrderStatus $status) {
    switch ($status) {
        case OrderStatus::Pending:
            echo "订单待处理n";
            break;
        case OrderStatus::Processing:
            echo "订单处理中n";
            break;
        case OrderStatus::Shipped:
            echo "订单已发货n";
            break;
        case OrderStatus::Delivered:
            echo "订单已送达n";
            break;
        case OrderStatus::Cancelled:
            echo "订单已取消n";
            break;
        default:
            echo "未知订单状态n"; // 理论上永远不会执行到这里
    }
}

processOrder(OrderStatus::Processing); // 输出: 订单处理中
// processOrder(2); //  Fatal error: Uncaught TypeError: processOrder(): Argument #1 ($status) must be of type OrderStatus, int given
// processOrder('processing'); // Fatal error: Uncaught TypeError: processOrder(): Argument #1 ($status) must be of type OrderStatus, string given

Enum 的优势:

  • 类型安全: 函数参数类型约束为 OrderStatus,只能传入 OrderStatus 枚举的成员,其他类型的值会导致 TypeError 错误。
  • 代码可读性: OrderStatus::ProcessingORDER_STATUS_PROCESSING 更具可读性,直接表达了其含义。
  • 减少错误: 避免了使用错误的常量值或拼写错误的字符串。
  • IDE 支持: IDE 可以提供 Enum 成员的自动补全和类型检查。

三、Match 表达式:简洁的状态判断

PHP 8.0 引入的 Match 表达式,是一种比 Switch 语句更简洁、更强大的条件判断结构。它可以直接返回匹配的值,并且只进行严格比较 (===)。结合 Enum 使用 Match 表达式,可以进一步简化状态判断逻辑。

<?php

enum OrderStatus {
    case Pending;
    case Processing;
    case Shipped;
    case Delivered;
    case Cancelled;
}

function processOrder(OrderStatus $status): string {
    return match ($status) {
        OrderStatus::Pending => "订单待处理",
        OrderStatus::Processing => "订单处理中",
        OrderStatus::Shipped => "订单已发货",
        OrderStatus::Delivered => "订单已送达",
        OrderStatus::Cancelled => "订单已取消",
    };
}

echo processOrder(OrderStatus::Processing); // 输出: 订单处理中

Match 表达式的优势:

  • 简洁性: 代码更简洁,减少了冗余的 break 语句。
  • 返回值: 直接返回匹配的值,避免了在每个 case 中使用 echoreturn
  • 严格比较: 使用严格比较 (===),避免了类型转换带来的潜在问题。
  • 强制完整性: 如果 Match 表达式没有覆盖所有可能的值,会抛出 UnhandledMatchError 异常,这有助于我们及时发现遗漏的状态。 (需要注意的是,如果 match 表达式可以推断出输入是 exhaustive 的,且的确是 exhaustive 的,那么就不会抛出异常,即使定义了 default 分支。 只有当 match 表达式无法推断出输入是 exhaustive 的,并且的确存在未匹配的输入时,才会抛出异常。)

四、Enum 与 Match 表达式的进阶用法

  1. 带有关联值的 Enum (Backed Enums)

    PHP 8.1 支持带有关联值的 Enum,也称为 Backed Enums。我们可以为每个 Enum 成员指定一个关联值,例如:

    <?php
    
    enum OrderStatus: int {
        case Pending = 1;
        case Processing = 2;
        case Shipped = 3;
        case Delivered = 4;
        case Cancelled = 5;
    
        public function label(): string {
            return match ($this) {
                OrderStatus::Pending => '待处理',
                OrderStatus::Processing => '处理中',
                OrderStatus::Shipped => '已发货',
                OrderStatus::Delivered => '已送达',
                OrderStatus::Cancelled => '已取消',
            };
        }
    }
    
    echo OrderStatus::Processing->value; // 输出: 2
    echo OrderStatus::Processing->label(); // 输出: 处理中

    Backed Enums 必须声明关联值的类型(例如 intstring),并且每个成员必须指定一个唯一的关联值。 可以通过 ->value 访问关联值。

    使用 String-backed Enum:

    <?php
    
    enum UserType: string {
        case Admin = 'admin';
        case Editor = 'editor';
        case Viewer = 'viewer';
    
        public function accessLevel(): int {
            return match ($this) {
                UserType::Admin => 3,
                UserType::Editor => 2,
                UserType::Viewer => 1,
            };
        }
    }
    
    echo UserType::Editor->value; // 输出: editor
    echo UserType::Editor->accessLevel(); // 输出: 2
    
  2. 带有方法的 Enum

    Enum 可以定义方法,用于封装与状态相关的逻辑。例如,我们可以定义一个方法来判断订单是否可以取消:

    <?php
    
    enum OrderStatus: int {
        case Pending = 1;
        case Processing = 2;
        case Shipped = 3;
        case Delivered = 4;
        case Cancelled = 5;
    
        public function canCancel(): bool {
            return match ($this) {
                OrderStatus::Pending, OrderStatus::Processing => true,
                default => false,
            };
        }
    }
    
    echo OrderStatus::Pending->canCancel() ? '可以取消' : '不能取消'; // 输出: 可以取消
    echo OrderStatus::Shipped->canCancel() ? '可以取消' : '不能取消'; // 输出: 不能取消

    在方法中使用 Match 表达式,可以更清晰地表达状态判断逻辑。

  3. 使用 Match 表达式进行状态转换

    我们可以使用 Match 表达式进行状态转换。例如,将订单状态从 Pending 转换为 Processing

    <?php
    
    enum OrderStatus: int {
        case Pending = 1;
        case Processing = 2;
        case Shipped = 3;
        case Delivered = 4;
        case Cancelled = 5;
    
        public function transition(): OrderStatus {
            return match ($this) {
                OrderStatus::Pending => OrderStatus::Processing,
                default => $this, // 其他状态不做转换
            };
        }
    }
    
    $status = OrderStatus::Pending;
    $newStatus = $status->transition();
    echo $newStatus === OrderStatus::Processing ? '转换成功' : '转换失败'; // 输出: 转换成功
  4. 在 Doctrine ORM 中使用 Enum

    Doctrine ORM 提供了对 Enum 的支持。我们可以将 Enum 类型映射到数据库字段,从而实现类型安全的数据库操作。需要注意的是,Doctrine 默认只支持 Backed Enums。

    首先,定义一个 Enum 类型:

    <?php
    
    namespace AppEnum;
    
    enum OrderStatus: string {
        case Pending = 'pending';
        case Processing = 'processing';
        case Shipped = 'shipped';
        case Delivered = 'delivered';
        case Cancelled = 'cancelled';
    }

    然后,在 Doctrine 配置文件中注册 Enum 类型:

    # config/packages/doctrine.yaml
    doctrine:
        dbal:
            types:
                order_status: AppDBALTypesOrderStatusType

    创建一个 Doctrine 类型类(OrderStatusType):

    <?php
    
    namespace AppDBALTypes;
    
    use AppEnumOrderStatus;
    use DoctrineDBALPlatformsAbstractPlatform;
    use DoctrineDBALTypesType;
    
    class OrderStatusType extends Type
    {
        public const NAME = 'order_status';
    
        public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
        {
            return 'VARCHAR(255)';
        }
    
        public function convertToPHPValue($value, AbstractPlatform $platform): ?OrderStatus
        {
            return $value === null ? null : OrderStatus::from($value);
        }
    
        public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
        {
            return $value instanceof OrderStatus ? $value->value : null;
        }
    
        public function getName(): string
        {
            return self::NAME;
        }
    
        public function requiresSQLCommentHint(AbstractPlatform $platform): bool
        {
            return true;
        }
    }
    

    最后,在 Entity 中使用 Enum 类型:

    <?php
    
    namespace AppEntity;
    
    use DoctrineORMMapping as ORM;
    use AppEnumOrderStatus;
    
    #[ORMEntity]
    class Order
    {
        #[ORMId]
        #[ORMGeneratedValue]
        #[ORMColumn(type: 'integer')]
        private int $id;
    
        #[ORMColumn(type: 'order_status')]
        private OrderStatus $status;
    
        public function getId(): int
        {
            return $this->id;
        }
    
        public function getStatus(): OrderStatus
        {
            return $this->status;
        }
    
        public function setStatus(OrderStatus $status): void
        {
            $this->status = $status;
        }
    }
    

    这样,就可以在 Doctrine 中使用 Enum 类型进行数据库操作了。

五、使用场景示例

  • 用户角色权限管理: 使用 Enum 定义用户角色(Admin, Editor, Viewer),并使用 Match 表达式判断用户是否具有特定权限。
  • 订单状态流转: 使用 Enum 定义订单状态(Pending, Processing, Shipped, Delivered, Cancelled),并使用 Match 表达式处理不同状态下的业务逻辑。
  • 支付方式选择: 使用 Enum 定义支付方式(CreditCard, PayPal, Alipay),并使用 Match 表达式处理不同支付方式的支付流程。
  • 配置项管理: 虽然配置项通常存储在配置文件中,但对于一些需要在代码中进行类型检查的配置项,可以使用 Enum 来定义。例如,定义日志级别(Debug, Info, Warning, Error, Critical)。

六、使用表格对比 Enum 和传统方式

特性 Enum 传统方式(常量)
类型安全 强类型,编译时检查 弱类型,运行时检查或无检查
代码可读性 具名常量,语义清晰 常量名称与值关联性弱,可读性差
维护性 修改 Enum 定义,影响范围可控 需要在多个地方修改,容易出错
IDE 支持 自动补全,类型检查 有限支持
防御性编程 强制完整性,避免遗漏状态 需要手动检查,容易遗漏
性能 略有优势(取决于具体实现和使用方式) 通常更快(但类型安全和可维护性降低)

七、Enum的限制

  • 不支持继承: Enum 不支持继承,这意味着无法通过继承来扩展 Enum。
  • 必须是常量: Enum 成员必须是常量,不能是变量或表达式。
  • 无法直接序列化/反序列化: 需要自定义序列化和反序列化逻辑。

八、总结:类型安全,代码清晰,易于维护

Enum 和 Match 表达式的结合使用,为我们提供了一种更加类型安全、可读性更强、且维护成本更低的业务状态判断方案。虽然 Enum 存在一些限制,但在大多数场景下,它都是一个比传统方式更好的选择。通过合理使用 Enum 和 Match 表达式,我们可以构建更加健壮、可靠的 PHP 应用程序。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注