教你更优雅地写 API 之「枚举使用」

教你更优雅地写 API  之「枚举使用」

前言

在上一篇 教你更优雅地写 API 之「规范响应数据」中,定义响应操作码时就已经用到枚举,通过定义 ResponseCodeEnum可以很方便地规范操作码的定义,以及实现多语言的操作码描述。本篇是基于前面文章的基础上继续深入讨论项目中枚举的使用。

Package 地址:larave-enum

Laravel 版本 Api 开发初始化项目:laravel-api-starter

Lumen 版本 Api 开发初始化项目:lumen-api-starter

更新

  • 2021-03-03:zh-CN 调整为 zh_CN,保持与 laravel-lang 命名一致

定义

引入维基百科的一段描述:枚举

枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,例如表示星期的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,对请求参数中的参数进行枚举校验
  • 支持路由中间件自动将 Request 参数转换成相应枚举实例
  • 支持 Eloquent\Model 中的 $casts 特性,将查询出的数据自动转换成枚举实例
  • 提供了便捷的比较方法isisNotin,用于枚举实例之间的对比
  • 内置了多种实用的枚举集:
    • 标准的 Http 状态码枚举定义,方便在 API 返回响应数据时设置 Http 状态码;
    • CacheEnum 缓存枚举定义,一种统一项目中缓存 key 和缓存过期时间定义的方案;
    • LogEnum 日志枚举定义,用于规范日志记录时的描述内容

补充:Http 状态码枚举,原先是定义在 laravel-response 中,为了简化 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 来描述账号类型。

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  之「枚举使用」

特别说明

laravel-enum 在下面2 个 package 功能的基础上,扩展增加了松散的(不区分大小写和数据类型) Request 参数转枚举实例,并内置提供了 HttpStatusCodeEnum 和 CacheEnum 等实用枚举集合。

  • BenSampo/laravel-enum:支持枚举定义、提供各种实用的枚举校验和转换,但是缺少中间件转换,不支持松散的 Request 参数转枚举实例
  • spatie/laravel-enum:支持中间件转换枚举,但是功能实现和使用较为复杂

其他

依照惯例,如果对您的日常工作有所帮助或启发,欢迎三连 star + fork + follow

如果有任何批评建议,通过邮箱(longjian.huang@foxmail.com)的方式可以联系到我。

总之,欢迎各路英雄好汉。

QQ 群:1105120693

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
Jianne
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 9
xiaoAgiao

点赞,写的很用心啊 想请问一下封面图是用啥做的呀 :+1:

3年前 评论
Jianne

@xiaoAgiao 用的这个 carbon.now.sh/

3年前 评论
xiaoAgiao 3年前

跟 BenSampo/laravel-enum 很像

3年前 评论
Jianne

@wind 有过特别说明,是 BenSampo/laravel-enumspatie/laravel-enum 的整合版

3年前 评论
Jianne

@Adminwj 需要配置 APP_LOCALE 为 zh_CN

3年前 评论
kenvent 3年前

反馈下lumen 注册服务容器的时候会报下面这个错

file

file

3年前 评论
kenvent (作者) 3年前
kenvent (作者) 3年前

您好,支持 php artisan make:enum 之类的快捷工具么?

2年前 评论
Jianne

@fabulous 暂不支持

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!