教你更優雅地寫 API 之「列舉使用」

Jiannei發表於2020-12-26

教你更優雅地寫 API  之「列舉使用」

前言

在上一篇 教你更優雅地寫 API 之「規範響應資料」中,定義響應操作碼時就已經用到列舉,通過定義 ResponseCodeEnum可以很方便地規範操作碼的定義,以及實現多語言的操作碼描述。本篇是基於前面文章的基礎上繼續深入討論專案中列舉的使用。

Package 地址:larave-enum

Laravel 版本 Api 開發初始化專案:laravel-api-starter

Lumen 版本 Api 開發初始化專案:lumen-api-starter

定義

在討論列舉使用前,首先丟擲一個概念:列舉 ≠ 常量

引入維基百科的一段描述:列舉

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

可以認為,列舉是用於描述某一特徵一組常量。(本文將定義在class 內部的一組常量稱為列舉)

  • 常量的定義
define('LARAVEL_START', microtime(true));

// 使用:LARAVEL_START
  • 列舉的定義
<?php

namespace App\Repositories\Enums;

class ResponseCodeEnum
{
    // 業務操作正確碼
    const SERVICE_REGISTER_SUCCESS = 200101;
    const SERVICE_LOGIN_SUCCESS = 200102;

    // 業務操作錯誤碼(外部服務或內部服務呼叫...)
    const SERVICE_REGISTER_ERROR = 500101;
    const SERVICE_LOGIN_ERROR = 500102;
}

// 使用:ResponseCodeEnum::SERVICE_LOGIN_SUCCESS

問題

實際開發中,像上面的定義和使用想必都已非常熟練,但隨著專案業務規模起來以後,如何來管理這些分散在不同檔案中定義的列舉?如何讓這些列舉定義更加規範?如何更充分地發揮列舉定義的好處呢?

實現過程

思路

  • 儘可能地遵循 Laravel 思維進行擴充套件,符合一定規範
  • 儘量少的依賴安裝,最好是 0 依賴,不額外增加負擔
  • 儘量完善的單元測試,保證程式碼質量(關於使用示例可以跳過下面的介紹直接檢視 github.com/Jiannei/laravel-enum/tr... 測試用例)
  • 實現需要簡潔,使用需要優雅
  • 方便擴充套件

規範

  • 列舉的定義需要統一到一個固定目錄,比如在 lumen-api-starter 中, 統一在app/Repositories/Enums 中進行定義;列舉可以視為資料層 Repository 中的靜態資料的部分。
  • 名稱使用全大寫英文字母,可以使用下劃線區分層級
  • 多語言描述在 resources/lang 中進行定義,比如前篇文章中關於響應碼的描述定義 resources/lang/zh-CN/enums.php
<?php

// resources/lang/zh-CN/enums.php
use App\Repositories\Enums\ResponseCodeEnum;

return [
    // 響應狀態碼
    ResponseCodeEnum::class => [
        // 成功
        ResponseCodeEnum::HTTP_OK => '操作成功', // 自定義 HTTP 狀態碼返回訊息
        ResponseCodeEnum::HTTP_UNAUTHORIZED => '授權失敗',
    ],
];

功能

以下均同時支援 Laravel 和 Lumen:

  • 提供豐富的方式來例項化列舉
  • 支援多語言本地化描述
  • 支援表單驗證列舉,提供驗證規則 enum,enum_key 和 enum_value,對請求引數中的引數進行列舉校驗
  • 支援中介軟體自動將引數轉換成相應列舉例項
  • 支援 Eloquent\Model 中的 $casts 特性,將查詢出的資料自動轉換成列舉例項
  • 提供便捷的方法對列舉進行校驗
  • 內建了標準的 Http 狀態碼列舉定義,方便在 API 返回響應資料時設定 Http 狀態碼;CacheEnum 定義,一種統一專案中快取 key 和快取時效定義的方案。

補充:Http 狀態碼列舉,原先是定義在 laravel-response 中,為了簡化,遷移到了 laravel-enum 中。

實現

laravel-enum 的核心程式碼如下:github.com/Jiannei/laravel-enum/bl...

protected static function getConstants(): array
{
    $calledClass = static::class;

    if (! array_key_exists($calledClass, static::$cache)) {
        $reflect = new ReflectionClass($calledClass);
        static::$cache[$calledClass] = $reflect->getConstants();
    }

    return static::$cache[$calledClass];
}

