簡單介紹 PHP 8.1 的列舉 (Enumerations)

程式技術

PHP 8.1 在前陣子正式推出!加入不少新功能,其中包含最多人期待的列舉 (Enumerations)。列舉可以用來定義一系列的常數設定值,可以放在類型提示告知開發者哪些值是可以使用的,避免在開發時使用無效的設定值。

簡單介紹 Enum 的特性與各種功能

PHP 中宣告 Enum 的方式如下。可以看到一個 Enum 可以包含多個 Case,以此明確告知開發者有哪些 Case 是可以使用的。

// 有回退的 enum (也就是那個 return type hint),一定要設定初始值
enum UserName: string {
    case ADMIN = 'admin';
    case USER = 'user';
    case GUEST = 'guest';
}

// 沒有回退 (non-backed) 的 enum 不能設定初始值
enum UserRole {
    case ADMIN;
    case USER;
    case GUEST;
}

// 有回退的 enum 只能設定 int 與 string
enum UserNumber: int {
    case ADMIN = 1;
    case USER = 2;
    case GUEST = 3;
}

Enum 可以看作是物件,有預設的屬性 namevalue

// enum 物件有預設的屬性 name 與 value
var_dump(UserName::ADMIN->name); // ADMIN
var_dump(UserName::ADMIN->value); // admin

此外也有預設的靜態方法 from(),此方法可以依據 value 回推 enum 中的 case,如果找不到就會拋出錯誤。如果不想讓錯誤停止程式執行,可以使用 tryFrom()

// enum 物件有預設的方法
var_dump(UserName::from('admin')); // enum(Enum\UserName::ADMIN)
var_dump(UserName::from('hello')); // throw error !!!
var_dump(UserName::tryFrom('hello')); // null

你可以使用 cases() 來遍歷 Enum 中所有的 Case。

enum UserRole {
    case ADMIN;
    case USER;
    case GUEST;
}

foreach (UserRole::cases() as $role) { 
    echo $role->name . "\n";
}

// 執行結果為
// ADMIN
// USER
// GUEST

Enum 內部還可以設定方法。

enum UserRole {
    case ADMIN;
    case USER;
    case GUEST;

    // enum 內部可以設定 method
    public function content(): string
    {
        return match($this) {
            self::ADMIN => 'admin',
            self::USER => 'user',
            self::GUEST => 'guest',
        };
    }
}

var_dump(UserRole::ADMIN->content()); // admin

當一個類別的初始參數被設定為 Enum 時,就只能傳入 Enum 的 Case,就樣就能限制值的範圍,避免傳入預料外的內容。

注意這裡不是接受 Case 的對應值,也就是純數字或是純字串。而是傳入 Enum 的 Case。

class User
{
    public function __construct(
        public UserName $userName,
    ) {}
}

$user = new User('admin'); // throw error !!!
$user = new User(UserName::ADMIN);

var_dump($user->userName); // enum(Enum\UserName::ADMIN)

用 Enum 來重構部落格的文章排列選單

我的部落格文章列表有三種排列順序,分別是「最新文章」、「最近更新」與「最多留言」。這種有固定一系列值的情境就很適合使用 Enum。

首先宣告一個 PostOrder 的 Enum。

namespace App\Enums;

enum PostOrder: string
{
    case LATEST = 'latest';
    case RECENT = 'recent';
    case COMMENT = 'comment';

    public function label(): string
    {
        return match ($this) {
            self::LATEST => '最新文章',
            self::RECENT => '最近更新',
            self::COMMENT => '最多留言',
        };
    }

    public function iconComponentName(): string
    {
        return match ($this) {
            self::LATEST => 'icon.stars',
            self::RECENT => 'icon.wrench',
            self::COMMENT => 'icon.chat-square-text',
        };
    }
}

接著在前端使用 PostOrder 來顯示選單。

<!-- laravel blade 範例程式碼 -->

<!-- 引入 PostOrder enums -->
@php
  use App\Enums\PostOrder;
@endphp

<nav>
  <!-- 遍歷 PostOrder enum 中所有的 case -->
  @foreach (PostOrder::cases() as $postOrder)
    <!-- 這裡使用 livewire 提供的語法來更新後端對文章的排列順序 -->
    <!-- changeOrder 這個方法我設定只能接收 PostOrder 的 case -->
    <!-- 傳入目前的 PostOrder case -->
    <button
      type="button"
      wire:click="changeOrder('{{ $postOrder }}')"
      wire:key="post-order-{{ $postOrder->value }}"
    >
      <!-- 根據目前的 PostOrder case,產生不一樣的 component -->
      <x-dynamic-component
        :component="$postOrder->iconComponentName()"
      />

      <!-- 根據目前的 PostOrder case,產生不一樣的選單文字 -->
      <span>{{ $postOrder->label() }}</span>
    </button>
  @endforeach
</nav>

在後端的 changeOrder 方法,我們可以使用類型提示限定參數只能接受 PostOrder 的 Case。

use App\Enums\PostOrder;

// ...

// 這裡設定參數只能接受 PostOrder 的 case
public function changeOrder(PostOrder $newOrder): void
{
    $this->order = $newOrder->value;

    $this->resetPage();
}

PHP 的 Enum 除了可以明確告知開發者應該使用哪些值之外,也可以根據不同的情況讓 Case 返回不一樣的內容,真的非常好用!

參考資料

PHP: Enumerations overview - Manual

sharkHead
written by
sharkHead

後端打工仔,在下班後喜歡研究各種不同的技術。稍微擅長 PHP,並偶爾涉獵前端開發。個性就像動態語言般隨興,但渴望做事能像囉嗦的靜態語言那樣嚴謹。

1 則留言
sharkHead sharkHead (已編輯)

話說更新這篇文章才發現原來 Livewire Component 支援在方法中傳入原生的 PHP Enum,詳細可以參考這個 PR

在 Livewire 中,下面這段 Blade 語法:

<!-- 假設 $postOrder 的值為 PostOrder::LATEST -->
<button
  type="button"
  wire:click="changeOrder('{{ $postOrder }}')"
>
  <!-- ... -->
</button>

渲染出來的 HTML 會變成:

<button
  type="button"
  wire:click="changeOrder('latest')"
>
  <!-- ... -->
</button>
新增留言
編輯留言