「資料庫存的是字串,程式碼裡拿字串來比對,有什麼問題嗎?」
字串比對能動,但它有一個天生的弱點:打錯一個字、大小寫不一致,程式不會在你寫程式碼的當下就告訴你錯了,只會在執行到那一行的時候,安靜地判斷失敗。今天要看的是一個把「字串欄位」升級成「型別安全查找表」的技巧——PHP 的 backed enum,用類別常數當作值。
::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 在編譯期就會把類別名稱解析成完整的字串,換來三個好處:
昨天看過的 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 的重新命名功能會自動處理。
這個系統剛好拿它對應到後台管理類別,但這個技巧本身適用於任何「一個型別值要對應到一個處理類別」的情境——例如一個支付方式的 enum,每個 case 對應到一個支付處理器類別;一個檔案格式的 enum,每個 case 對應到一個解析器類別。只要你發現自己在寫「拿一個字串去 match 出對應類別」這種邏輯,backed enum 用 ::class 當值,會比裸字串常數安全很多。
你的專案裡有沒有一段「拿一個字串值去對應到一個處理類別」的邏輯,目前是用裸字串比對寫的?如果換成 backed enum 用 ::class 當值,能不能提早抓到打錯字或忘記更新的情況?
::class 類別常數,不用寫裸字串PageType 每個 case 對應到一個處理類別,用 ::class 常數換掉裸字串,換來編譯期檢查跟 IDE 支援Page Model)的判斷邏輯只在乎「是不是這個 enum case」,完全不需要知道 case 值背後指向的類別在做什麼明天要看測試資料怎麼用具名的 Factory state 方法(例如 superAdmin()),讓建立測試資料的意圖一目了然。