需要關注的是new ReflectionClass($calledClass),利用 ReflectionClass 中的 getConstants 的方法,將原先定義在 Class 中的列舉以陣列形式提取出來,後續都是在此基礎上進行擴充套件,是反射機制的一種實際應用。

PHP 反射介紹:www.php.net/manual/zh/class.reflec...

使用示例

更為具體的使用可以檢視測試用例:github.com/Jiannei/laravel-enum/tr...

常規使用

  • 定義
<?php

namespace App\Repositories\Enums;

use Jiannei\Enum\Laravel\Enum;

use Jiannei\Enum\Laravel\Enum;

final class UserTypeEnum extends Enum
{
    const ADMINISTRATOR = 0;

    const MODERATOR = 1;

    const SUBSCRIBER = 2;

    const SUPER_ADMINISTRATOR = 3;
}
  • 使用
// 獲取常量的值
UserTypeEnum::ADMINISTRATOR;// 0

// 獲取所有已定義常量的名稱
$keys = UserTypeEnum::getKeys();// ['ADMINISTRATOR', 'MODERATOR', 'SUBSCRIBER', 'SUPER_ADMINISTRATOR']

// 根據常量的值獲取常量的名稱
UserTypeEnum::getKey(1);// MODERATOR

// 獲取所有已定義常量的值
$values = UserTypeEnum::getValues();// [0, 1, 2, 3]

// 根據常量的名稱獲取常量的值
UserTypeEnum::getValue('MODERATOR');// 1
  • 本地化描述

// 1. 不存在語言包的情況,返回較為友好的英文描述
UserTypeEnum::getDescription(UserTypeEnum::ADMINISTRATOR);// Administrator

// 2. 在 resource/lang/zh-CN/enums.php 中定義常量與描述的對應關係(enums.php 檔名稱可以在 config/enum.php 檔案中配置)
ExampleEnum::getDescription(ExampleEnum::ADMINISTRATOR);// 管理員

// 補充:也可以先例項化常量物件,然後再根據物件例項來獲取常量描述
$responseEnum = new ExampleEnum(ExampleEnum::ADMINISTRATOR);
$responseEnum->description;// 管理員

// 其他方式
ExampleEnum::ADMINISTRATOR()->description;// 管理員
  • 列舉校驗
// 檢查定義的常量中是否包含某個「常量值」
UserTypeEnum::hasValue(1);// true
UserTypeEnum::hasValue(-1);// false

// 檢查定義的常量中是否包含某個「常量名稱」 

UserTypeEnum::hasKey('MODERATOR');// true
UserTypeEnum::hasKey('ADMIN');// false
  • 列舉例項化:列舉例項化以後可以方便地通過物件例項訪問列舉的 key、value 以及 description 屬性的值。
// 方式一:new 傳入常量的值
$administrator1 = new UserTypeEnum(UserTypeEnum::ADMINISTRATOR);

// 方式二:fromValue
$administrator2 = UserTypeEnum::fromValue(0);

// 方式三:fromKey
$administrator3 = UserTypeEnum::fromKey('ADMINISTRATOR');

// 方式四:magic
$administrator4 = UserTypeEnum::ADMINISTRATOR();

// 方式五:make,嘗試根據「常量的值」或「常量的名稱」例項化物件常量,例項失敗時返回原先傳入的值
$administrator5 = UserTypeEnum::make(0); // 此處嘗試根據「常量的值」例項化
$administrator6 = UserTypeEnum::make('ADMINISTRATOR'); // 此處嘗試根據「常量的名稱」例項化
  • 列舉例項化進階:(TransfrormEnums 中介軟體自動轉換請求引數為列舉例項,便是用的下面的 make 方法)
$administrator2 = UserTypeEnum::make('ADMINISTRATOR');// strict 預設為 true;準備被例項化

$administrator3 = UserTypeEnum::make(0);// strict 預設為 true;準備被例項化

// 注意:這裡的 0 是字串型別,而原先定義的是數值型別
$administrator4 = UserTypeEnum::make('0', false); // strict 設定為 false,不校驗傳入值的型別;會被準確例項化

// 注意:這裡的 AdminiStrator 是大小寫混亂的
$administrator6 = UserTypeEnum::make('AdminiStrator', false); // strict 設定為 false,不校驗傳入值的大小寫;會被準確例項化
  • 隨機獲取
// 隨機獲取一個常量的值
UserTypeEnum::getRandomValue();

// 隨機獲取一個常量的名稱
UserTypeEnum::getRandomKey();

