天天看點

你是如何處理 PHP 代碼中的枚舉類型 Enum 的?

本文旨在提供一些更好的了解什麼是枚舉,什麼時候使用它們以及如何在php中使用它們.

我們在某些時候使用了常量來定義代碼中的一些常數值.他們被用來避免 魔法值 .用一個象征性的名字代替一些 魔法值 ,我們可以給它一些意義.然後我們在代碼中引用這個符号名稱.因為我們定義了一次并使用了很多次,是以搜尋它并稍後重命名或更改一個值會更容易.

這就是為什麼看到類似于下面的代碼并不罕見.

<?php
class User {
    const GENDER_MALE = 0;
    const GENDER_FEMALE = 1;
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
}           

複制

以上常量表示了兩組屬性,GEDNER_* 和 STATUS_*。他們表示一組性别和一組使用者狀态。每一組都是一個 枚舉 。枚舉是一組元素(也叫做成員)的集合,每一個枚舉都定義了一種新類型。這個類型,和它的值一樣,可以包含任意屬于該枚舉的元素。

在上面的例子中,枚舉借助于常量,每一個常量的值都是一個成員。注意,這樣做的話,我們隻能在常量包含的類型中取值。是以,我們在寫這些值的時候不會有類型提示,不知道詳細的枚舉類型。

來看一個簡短的例子, 但我們假定例子中有更多的代碼

<?php
interface UserFactory {
    public function create(
        string $email,
        int $gender,
        int $status
    ): User;
}
$factory->create(
    $email,
    User::STATUS_ACTIVE,
    User::GENDER_FEMALE
);           

複制

第一眼看上去代碼很好,但是他隻是碰巧正确運作了!因為兩個不同的枚舉成員實際上是同一個值,調用create方法成功,是因為這最後兩個參數被互換了不影響結果。盡管我們檢查方法接受的值是否有效,運作界面也不會警告我們,測試也會通過。有人能正确的發現這些bug,但是它也很可能被忽視掉。之後一些情況,比如合并沖突的時候,如果它的值改變了,它可能會引起系統異常。

如果使用标量類型,我們會受限于這種類型,無法辨識這兩個值是是不是屬于兩個不同的枚舉。

另一個問題是這個代碼描述的的不是很好。想象一下

create

方法沒有引用常量。

$gender

被别人看作為一個枚舉元素将是有多麼困難?看這些元素在哪裡被定義又有多麼困難?我們之後将會閱讀那些代碼,是以我們應該盡可能是讓代碼易于閱讀以及和通過。

我們可以做得更好嗎?Sure!這個方法就是是使用類執行個體作為枚舉元素,類本身定義了一個新的類型。直到PHP 7,我們可以安裝 SPL類 PECL擴充并且使用 SplEnum 。

<?php
class YesNo extends \SplEnum
{
    const __default =  self::YES;
    const NO = 0;
    const YES = 1;
}
$no = new YesNo(YesNo::NO);
var_dump($no == YesNo::NO); //true
var_dump(new YesNo(YesNo::NO) == YesNo::NO); //true           

複制

我們擴充

SplEnum

并且定義用于建立枚舉元素的常量。枚舉元素是我們手動構造的對象,在這種情況下是常量值本身。我們可以将整型與對象進行比較,這可能很奇怪。另外,正如文檔所述,這是一個仿真的枚舉。PHP本身并不支援枚舉類型,是以我們在這裡探讨的所有内容都是仿真的。

我們用這種方法得到了什麼?我們可以輸入提示我們的參數,并讓PHP引擎在發生錯誤時提醒我們。我們還可以在枚舉類中包含一些邏輯,并使用

switch

語句來模拟多态行為。

但也有一些缺點. 例如, 在大多數情況下, 有些你可以用枚舉元素而不能用辨別檢查. 這不是不可能的,我們不得不非常小心. 由于我們手動建立枚舉成員, 是以許多成員應該是同一個成員, 但這一點手動很難确定.

利用

SplEnum

我們解決枚舉類型問題, 但是當我們用辨別檢查的時候不得不非常小心. 我們需要一個方法限制可以建立的多個元素, 例如 multiton (multiple singleton objects ).

現在我們将看到由 Java Enum 啟發并實作

multiton

的兩個不同的庫.

第一個是 eloquent/enumeration . 它為每個元素建立一個定義類的執行個體. 請注意, 沒有我們的幫助, 枚舉的使用者仿真永遠不能保證一個枚舉執行個體, 因為我們限制它的每一步都有一個方法去避免.

這個庫可以讓我們用錯誤的方式去嘗試, 例如用反射建立一個執行個體, 在這一點上我們可以問我們自己是否做了正确的事. 它也可以在代碼的評審過程中有所幫助,因為這樣的實作可以定義幾個應該被遵循的規則. 如果這些規則比較簡單很容易發現代碼中存在的問題.

讓我們看些執行個體.

