Skip to content

枚举 Trait

概述

PHP 8.1+ 允许在枚举中使用 Trait(use trait),将通用的枚举方法抽取到可复用的 Trait 中。这解决了多个枚举之间存在相同方法逻辑时代码重复的问题。枚举中使用 Trait 的方式与类中使用 Trait 完全一致。

版本要求

枚举中使用 Trait 是 PHP 8.1+ 专属特性。

基础概念

为什么枚举需要 Trait

枚举不能被继承(隐式 final),因此无法通过抽象基类来共享方法。Trait 提供了一种在多个枚举之间复用方法的机制。

枚举 Trait 的限制

  • Trait 中的方法不能向枚举添加属性
  • Trait 中可以使用 $this 引用枚举实例
  • Trait 可以包含抽象方法,由枚举实现

语法与代码

基本用法

php
<?php

declare(strict_types=1);

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

    public function description(): string
    {
        return "This is {$this->label()}";
    }
}

enum Status
{
    use HasLabel;

    case Active;
    case Inactive;

    public function label(): string
    {
        return match ($this) {
            self::Active => '活跃',
            self::Inactive => '不活跃',
        };
    }
}

echo Status::Active->label();       // 活跃
echo Status::Active->description(); // This is 活跃

多个枚举共享 Trait

php
<?php

declare(strict_types=1);

trait HasColor
{
    abstract public function color(): string;

    public function colorHtml(): string
    {
        return "<span style=\"color: {$this->color()}\">{$this->name}</span>";
    }
}

trait HasOptions
{
    public static function options(): array
    {
        return array_column(self::cases(), 'name');
    }
}

enum TrafficLight
{
    use HasColor;
    use HasOptions;

    case Red;
    case Yellow;
    case Green;

    public function color(): string
    {
        return match ($this) {
            self::Red => '#FF0000',
            self::Yellow => '#FFFF00',
            self::Green => '#00FF00',
        };
    }
}

enum AlertLevel
{
    use HasColor;
    use HasOptions;

    case Info;
    case Warning;
    case Error;

    public function color(): string
    {
        return match ($this) {
            self::Info => '#3B82F6',
            self::Warning => '#F59E0B',
            self::Error => '#EF4444',
        };
    }
}

echo TrafficLight::Red->colorHtml();
echo AlertLevel::Error->colorHtml();
print_r(TrafficLight::options());

Backed Enum 中使用 Trait

php
<?php

declare(strict_types=1);

trait Selectable
{
    public static function selectOptions(): array
    {
        $options = [];
        foreach (self::cases() as $case) {
            $options[$case->value] = $case->name;
        }
        return $options;
    }

    public static function toSelectArray(): array
    {
        return array_map(
            fn(self $case) => ['value' => $case->value, 'label' => $case->name],
            self::cases()
        );
    }
}

enum PaymentStatus: string
{
    use Selectable;

    case Pending = 'pending';
    case Completed = 'completed';
    case Failed = 'failed';
    case Refunded = 'refunded';
}

// 获取下拉选项
$options = PaymentStatus::selectOptions();
// ['pending' => 'Pending', 'completed' => 'Completed', ...]

Trait 中的抽象方法

php
<?php

declare(strict_types=1);

trait Enumerable
{
    abstract public function value(): mixed;

    public static function all(): array
    {
        return self::cases();
    }

    public static function count(): int
    {
        return count(self::cases());
    }

    public static function first(): static
    {
        return self::cases()[0];
    }

    public static function last(): static
    {
        $cases = self::cases();
        return $cases[array_key_last($cases)];
    }
}

enum Plan
{
    use Enumerable;

    case Free;
    case Basic;
    case Premium;

    public function value(): mixed
    {
        return $this->name;
    }
}

echo Plan::count();  // 3
echo Plan::first()->name;  // "Free"
echo Plan::last()->name;   // "Premium"

Trait 方法与枚举方法冲突

php
<?php

declare(strict_types=1);

trait CommonMethods
{
    public function name(): string
    {
        return $this->name;  // 与枚举内置 name 属性冲突
    }
}

enum BadEnum
{
    use CommonMethods;  // Fatal error: Trait method name conflicts

    case A;
}

冲突处理

如果 Trait 方法与枚举内置属性或方法冲突,需要使用 insteadofas 操作符解决,或者重命名 Trait 方法。

详细说明