// 隨機獲取一個列舉例項
UserTypeEnum::getRandomInstance()
  • toArray
$array = UserTypeEnum::toArray();

/*
[
    'ADMINISTRATOR' => 0,
    'MODERATOR' => 1,
    'SUBSCRIBER' => 2,
    'SUPER_ADMINISTRATOR' => 3,
]
*/
  • toSelectArray
$array = UserTypeEnum::toSelectArray();// 支援多語言配置

/*
[
    0 => '管理員',
    1 => '監督員',
    2 => '訂閱使用者',
    3 => '超級管理員',
]
*/

列舉轉換和校驗

這一部分通過一個需求場景來描述:使用者登入 API 需要校驗傳入的 identity_type 是否合法,並且根據不同的值呼叫不同的登入邏輯。

  • 定義 IdentityTypeEnum
<?php

namespace App\Repositories\Enums;

use Jiannei\Enum\Laravel\Contracts\LocalizedEnumContract;
use Jiannei\Enum\Laravel\Enum;

class IdentityTypeEnum extends Enum implements LocalizedEnumContract
{
    const NAME = 1;
    const EMAIL = 2;
    const PHONE = 3;
    const GITHUB = 4;
    const WECHAT = 5;
}
  • app/Http/Kernel.php 中新增路由中介軟體
protected $routeMiddleware = [

        // ...
    'enum' => \Jiannei\Enum\Laravel\Http\Middleware\TransformEnums::class
];
  • config/enum.php 中配置 Request 引數列舉之間的轉換關係:引數 ⇒ 列舉
<?php

use App\Repositories\Enums\IdentityTypeEnum;

return [
    'localization' => [
        'key' => env('ENUM_LOCALIZATION_KEY', 'enums'),
    ],

    // 你可以將請求引數中用到的列舉定義在下面,通過中介軟體,將會被自動轉換成列舉類
    'transformations' => [
        // 引數名 => 對應的列舉類
        'identity_type' => IdentityTypeEnum::class,
    ],
];
  • Controller 中使用
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Repositories\Enums\IdentityTypeEnum;
use App\Services\AuthorizationService;
use Illuminate\Http\Request;
use Jiannei\Response\Laravel\Support\Facades\Response;

class AuthorizationController extends Controller
{
    private AuthorizationService $service;

    public function __construct(AuthorizationService $service)
    {
        $this->middleware('auth:api', ['except' => ['store',]]);
        $this->middleware('enum:false');// 請求引數中包含列舉時轉換過程中不區分大小寫

        $this->service = $service;
    }

        // 假設客戶端 POST 方式傳入下面引數:(注意,這裡 identity_type 傳入的值是 email)

        /*
        {
            "identity_type":"email",// 對應 IdentityTypeEnum 中的 EMAIL,這裡為小寫形式
            "account":"longjian.huang@foxmail.com",
            "password":"password",
            "remember":false
        }
        */
    public function store(Request $request)
    {
        $this->validate($request, [
           'identity_type' => 'required|enum:'.IdentityTypeEnum::class,// 校驗傳入的 identity_type 是否能夠被例項化成列舉 
                'account' => 'required|string|max:64|unique:users,account', // 賬號
                'password' => 'required|min:8', // 密碼
                'remember' => 'boolean', // 記住我
        ]);

                // identity_type 為 github 時走 Github 登入
                // $request->get('identity_type') 為 IdentityTypeEnum 例項,可以呼叫物件中的方法
        if ($request->get('identity_type')->is(IdentityTypeEnum::GITHUB)) {
            $token = $this->service->handleGithubLogin($request->all());
        }else{
            $token = $this->service->handleLogin($request->all());
        }

        return Response::created($token);
    }
}

說明:

  • 擴充套件了驗證規則 enum、enum_key、enum_value,可以對 Request 中的引數 identity_type 進行校驗
  • 引入了 \Jiannei\Enum\Laravel\Http\Middleware\TransformEnums 到路由中介軟體中;
  • 在 Controller 中以 $this->middleware('enum:false'); 形式使用TransformEnums 中介軟體,並且向中介軟體傳入了 false 引數,對應上面的UserTypeEnum::make('AdminiStrator', false); 將不會對列舉引數進行大小寫和型別校驗
  • $request->get('identity_type') 獲取到的是 IdentityTypeEnum 例項,Enum 例項中提供了 isisNotin 共 3 種列舉例項之間的比較方法

