Skip to content

UnitEnum / BackedEnum 接口

概述

PHP 8.1 引入了枚举(Enum)功能,并提供了两个内置接口来支持枚举的反射和操作:

  • UnitEnum — 所有枚举类型的公共父接口,提供 cases() 静态方法获取所有枚举值
  • BackedEnum — 带值枚举(enum X: type)的接口,提供 from()tryFrom() 方法进行值到枚举的转换
php
<?php

declare(strict_types=1);

// UnitEnum 接口
// interface UnitEnum
// {
//     public static function cases(): array;
// }

// BackedEnum 接口
// interface BackedEnum extends UnitEnum
// {
//     public static function from(int|string $value): static;
//     public static function tryFrom(int|string $value): ?static;
// }

版本说明

枚举功能在 PHP 8.1 中引入。所有枚举类型自动实现 UnitEnum。带值枚举(enum Status: string)额外实现 BackedEnum。不能被用户类直接实现这两个接口。

基础概念

枚举的基本形式

php
<?php

declare(strict_types=1);

// 纯枚举(Unit Enum)— 自动实现 UnitEnum
enum Status
{
    case Active;
    case Inactive;
    case Pending;
}

// 带值枚举(Backed Enum)— 自动实现 BackedEnum(继承 UnitEnum)
enum UserRole: string
{
    case Admin = 'admin';
    case Editor = 'editor';
    case Viewer = 'viewer';
}

enum Priority: int
{
    case Low = 1;
    case Medium = 2;
    case High = 3;
    case Critical = 4;
}

枚举值的特性

  • 枚举值是单例对象(每个 case 全局唯一)
  • 枚举值可以拥有方法
  • 枚举值可以有属性(PHP 8.2+)
  • 枚举不能被实例化(除了 case 本身)

语法与代码

UnitEnum::cases()

php
<?php

declare(strict_types=1);

enum HttpMethod: string
{
    case Get = 'GET';
    case Post = 'POST';
    case Put = 'PUT';
    case Delete = 'DELETE';
    case Patch = 'PATCH';
}

// cases() 返回所有枚举值的数组
$methods = HttpMethod::cases();

foreach ($methods as $method) {
    echo "{$method->name} = {$method->value}\n";
    // Get = GET
    // Post = POST
    // ...
}

// cases() 返回类型:array<int, static>
// 键是枚举定义顺序的数字索引
echo $methods[0]->name;  // Get

// 获取枚举值名称列表
$names = array_map(fn (HttpMethod $m): string => $m->name, HttpMethod::cases());
// ['Get', 'Post', 'Put', 'Delete', 'Patch']

// 获取枚举值的值列表(仅 BackedEnum)
$values = array_map(fn (HttpMethod $m): string => $m->value, HttpMethod::cases());
// ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']

BackedEnum::from() 和 tryFrom()

php
<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Created = 'created';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';
}

// from() — 值转枚举(不存在则抛出 ValueError)
$status = OrderStatus::from('paid');
echo $status->name;    // Paid
echo $status->value;   // paid

// tryFrom() — 值转枚举(不存在返回 null)
$status = OrderStatus::tryFrom('expired');
var_dump($status);     // null

// tryFrom 的典型用法
$inputStatus = $_GET['status'] ?? '';
$orderStatus = OrderStatus::tryFrom($inputStatus);

if ($orderStatus === null) {
    throw new InvalidArgumentException("无效的状态值: {$inputStatus}");
}

// from() 在确认值有效时使用
function updateOrder(int $orderId, string $statusValue): void
{
    // 数据库中存储的是合法枚举值,可以安全使用 from()
    $status = OrderStatus::from($statusValue);
    echo "订单 {$orderId} 状态更新为 {$status->name}";
}

详细说明

枚举方法

php
<?php

declare(strict_types=1);

enum Color: string
{
    case Red = '#FF0000';
    case Green = '#00FF00';
    case Blue = '#0000FF';

    // 枚举可以定义方法
    public function rgb(): array
    {
        return [
            hexdec(substr($this->value, 1, 2)),
            hexdec(substr($this->value, 3, 2)),
            hexdec(substr($this->value, 5, 2)),
        ];
    }