<?php
final class YesNo extends \Eloquent\Enumeration\AbstractEnumeration {
    const NO = 0;
    const YES = 1;
}
var_dump(YesNo::YES()->key()); // YES           

複制

我們定義了一個繼承

\Eloquent\Enumeration\AbstractEnumeration

的新類

YesNo

. 接下來我們定義一個定義元素名和建立表現這些元素的對象的庫的常量.

還有一些情況我們需要謹記,用

serialize

/

deserialize

在其中建立自定義對象 .

我們可以在GitHub頁面上找到更多的例子和很完善的文檔。

我們要展示的第二個庫是 zlikavac32/php-enum . 與

eloquent/enumeration

不同,這個庫面向允許真正的多态行為的抽象類。是以,我們可以用每個方法都定義一個枚舉元素來實作,而不是使用

switch

的方法。通過嚴格的規則來定義枚舉,也可以相當可靠地確定每個元素隻有一個執行個體。

這個庫面向抽象類,以便将每個成員的許多執行個體限制為一個。這個想法是,每個枚舉必須被定義為抽象的,并枚舉它的元素。請注意,你可以通過擴充類,然後構造一個元素來濫用,但是如果你這麼用了,這些是會在代碼審查過程中标紅的。

對于抽象類,我們知道我們不會意外地有一個枚舉的新元素,因為它需要具體的實作。通過遵循在enum本身中保持這些具體實作的規則,我們可以很容易地發現濫用。 匿名類 在這裡很有用。

庫強制抽象枚舉類,但不能強制建立有效的元素。這是這個庫的使用者的責任。圖書館照顧其餘的。

讓我們看一個簡單的例子。

<?php
/**
 * @method static YesNo YES
 * @method static YesNo NO
 */
abstract class YesNo extends \Zlikavac32\Enum\Enum
{
    protected static function enumerate(): array
    {
        return [
            'YES', 'NO'
        ];
    }
}
var_dump(YesNo::YES()->name()); // YES           

複制

PHPDoc注釋定義了傳回枚舉元素的現有靜态方法。這有助于搜尋和重構代碼。接下來,我們将枚舉

YesNo

定義為抽象,并擴充

\Zlikavac32\Enum\Enum

并定義一個靜态方法

enumerate

。然後,在

enumerate

方法中,我們列出将被用來表示它們的元素名稱。

剛剛我們提到了多态行為,那麼為什麼我們會使用它呢?當我們試圖限制同一個枚舉元素的多個執行個體時會發生一件事,那就是我們不能有循環引用。讓我們想象一下,我們想擁有由

NORTH

SOUTH

EAST

WEST

組成的

WorldSide

枚舉。我們還想有一個方法

opposite():WorldSide

,它傳回代表相反的元素。

如果我們試圖通過構造函數注入相反元素,在某一時刻,我們獲得一個循環引用,這意味着,我們需要相同元素的第二個執行個體。為了傳回一個有效的相反世界,我們不得不用一個 代理對象 或者

switch

語句破解。

随着多态行為,我們能做的就是讓我們看到我們可定義我們需要的

WorldSide

枚舉。

<?php
/**
 * @method static WorldSide NORTH
 * @method static WorldSide SOUTH
 * @method static WorldSide EAST
 * @method static WorldSide WEST
 */
abstract class WorldSide extends \Zlikavac32\Enum\Enum
{
    protected static function enumerate(): array
    {
        return [
            'NORTH' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::SOUTH();
                }
            },
            'SOUTH' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::NORTH();
                }
            },
            'EAST' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::WEST();
                }
            },
            'WEST' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::EAST();
                }
            }
        ];
    }
    abstract public function opposite(): WorldSide;
}
foreach (WorldSide::iterator() as $worldSide) {
    var_dump(sprintf(
        'Opposite of %s is %s',
        (string) $worldSide,
        (string) $worldSide->opposite()
    ));
}           

複制

enumerate

方法,我們提供了每一個枚舉元素的實作。數組是用枚舉元素名稱來索引的。當手動的建立元素,我們定義我們元素名稱作為資料的鍵。

我們可以用

WorldSide::iterator()

擷取枚舉元素的順序疊代器,來定義和周遊他們。每一個枚舉元素都有一個預設的

__toString(): string

實作傳回元素的名稱。

每個枚舉元素傳回其相反的元素。

回顧一下,常量不是枚舉,枚舉不是常量。每個枚舉定義一個類型。如果我們有一些常數的值對我們很重要,但名字沒有,我們應該堅持常數。如果我們有一些常量的價值對我們無關緊要,但是與同一群體中的其他所有人有所不同則是重要的,請使用枚舉

枚舉為代碼提供了更多的上下文,也可以将某些檢查委托給引擎本身。如果PHP有一個本地的枚舉支援,這将是非常好的。文法更改可以使代碼更具可讀性。引擎可以為我們執行檢查,并執行一些不能從使用者區執行的規則。

原文:https://learnku.com/laravel/t/7479/how-do-you-deal-with-the-enumerated-type-enum-in-the-php-code