怎樣用 PHP 來實現枚舉?

枚舉

數學計算機科學理論中,一個集的枚舉是列出某些有窮序列集的所有成員的程序,或者是一種特定類型對象的計數。這兩種類型經常(但不總是)重疊。

枚舉是一個被命名的整型常數的集合,枚舉在日常生活中很常見,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就是一個枚舉。—— 維基百科

在上一篇文章中,我談到了 PHP 中的類型約束,這次我們來談實際應用。

業務場景

在實際開發過程中我們非常容易接觸到枚舉類型,但是又因爲 PHP 原生對枚舉的支持不是太好,所以很多時候 開發人員並沒有重視枚舉的使用,而是使用全局常量或者類常量代替,而這兩個數據原則上還是 字符串 並不能用來做類型判斷。

業務🌰

  • 訂單狀態 待支付/待發貨/待收貨/待評價
  • 會員狀態 激活/未激活
  • ....

等等 ,很多時候我們都會用簡單的 1/2/3/4 或者0/1 這樣的方式去代表,然後在文檔或者註釋中規定這些東西。

更高級一點兒的就是定義成常量,然後方便統一存取,但是常量的值還是是字符串,無法進行類型判斷。

這裏就要看一下 PHP 對枚舉的支持,雖然 PHP 對枚舉沒有完美的支持,但是在 SPL 中還是有一個基礎的枚舉類

SPL 枚舉

SplEnum extends SplType {

/ Constants /

const NULL __default = NULL ;

/ 方法 /

public getConstList ([ bool $include_default = FALSE ] ) : array

/ 繼承的方法 /

SplType::__construct ( [mixed $initial_value [, bool $strict ]] )

}

但是!這個需要額外的安裝 PECL 用 PECL 安裝 Spl_Types,無意間增加了使用成本,那有沒有其他解決方案?答案是肯定的。

直接手寫一個。

開始準備

首先定一個枚舉

class Enum
{
    // 默認值
    const __default = self::WAIT_PAYMENT;
    // 待付款
    const WAIT_PAYMENT = 0;
    // 待發貨
    const WAIT_SHIP = 1;
    // 待收貨
    const WAIT_RECEIPT = 2;
    // 待評價
    const WAIT_COMMENT = 3;
}

這樣似乎就完成了,我們直接使用 Enum::WAIT_PAYMENT 就可以拿到裏面的值了,但是傳參的地方我們並沒法校驗他。

function setStatus(Enum $status){
    // TODO
}
setStatus(Enum::WAIT_PAYMENT);
// Error 顯然這是不行的 因爲上面常量的值時一個 int 並不是 Enum 類型。

這裏我們就需要用到 PHP 面向對象中的一個魔術方法 __toString()

public __toString ( void ) : string

__toString() 方法用於一個類被當成字符串時應怎樣迴應。例如 echo $obj; 應該顯示些什麼。此方法必須返回一個字符串,否則將發出一條 E_RECOVERABLE_ERROR 級別的致命錯誤。

現在我們來完善一下這個方法。

class OrderStatus extends Enum
{
    // 默認值
    const __default = self::WAIT_PAYMENT;
    // 待付款
    const WAIT_PAYMENT = 0;
    // 待發貨
    const WAIT_SHIP = 1;
    // 待收貨
    const WAIT_RECEIPT = 2;
    // 待評價
    const WAIT_COMMENT = 3;

    public function __toString()
    {
        return '233';
    }
}
// object
echo gettype($orderStatus) . PHP_EOL;
// boolean true
var_dump($orderStatus instanceof Enum);
// 233
echo $orderStatus;

初具模型

這裏似乎實現了一部分,那我們應該怎麼樣讓他做的更好?再來改造一下。

class OrderStatus extends Enum
{
    // 默認值
    const __default = self::WAIT_PAYMENT;
    // 待付款
    const WAIT_PAYMENT = 0;
    // 待發貨
    const WAIT_SHIP = 1;
    // 待收貨
    const WAIT_RECEIPT = 2;
    // 待評價
    const WAIT_COMMENT = 3;
    /**
     * @var string
     */
    protected $value;

    public function __construct($value = null)
    {
        $this->value = is_null($value) ? self::__default : $value;
    }

    public function __toString()
    {
        return (string)$this->value;
    }
}

// 1️⃣
$orderStatus = new OrderStatus(OrderStatus::WAIT_SHIP);

// object
echo gettype($orderStatus) . PHP_EOL;
// boolean true
var_dump($orderStatus instanceof Enum);
// 1
echo $orderStatus . PHP_EOL;
// 2️⃣
$orderStatus = new OrderStatus();
// object
echo gettype($orderStatus) . PHP_EOL;
// boolean true
var_dump($orderStatus instanceof Enum);
// 0
echo $orderStatus;
// 3️⃣
$orderStatus = new OrderStatus('意外的參數');
// object
echo gettype($orderStatus) . PHP_EOL;
// boolean true
var_dump($orderStatus instanceof Enum);
// 意外的參數
echo $orderStatus;