    public function isWarm(): bool
    {
        return in_array($this, [self::Red, self::Green]);
    }

    // 静态方法作为工厂
    public static function fromRgb(int $r, int $g, int $b): self
    {
        $hex = sprintf('#%02X%02X%02X', $r, $g, $b);
        return self::from($hex);
    }
}

$red = Color::Red;
echo $red->rgb()[0];  // 255
echo $red->isWarm() ? '暖色' : '冷色';  // 暖色

枚举实现接口

php
<?php

declare(strict_types=1);

interface Describable
{
    public function description(): string;
}

enum LogLevel: int implements Describable
{
    case Debug = 0;
    case Info = 1;
    case Warning = 2;
    case Error = 3;
    case Critical = 4;

    public function description(): string
    {
        return match ($this) {
            self::Debug     => '调试信息',
            self::Info      => '一般信息',
            self::Warning   => '警告信息',
            self::Error     => '错误信息',
            self::Critical  => '严重错误',
        };
    }

    public function isSevere(): bool
    {
        return $this->value >= self::Error->value;
    }
}

$level = LogLevel::from(3);
echo $level->description();  // 错误信息
echo $level->isSevere() ? '严重' : '一般';  // 严重

traits 在枚举中的使用

php
<?php

declare(strict_types=1);

trait HasLabel
{
    abstract public function label(): string;
}

enum PaymentMethod: string
{
    use HasLabel;

    case CreditCard = 'credit_card';
    case Alipay = 'alipay';
    case WechatPay = 'wechat_pay';
    case BankTransfer = 'bank_transfer';

    public function label(): string
    {
        return match ($this) {
            self::CreditCard    => '信用卡',
            self::Alipay       => '支付宝',
            self::WechatPay    => '微信支付',
            self::BankTransfer => '银行转账',
        };
    }

    public function feeRate(): float
    {
        return match ($this) {
            self::CreditCard    => 0.006,
            self::Alipay       => 0.001,
            self::WechatPay    => 0.001,
            self::BankTransfer => 0.005,
        };
    }
}

$method = PaymentMethod::tryFrom('alipay');
echo $method?->label();       // 支付宝
echo $method?->feeRate();     // 0.001

枚举属性(PHP 8.2+)

php
<?php

declare(strict_types=1);

// PHP 8.2+ 支持枚举常量属性
enum FeatureFlag
{
    case DarkMode;
    case NewDashboard;
    case BetaAPI;

    // PHP 8.2+ 枚举属性
    #[\Attribute]
    public const IS_DEFAULT = [
        DarkMode::class => true,
    ];
}

// PHP 8.2+ 的枚举属性(用常量表达元数据)
enum ApiVersion: string
{
    case V1 = 'v1';
    case V2 = 'v2';
    case V3 = 'v3';

    public const DEPRECATED = [
        self::V1,
    ];

    public function isDeprecated(): bool
    {
        return in_array($this, self::DEPRECATED, true);
    }
}

实战示例

枚举在状态机中的应用

php
<?php

declare(strict_types=1);

enum OrderState: string
{
    case Draft = 'draft';
    case Confirmed = 'confirmed';
    case Paid = 'paid';
    case Processing = 'processing';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';

    /**
     * 获取允许的下一状态
     * @return array<int, self>
     */
    public function allowedTransitions(): array
    {
        return match ($this) {
            self::Draft      => [self::Confirmed, self::Cancelled],
            self::Confirmed  => [self::Paid, self::Cancelled],
            self::Paid       => [self::Processing, self::Cancelled],
            self::Processing => [self::Shipped],
            self::Shipped     => [self::Delivered],
            self::Delivered   => [],
            self::Cancelled   => [],
        };
    }

    /**
     * 检查是否可以转换到指定状态
     */
    public function canTransitionTo(self $target): bool
    {
        return in_array($target, $this->allowedTransitions(), true);
    }

    /**
     * 执行状态转换
     */
    public function transitionTo(self $target): self
    {
        if (!$this->canTransitionTo($target)) {
            throw new InvalidArgumentException(
                "不能从 {$this->value} 转换到 {$target->value}"
            );
        }
        return $target;
    }