Model 中的列舉轉換

為了實現上面的多賬號型別登入,account 資料表中就需要有一個欄位 identity_type 來描述賬號型別。identity_type 是定義成整型?字串型別?還是 Enum 型別呢?

Laravel 的 Eloquent\Model 提供了 $casts 特性,可以查詢出來的資料欄位轉換成指定型別。這裡也可以利用這個特性,將 account 表中的 identity_type 轉換成 IdentityTypeEnum 例項

<?php

namespace App\Repositories\Models\MySql;

use App\Repositories\Enums\IdentityTypeEnum;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{

    protected $casts = [
        'identity_type' => IdentityTypeEnum::class
    ];
}

One more thing?

有了上面的基礎,我們可以繼續擴充套件。laravel-enum 中內建了 HttpStatusCodeEnum、CacheEnum 以及 LogEnum 3 個列舉集合。

  • HttpStatusCodeEnum 為原先 laravel-response 中使用到的 ResponseCodeEnum,用來規範響應資料中的操作碼部分,現已遷移到了 laravel-enum
  • CacheEnum,用來規範專案中快取 key 的定義以及快取時間的定義;
  • LogEnum ,用來規範專案中日誌記錄時的名稱定義,進一步方便日誌排查;(後面講 laravel-logger 時會用到)

快取列舉

為了提高應用效能,在應用中使用快取是很常見的一種做法。但是隨著業務到達一定規模後,分散在各處的快取使用,由於定義快取 key 的方式不統一,快取生效時間無法確認,導致快取維護起來較為困難。

可以直接檢視原始碼進行了解:github.com/Jiannei/laravel-enum/bl...

lumen-api-statrer 中的實際使用例項:

  • 定義:通過下面的方式將專案中所有快取 key 和過期時間的定義統一到 CacheEnum 中,從而可以很直觀的看出專案哪些地方使用了快取,快取多久失效。
<?php

namespace App\Repositories\Enums;

use Illuminate\Support\Carbon;
use Jiannei\Enum\Laravel\Repositories\Enums\CacheEnum as BaseCacheEnum;

class CacheEnum extends BaseCacheEnum
{
        // key => 過期時間計算方法
    // 警告:方法名不能相同
    const AUTHORIZATION_USER = 'authorizationUser';// 將呼叫下面定義的 authorizationUser 方法獲取快取過期時間

        // ...

        // 授權使用者資訊 將在 Jwt token 過期時一同失效
    protected static function authorizationUser($options)
    {
        $exp = auth('api')->payload()->get('exp'); // token 剩餘有效時間

        return Carbon::now()->diffInSeconds(Carbon::createFromTimestamp($exp));
    }

        // ...
}
  • 使用
// app/Providers/EloquentUserProvider.php

public function retrieveById($identifier)
{
        // 獲取授權使用者的快取 key:類似於 lumen_cache:authorization:user:11
    $cacheKey = CacheEnum::getCacheKey(CacheEnum::AUTHORIZATION_USER,$identifier);
    // 獲取快取使用者快取的過期時間
        $cacheExpireTime = CacheEnum::getCacheExpireTime(CacheEnum::AUTHORIZATION_USER);

    return Cache::remember($cacheKey, $cacheExpireTime, function () use ($identifier) {
        $model = $this->createModel();

        return $this->newModelQuery($model)
            ->where($model->getAuthIdentifierName(), $identifier)
            ->first();
    });
}
  • 可以通過工具看一下快取實際儲存的 key 和 value

教你更優雅地寫 API  之「列舉使用」

特別說明

  • BenSampo/laravel-enum:支援列舉定義、提供各種實用的列舉校驗和轉換,但是缺少中介軟體轉換,不支援鬆散的 Request 引數轉列舉例項
  • spatie/laravel-enum:支援中介軟體轉換列舉,但是實現和使用較為複雜

laravel-enum 在2 個 package 功能的基礎上,進行了合併,擴充套件增加了鬆散的(不區分大小寫和資料型別) Request 引數轉列舉例項,內建提供了 HttpStatusCodeEnum 和 CacheEnum 等實用列舉集合。

其他

依照慣例,如果對您的日常工作有所幫助或啟發,歡迎三連 star + fork + follow

如果有任何批評建議,通過郵箱(longjian.huang@foxmial.com)的方式可以聯絡到我。

總之,歡迎各路英雄好漢。

QQ 群:1105120693

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Peace & Love ❤️

相關文章