Trait 在枚举中的适用场景

  1. 通用查询方法:如 all()count()options()
  2. 通用显示方法:如 label()color()icon()
  3. 通用转换方法:如 toArray()toJson()
  4. 通用验证方法:如 isValid()isDefault()

Trait 不能向枚举添加属性

php
<?php

declare(strict_types=1);

trait WithProperties
{
    // 枚举不能有属性,以下代码在枚举中使用会报错
    protected string $label = '';
}

enum BrokenEnum
{
    use WithProperties;  // Fatal error if trait has properties

    case A;
}

Trait 组合多个枚举的能力

php
<?php

declare(strict_types=1);

trait HasValidation
{
    public function isValid(): bool
    {
        return in_array($this, self::cases(), true);
    }
}

trait HasSerialization
{
    public function serialize(): string
    {
        return $this instanceof \BackedEnum
            ? $this->value
            : $this->name;
    }

    public static function deserialize(string $value): ?static
    {
        if (self::cases()[0] instanceof \BackedEnum) {
            return self::tryFrom($value);
        }
        return null;
    }
}

enum ErrorCode: int
{
    use HasValidation;
    use HasSerialization;

    case None = 0;
    case NotFound = 404;
    case ServerError = 500;
}

实战示例

场景一:通用枚举转换 Trait

php
<?php

declare(strict_types=1);

trait BackedEnumHelper
{
    /**
     * 获取所有值的数组
     * @return array<int|string>
     */
    public static function values(): array
    {
        return array_map(fn(self $case) => $case->value, self::cases());
    }

    /**
     * 值到名称的映射
     * @return array<int|string, string>
     */
    public static function valueToNameMap(): array
    {
        $map = [];
        foreach (self::cases() as $case) {
            $map[$case->value] = $case->name;
        }
        return $map;
    }

    /**
     * 检查值是否合法
     */
    public static function isValidValue(int|string $value): bool
    {
        return in_array($value, self::values(), true);
    }
}

enum UserType: string
{
    use BackedEnumHelper;

    case Customer = 'customer';
    case Merchant = 'merchant';
    case Admin = 'admin';
}

// 使用
var_dump(UserType::values());           // ['customer', 'merchant', 'admin']
var_dump(UserType::isValidValue('xxx')); // false

场景二:本地化 Trait

php
<?php

declare(strict_types=1);

trait HasLocalizedLabels
{
    abstract protected function labelMap(): array;

    public function label(string $locale = 'zh'): string
    {
        $map = $this->labelMap();
        return $map[$locale][$this->name] ?? $this->name;
    }
}

enum OrderState: string
{
    use HasLocalizedLabels;

    case New = 'new';
    case Processing = 'processing';
    case Shipped = 'shipped';
    case Completed = 'completed';

    protected function labelMap(): array
    {
        return [
            'zh' => [
                'New' => '新订单',
                'Processing' => '处理中',
                'Shipped' => '已发货',
                'Completed' => '已完成',
            ],
            'en' => [
                'New' => 'New Order',
                'Processing' => 'Processing',
                'Shipped' => 'Shipped',
                'Completed' => 'Completed',
            ],
        ];
    }
}

echo OrderState::New->label();       // 新订单
echo OrderState::New->label('en');  // New Order

注意事项

注意事项

  • 枚举不能被继承,Trait 是共享枚举方法的唯一方式
  • Trait 不能向枚举添加属性(properties)
  • Trait 方法名不能与枚举内置属性/方法冲突(如 namevalue
  • 可以使用 as 操作符为 Trait 方法起别名来避免冲突

小贴士

  • 将高频使用的枚举工具方法抽取为 Trait
  • 使用抽象方法确保枚举实现 Trait 所需的行为
  • Backed Enum 和纯枚举可以使用不同的 Trait

最佳实践

1. 保持 Trait 专注单一职责

php
<?php

declare(strict_types=1);

// 推荐 - 单一职责的 Trait
trait HasLabel { /* ... */ }
trait HasColor { /* ... */ }

// 不推荐 - 职责过多的 Trait
trait Everything { /* ... */ }

2. 使用抽象方法约束枚举实现

php
<?php

declare(strict_types=1);

trait Sortable
{
    abstract public function sortOrder(): int;

    public static function sorted(): array
    {
        $cases = self::cases();
        usort($cases, fn($a, $b) => $a->sortOrder() <=> $b->sortOrder());
        return $cases;
    }
}

参考链接