    public function isFinal(): bool
    {
        return empty($this->allowedTransitions());
    }
}

// 使用
$currentState = OrderState::Draft;
$newState = $currentState->transitionTo(OrderState::Confirmed);
echo $newState->value;  // confirmed

try {
    OrderState::Draft->transitionTo(OrderState::Shipped);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();  // 不能从 draft 转换到 shipped
}

枚举在数据库映射中的应用

php
<?php

declare(strict_types=1);

enum DbEnum: string
{
    case Active = 'active';
    case Inactive = 'inactive';
    case Archived = 'archived';
}

// 用于 Eloquent/Doctrine 等 ORM 的枚举转换 trait
trait BackedEnumCast
{
    /**
     * 从数据库值创建枚举
     */
    public static function fromDatabase(string|int $value): static
    {
        return static::from($value);
    }

    /**
     * 转换为数据库存储值
     */
    public function toDatabase(): string|int
    {
        return $this->value;
    }
}

// 使用
enum UserStatus: string
{
    use BackedEnumCast;

    case Active = 'active';
    case Inactive = 'inactive';
    case Banned = 'banned';
}

// 从数据库读取
$rawStatus = 'active';
$status = UserStatus::fromDatabase($rawStatus);
echo $status->name;  // Active

// 写入数据库
$dbValue = $status->toDatabase();
echo $dbValue;  // active

注意事项

枚举值的比较

php
<?php

declare(strict_types=1);

enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';
}

$a = Status::Active;
$b = Status::Active;
$c = Status::Inactive;

// 枚举值是单例,可以用 === 比较
var_dump($a === $b);  // bool(true)
var_dump($a === $c);  // bool(false)
var_dump($a == $b);   // bool(true)

// 比较值(而不是身份)
var_dump($a->value === 'active');  // bool(true)

// match 使用枚举值(推荐用 match 而非 switch)
$result = match ($a) {
    Status::Active   => '活跃',
    Status::Inactive => '非活跃',
};

枚举的限制

php
<?php

declare(strict_types=1);

// 枚举不能:
// 1. 被手动实例化(new Status() 会报错)
// 2. 拥有构造函数参数
// 3. 继承其他枚举或类
// 4. 被继承
// 5. 有动态属性
// 6. 实现 clone/unserialize

// 以下都是非法的:
// new Status();                    // 错误
// enum ChildStatus extends Status // 错误

// 但枚举可以:
// - 定义方法
// - 实现接口
// - 使用 traits
// - 作为类型提示
// - 作为数组键(通过 SplFixedArray 等方式)

枚举限制

枚举是单例模式,不能被 new 实例化。枚举不能被继承(final class 是隐含的)。枚举不能有构造函数参数。枚举 case 不能有对象属性。

最佳实践

  1. 始终使用 tryFrom 处理外部输入:来自用户输入或外部系统的值使用 tryFrom(),避免异常
  2. 使用 match 替代 switchmatch 对枚举值更安全,支持表达式和穷尽检查
  3. 为枚举添加有意义的方法:将枚举相关逻辑封装在枚举类中,而不是散落在其他类中
  4. 使用枚举类型声明:函数参数和返回值使用枚举类型,获得编译时类型安全
  5. 带值枚举的值选择int 更紧凑,string 更可读(如 API 接口)
php
<?php

declare(strict_types=1);

// 最佳实践:完整枚举模板
enum ErrorCode: int implements Stringable
{
    use HasLabel;

    case NotFound = 404;
    case ServerError = 500;
    case Unauthorized = 401;
    case Forbidden = 403;

    public function label(): string
    {
        return match ($this) {
            self::NotFound     => '资源未找到',
            self::ServerError  => '服务器错误',
            self::Unauthorized => '未授权',
            self::Forbidden    => '禁止访问',
        };
    }

    public function httpStatus(): int
    {
        return $this->value;
    }

    public function __toString(): string
    {
        return "Error({$this->value}): {$this->label()}";
    }
}

// 类型安全的函数
function handleError(ErrorCode $code, string $context): string
{
    return "{$code} — {$context}";
}

echo handleError(ErrorCode::NotFound, '用户不存在');
// Error(404): 资源未找到 — 用户不存在

参考链接