在這一次,我們加入了 構造函數 並且允許他傳入一個可選的值,然後來作爲 __toString 方法的輸出值,這次看起來不錯,功能都已經實現了,如果傳入的參數否和我們的預期的話。但是 萬一不符合呢?看看,第 3️⃣ 個那裏,就已經成了意外了,哪還有沒有辦法補救?答案當然是 有的 ,在這裏我們會用到 PHP 另一個好東西 反射類 ,當然這個不是 PHP 特有的,其他語言也有。
當然,除了反射,我們還會用到另外一個東西 方法重載 裏面的 __callStatic 方法。

更進一步

public static __callStatic ( string $name , array $arguments ) : mixed

在靜態上下文中調用一個不可訪問方法時,__callStatic() 會被調用。

$name 參數是要調用的方法名稱。$arguments 參數是一個枚舉數組,包含着要傳遞給方法 $name 的參數。

繼續改造。

class Enum
{
    const __default = null;
    /**
     * @var string
     */
    protected static $value;

    // 注意這裏 將構造函數的 修飾符改成了 受保護的 即 外部無法直接 new
    protected function __construct($value = null)
    {
        // 很常規
        self::$value = is_null($value) ? static::__default : $value;
    }

    /**
     * @param $name
     * @param $arguments
     * @return mixed
     * @throws ReflectionException
     */
    public static function __callStatic($name, $arguments)
    {
        // 實例化一個反射類 static::class 表示調用者
        $reflectionClass = new ReflectionClass(static::class);
        // 這裏我們要有一個約定, 就是類常量成員的名字必須的大寫。
        // 這裏就是取出來調用的靜態方法名對應的常量值 雖然這裏有個 getValue 方法
        // 但是因爲其返回值不可靠 我們就依賴於他原本的隱式的 __toString 方法來幫我們輸出字符串即可。
        $constant = $reflectionClass->getConstant(strtoupper($name));
        // 獲取調用者的 構造方法
        $construct = $reflectionClass->getConstructor();
        // 設置成可訪問 因爲我們把修飾符設置成了受保護的 這裏需要訪問到,所以就需要設置成可訪問的。
        $construct->setAccessible(true);
        // 因爲現在類已經是可以訪問的了所以我們直接實例化即可,實例化之後 PHP 會自動調用 __toString 方法 使得返回預期的值。
        $static = new static($constant);
        return $static;
    }

    public function __toString()
    {
        return (string)self::$value;
    }

}

class OrderStatus extends Enum
{
    // 默認值
    const __default = self::WAIT_PAYMENT;
    // 待付款
    const WAIT_PAYMENT = 0;
    // 待發貨
    const WAIT_SHIP = 1;
    // 待收貨
    const WAIT_RECEIPT = 2;
    // 待評價
    const WAIT_COMMENT = 3;

}

$WAIT_SHIP = OrderStatus::WAIT_SHIP();
var_dump($WAIT_SHIP . '');
var_dump($WAIT_SHIP instanceof Enum);

到這裏 一個簡單的枚舉類就完成了。

完結

那如果我們還有其他需求、比如 判斷一個值是不是在枚舉範圍內?獲取所有的枚舉值?獲取所有的枚舉鍵,判斷枚舉鍵是否有效?自動格式化「因爲 __toString 方法只允許返回字符串 ,但是有的時候我們強制需要整形、bool 等類型

class Enum
{
    const __default = null;
    /**
     * @var string
     */
    protected static $value;
    /**
     * @var ReflectionClass
     */
    protected static $reflectionClass;

    // 注意這裏 將構造函數的 修飾符改成了 受保護的 即 外部無法直接 new
    protected function __construct($value = null)
    {
        // 很常規
        self::$value = is_null($value) ? static::__default : $value;
    }

    /**
     * @param $name
     * @param $arguments
     * @return mixed
     */
    public static function __callStatic($name, $arguments)
    {
        // 實例化一個反射類 static::class 表示調用者
        $reflectionClass = self::getReflectionClass();
        // 這裏我們要有一個約定, 就是類常量成員的名字必須的大寫。
        // 這裏就是取出來調用的靜態方法名對應的常量值 雖然這裏有個 getValue 方法
        // 但是因爲其返回值不可靠 我們就依賴於他原本的隱式的 __toString 方法來幫我們輸出字符串即可。
        $constant = $reflectionClass->getConstant(strtoupper($name));
        // 獲取調用者的 構造方法
        $construct = $reflectionClass->getConstructor();
        // 設置成可訪問 因爲我們把修飾符設置成了受保護的 這裏需要訪問到,所以就需要設置成可訪問的。
        $construct->setAccessible(true);
        // 因爲現在類已經是可以訪問的了所以我們直接實例化即可,實例化之後 PHP 會自動調用 __toString 方法 使得返回預期的值。
        $static = new static($constant);
        return $static;
    }

