用 PHP 8 里的枚举特性来优化 API 错误码管理

​在使用 PHP 开发 API 时,经常会遇到需要返回错误信息的场景。前端根据返回的错误码,再进行相应的操作。

发现问题

这里用 ThinkPHP8 举个例子,这是一个用户登录接口的例子:

public function login()
{
    $username = $this->request->param('username');
    $password = $this->request->param('password');

    if (!$username || !$password) {
        $result = ['code' => 1001, 'msg' => '参数不完整'];
    } else if ($username !== 'admin') {
        $result = ['code' => 1002, 'msg' => '用户不存在'];
    } else if ($password !== '123456') {
        $result = ['code' => 1003, 'msg' => '密码错误'];
    } else {
        $result = ['code' => 1, 'msg' => '登录成功'];
    }

    return json($result);
}

上面的代码里,对提交的参数进行一些简单的判断,定义了 $result 数组,并为每个不同的错误都提供了一个错误码和提示信息。最后返回给前端。为了让前端能理解错误码,后端开发人员需要把上面的 code 和 msg 都写在 api 文档的错误码里,以供前端参考。

不过大家是否觉得这种直接在代码里零散地写 code 和 msg 不够直观和优雅(在实际开发过程中,不大可能会如此集中地显示错误信息),而且容易不小心把 code 写错或者重复?或许我们可以换一种方式来表达错误码。

解决方案

我先把调整后的代码贴出:

public function login()
{
    $username = $this->request->param('username');
    $password = $this->request->param('password');

    if (!$username || !$password) {
        $status = LoginStatus::PARAM_INCOMPLETE;
    } else if ($username !== 'admin') {
        $status = LoginStatus::USER_NOT_FOUND;
    } else if ($password !== '123456') {
        $status = LoginStatus::PASSWORD_WRONG;
    } else {
        $status = LoginStatus::SUCCESS;
    }

    return json($status->apiResult());
}

上面代码没有了 code 和 msg 信息,但是却不难理解 $status 所表达的含义,让代码看上去清爽、直观很多。

要实现上面的代码,我们使用到了2个 PHP 8 里的新特性,1、枚举(php8.1 新增),2、注解。下面看下是如何实现上面的代码效果的。

首先看一下枚举类型 LoginStatus 的完整代码:

<?php
namespace app\enum;

use app\attribute\Description;

enum LoginStatus: int
{
    #[Description('登录成功')]
    case SUCCESS = 1;

    #[Description('参数不完整')]
    case PARAM_INCOMPLETE = 1001;

    #[Description('用户不存在')]
    case USER_NOT_FOUND = 1002;

    #[Description('密码错误')]
    case PASSWORD_WRONG = 1003;

    /**
     * 获取当前枚举的描述
     * @return string
     */
    public function getDescription(): string
    {
        $reflectionClass = new \ReflectionClass($this);
        return $reflectionClass->getReflectionConstant($this->name)->getAttributes()[0]->getArguments()[0];
    }

    /**
     * 返回API接口所需的数据
     *
     * @return array
     */
    public function apiResult()
    {
        return [
            'code' => $this->value, // 状态码
            'msg' => $this->getDescription() // 描述
        ];
    }

}

可以看到 LoginStatus 里定义了4个枚举值,用来表示登录时会出现的错误代码,通过集中管理错误代码,可以让编码显得直观,也避免一些 code 出现重复的问题。

同时利用注解特性,增加自定义注解 Description ,用以描述枚举值的含义,通过 getDescription 方法就可以获取枚举值对应的描述。

apiResult 方法则用来返回接口所需要的 code 和 msg 信息。

自定义注解类  Description 的代码如下:

<?php
namespace app\attribute;

class Description
{
}

由于我们并没有实例化这个注解类,而是通过反射类里的信息获取注解内容,所以里面并没有任何代码,只做了类的定义。

继续优化

我们可以把 getDescription 和 apiResult 提取出来,放到 trait 中,作为公共方法(也可以封装成父级枚举类型)。这样可以用在更多不同的枚举类型上。以下是一个示例:

<?php
namespace app\traits;

trait EnumHelper
{
    /**
     * 获取当前枚举的描述
     * @return string
     */
    public function getDescription(): string
    {
        $reflectionClass = new \ReflectionClass($this);
        return $reflectionClass->getReflectionConstant($this->name)->getAttributes()[0]->getArguments()[0];
    }

    /**
     * 返回 API 接口所需的数据
     * @return array
     */
    public function apiResult(): array
    {
        return [
            'code' => $this->value,
            'msg' => $this->getDescription()
        ];
    }
}

然后我们在 LoginStatus 枚举中使用这个 trait:

<?php
namespace app\enum;

use app\attribute\Description;
use app\traits\EnumHelper;

enum LoginStatus: int
{
    use EnumHelper;

    #[Description('登录成功')]
    case SUCCESS = 1;

    #[Description('参数不完整')]
    case PARAM_INCOMPLETE = 1001;

    #[Description('用户不存在')]
    case USER_NOT_FOUND = 1002;

    #[Description('密码错误')]
    case PASSWORD_WRONG = 1003;
}

这样我们可以将 EnumHelper trait 重用到其他枚举类型中,提供一致的描述和 API 返回格式。

文章总结

通过使用 PHP8 的枚举和注解特性,我们可以更加优雅地处理 API 中的错误码和错误信息。相比于传统的在代码中直接写 code 和 msg,这种方式更加直观、清晰,并且可以集中管理错误代码,减少重复和错误的可能性。

利用 PHP8 的新特性,我们能够编写出更加优雅和高效的代码,提高整个项目的可维护性和可读性。希望通过这篇文章,能够给大家带来一些新的思路和启发。在实际项目中,可以根据具体需求进一步扩展和优化代码。 ​