iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 6

Day 06:Backed Enum 玩法——用類別常數當 enum 值做型別查找表

  • 分享至 

  • xImage
  •  

前言:一個字串欄位,怎麼變成一個安全的型別?

「資料庫存的是字串,程式碼裡拿字串來比對,有什麼問題嗎?」

字串比對能動,但它有一個天生的弱點:打錯一個字、大小寫不一致,程式不會在你寫程式碼的當下就告訴你錯了,只會在執行到那一行的時候,安靜地判斷失敗。今天要看的是一個把「字串欄位」升級成「型別安全查找表」的技巧——PHP 的 backed enum,用類別常數當作值。

今日目標

  • 認識 PHP backed enum 的基本語法
  • 看一個真實案例:enum 的 case 值直接用 ::class 常數,而不是隨手寫的字串
  • 理解這個技巧解決了什麼問題、換來了什麼好處
  • 知道這個技巧不限定用在特定情境,只要是「型別 → 對應處理類別」的查找需求都適用

本文主體

一個欄位存的其實是類別名稱字串

昨天提到 Page Model 有一個 template 欄位,這個欄位實際存的值,是後台一個管理類別的完整類別名稱字串(例如 "App\Xxx\PageTemplate" 這種格式)。這個系統用一個 backed enum 把這些字串值定義成型別:

enum PageType: string
{
    case PAGE = PageTemplate::class;
    case NEWS = NewsTemplate::class;
    case ALIAS = AliasTemplate::class;
    case LINK = LinkTemplate::class;
    case LOGIN = LoginTemplate::class;
}

注意每個 case 的值不是隨手寫的字串(像 case PAGE = 'page'),而是直接用 ::class 這個類別常數。這幾個類別各自對應到後台管理層的一個具體實作(這個系統用它們渲染不同的頁面編輯表單,這一層跟今天要講的技巧無關,不展開),但這個技巧本身值得單獨拆出來看,因為它跟這些類別實際是做什麼的完全無關

為什麼不直接寫字串常數

如果 case 值直接寫字串:

enum PageType: string
{
    case PAGE = 'App\\Xxx\\PageTemplate';
    case NEWS = 'App\\Xxx\\NewsTemplate';
}

這樣寫,字串本身跟它指向的類別是兩個完全獨立的東西——類別如果哪天被重新命名或搬到別的命名空間,這個字串不會自動更新,也不會有任何工具告訴你這裡忘記改了,只有在執行期真的比對到這個 case 時才會發現「找不到這個類別」。

改用 ::class 常數,PHP 在編譯期就會把類別名稱解析成完整的字串,換來三個好處:

  • 換類別名稱時,IDE 的重新命名功能會自動更新這裡,不會出現改了類別名稱、卻忘了改 enum 裡對應字串的情況
  • 打錯類別名稱會在載入類別時就直接報錯,不用等到執行期比對失敗才發現
  • IDE 能直接從 enum 的 case 定義跳到對應類別的原始碼,字串常數做不到這件事

消費端怎麼用這個 enum

昨天看過的 Page Model,用 PageType::tryFrom() 把資料庫存的字串值轉成型別:

protected function type(): Attribute
{
    return Attribute::get(fn () => PageType::tryFrom($this->template));
}

轉成型別之後,後續的判斷全部用型別比對,不再是裸字串比對:

protected function url(): Attribute
{
    return Attribute::get(function () {
        if ($this->type === PageType::ALIAS) {
            return $this->alias?->url;
        }

        if ($this->type === PageType::LINK) {
            return $this->content['url'];
        }
        // ...
    });
}

這裡才是這個技巧真正發威的地方:Page 消費 PageType 的方式,完全是純 Eloquent 的型別分派邏輯,跟這個 enum 的值指向什麼類別、那些類別在做什麼完全無關。 就算把 PageType 的每個 case 值換成完全不相關的類別,Page::url() 這段程式碼一個字都不用改,因為它只在乎「這是不是 PageType::LINK」,不在乎 LinkTemplate 這個類別本身長什麼樣。

❌ 用裸字串當查找表

if ($page->template === 'App\\Xxx\\LinkTemplate') {
    return $page->content['url'];
}

打錯一個字,這個判斷式永遠不會成立,而且不會有任何提示。

✅ 用 ::class 常數當 backed enum 值

enum PageType: string
{
    case LINK = LinkTemplate::class;
}

if ($page->type === PageType::LINK) {
    return $page->content['url'];
}

打錯類別名稱,在類別載入時就會直接報錯;換類別名稱,IDE 的重新命名功能會自動處理。

這個技巧不限定用在 UI 類別對應

這個系統剛好拿它對應到後台管理類別,但這個技巧本身適用於任何「一個型別值要對應到一個處理類別」的情境——例如一個支付方式的 enum,每個 case 對應到一個支付處理器類別;一個檔案格式的 enum,每個 case 對應到一個解析器類別。只要你發現自己在寫「拿一個字串去 match 出對應類別」這種邏輯,backed enum 用 ::class 當值,會比裸字串常數安全很多。

今日思考題

你的專案裡有沒有一段「拿一個字串值去對應到一個處理類別」的邏輯,目前是用裸字串比對寫的?如果換成 backed enum 用 ::class 當值,能不能提早抓到打錯字或忘記更新的情況?

今日重點回顧

  • PHP backed enum 的 case 值可以直接用 ::class 類別常數,不用寫裸字串
  • 真實案例:PageType 每個 case 對應到一個處理類別,用 ::class 常數換掉裸字串,換來編譯期檢查跟 IDE 支援
  • 消費端(Page Model)的判斷邏輯只在乎「是不是這個 enum case」,完全不需要知道 case 值背後指向的類別在做什麼
  • 這個技巧不限定用在特定框架或情境,任何「型別 → 對應處理類別」的查找需求都適用

明日預告

明天要看測試資料怎麼用具名的 Factory state 方法(例如 superAdmin()),讓建立測試資料的意圖一目了然。


上一篇
Day 05:查詢邏輯怎麼寫成可組合的 Query Scope Class
下一篇
Day 07:Model Factory 語意化設計——用具名 state 表達建立資料的意圖
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言