    /**
     * 實例化一個反射類
     * @return ReflectionClass
     * @throws ReflectionException
     */
    protected static function getReflectionClass()
    {
        if (!self::$reflectionClass instanceof ReflectionClass) {
            self::$reflectionClass = new ReflectionClass(static::class);
        }
        return self::$reflectionClass;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return (string)self::$value;
    }

    /**
     * 判斷一個值是否有效 即是否爲枚舉成員的值
     * @param $val
     * @return bool
     * @throws ReflectionException
     */
    public static function isValid($val)
    {
        return in_array($val, self::toArray());
    }

    /**
     * 轉換枚舉成員爲鍵值對輸出
     * @return array
     * @throws ReflectionException
     */
    public static function toArray()
    {
        return self::getEnumMembers();
    }

    /**
     * 獲取枚舉的常量成員數組
     * @return array
     * @throws ReflectionException
     */
    public static function getEnumMembers()
    {
        return self::getReflectionClass()
            ->getConstants();
    }

    /**
     * 獲取枚舉成員值數組
     * @return array
     * @throws ReflectionException
     */
    public static function values()
    {
        return array_values(self::toArray());
    }

    /**
     * 獲取枚舉成員鍵數組
     * @return array
     * @throws ReflectionException
     */
    public static function keys()
    {
        return array_keys(self::getEnumMembers());
    }

    /**
     * 判斷 Key 是否有效 即存在
     * @param $key
     * @return bool
     * @throws ReflectionException
     */
    public static function isKey($key)
    {
        return in_array($key, array_keys(self::getEnumMembers()));
    }

    /**
     * 根據 Key 去獲取枚舉成員值
     * @param $key
     * @return static
     */
    public static function getKey($key)
    {
        return self::$key();
    }

    /**
     * 格式枚舉結果類型
     * @param null|bool|int $type 當此處的值時什麼類時 格式化輸出的即爲此類型
     * @return bool|int|string|null
     */
    public function format($type = null)
    {
        switch (true) {
            // 當爲純數字 或者類型處傳入的爲 int 值時 轉爲 int
            case ctype_digit(self::$value) || is_int($type):
                return (int)self::$value;
                break;
            // 當 type 傳入 true 時 返回 bool 類型
            case $type === true:
                return (bool)filter_var(self::$value, FILTER_VALIDATE_BOOLEAN);
                break;
            default:
                return self::$value;
                break;
        }
    }

}

class OrderStatus extends Enum
{
    // 默認值
    const __default = self::WAIT_PAYMENT;
    // 待付款
    const WAIT_PAYMENT = 0;
    // 待發貨
    const WAIT_SHIP = 1;
    // 待收貨
    const WAIT_RECEIPT = 2;
    // 待評價
    const WAIT_COMMENT = 3;

}

$WAIT_SHIP = OrderStatus::WAIT_SHIP();
// 直接輸出是字符串
echo $WAIT_SHIP;
// 判斷類型是否存在
var_dump($WAIT_SHIP instanceof OrderStatus);
// 格式化輸出一下 是要 字符串 、還是 bool 還是整形
// 自動
var_dump($WAIT_SHIP->format());
// 整形
var_dump($WAIT_SHIP->format(1));
// bool
var_dump($WAIT_SHIP->format(true));
// 判斷這個值是否有效的枚舉值
var_dump(OrderStatus::isValid(2));
// 判斷這個值是否有效的枚舉值
var_dump(OrderStatus::isValid(8));
// 獲取所有枚舉成員的 Key
var_dump(OrderStatus::keys());
// 獲取所有枚舉成員的值
var_dump(OrderStatus::values());
// 獲取枚舉成員的鍵值對
var_dump(OrderStatus::toArray());
// 判斷枚舉 Key 是否有效
var_dump(OrderStatus::isKey('WAIT_PAYMENT'));
// 判斷枚舉 Key 是否有效
var_dump(OrderStatus::isKey('WAIT_PAYMENT_TMP'));
// 根據 Key 取去 值 注意 這裏取出來的已經不帶有類型了
// 更加建議直接使用 取類常量的方式去取 或者在高版本的 直接使用類常量修飾符 
// 將類常量不可見最佳,但是需要額外處理了
var_dump(OrderStatus::getKey('WAIT_PAYMENT')
    ->format(1));

截至目前 一個完整的枚舉就完成了~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章