Laravel 开发Api规范

一,修改时区

配置 config/app.php 文件

php 复制代码
// 时区修改,感觉两者皆可,自己根据实际情况定义
'timezone' => 'PRC', // 大陆时间

二,设置 Accept 头中间件

accept头即为客户端请求头,做成中间件来使用。Accept 决定了响应返回的格式,设置为 application/json, 遇到的所有报错 Laravel 会默认处理为 JSON 格式。

  1. 生成中间件
php 复制代码
php artisan make:middleware AcceptHeader
php 复制代码
<?php
namespace App\Http\Middleware;
use Closure;
class AcceptHeader
{
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }
}
  1. 添加到全局中间件
    修改文件 app/http/Kernel.php
php 复制代码
protected $middlewareGroups = [
    'web' => [
        ...
    ],
    'api' => [
        \App\Http\Middleware\AcceptHeader::class,
        ...
    ],
];

二,统一封装响应码

Enum 枚举,新建app/Enums/ResponseEnum.php

php 复制代码
<?php

namespace App\Enums;

class ResponseEnum
{
    // sevming/laravel-response 默认以 '|' 作为分割错误码与错误信息的字符串
    public const INVALID_REQUEST = '无效请求|21001';
    // 001 ~ 099 表示系统状态;100 ~ 199 表示授权业务;200 ~ 299 表示用户业务

    /*-------------------------------------------------------------------------------------------*/
    // 100开头的表示 信息提示,这类状态表示临时的响应
    // 100 - 继续
    // 101 - 切换协议


    /*-------------------------------------------------------------------------------------------*/
    // 200表示服务器成功地接受了客户端请求
    const HTTP_OK = [200001, '操作成功'];
    const HTTP_ERROR = [200002, '操作失败'];
    const HTTP_ACTION_COUNT_ERROR = [200302, '操作频繁'];
    const USER_SERVICE_LOGIN_SUCCESS = [200200, '登录成功'];
    const USER_SERVICE_LOGIN_ERROR = [200201, '登录失败'];
    const USER_SERVICE_LOGOUT_SUCCESS = [200202, '退出登录成功'];
    const USER_SERVICE_LOGOUT_ERROR = [200203, '退出登录失败'];
    const USER_SERVICE_REGISTER_SUCCESS = [200104, '注册成功'];
    const USER_SERVICE_REGISTER_ERROR = [200105, '注册失败'];
    const USER_ACCOUNT_REGISTERED = [23001, '账号已注册'];


    /*-------------------------------------------------------------------------------------------*/
    // 300开头的表示服务器重定向,指向的别的地方,客户端浏览器必须采取更多操作来实现请求
    // 302 - 对象已移动。
    // 304 - 未修改。
    // 307 - 临时重定向。


    /*-------------------------------------------------------------------------------------------*/
    // 400开头的表示客户端错误请求错误,请求不到数据,或者找不到等等
    // 400 - 错误的请求
    const CLIENT_NOT_FOUND_HTTP_ERROR = [400001, '请求失败'];
    const CLIENT_PARAMETER_ERROR = [400200, '参数错误'];
    const CLIENT_CREATED_ERROR = [400201, '数据已存在'];
    const CLIENT_DELETED_ERROR = [400202, '数据不存在'];
    // 401 - 访问被拒绝
    const CLIENT_HTTP_UNAUTHORIZED = [401001, '授权失败,请先登录'];
    const CLIENT_HTTP_UNAUTHORIZED_EXPIRED = [401200, '账号信息已过期,请重新登录'];
    const CLIENT_HTTP_UNAUTHORIZED_BLACKLISTED = [401201, '账号在其他设备登录,请重新登录'];
    // 403 - 禁止访问
    // 404 - 没有找到文件或目录
    const CLIENT_NOT_FOUND_ERROR = [404001, '没有找到该页面'];
    // 405 - 用来访问本页面的 HTTP 谓词不被允许(方法不被允许)
    const CLIENT_METHOD_HTTP_TYPE_ERROR = [405001, 'HTTP请求类型错误'];
    // 406 - 客户端浏览器不接受所请求页面的 MIME 类型
    // 407 - 要求进行代理身份验证
    // 412 - 前提条件失败
    // 413 -- 请求实体太大
    // 414 - 请求 URI 太长
    // 415 -- 不支持的媒体类型
    // 416 -- 所请求的范围无法满足
    // 417 -- 执行失败
    // 423 -- 锁定的错误


    /*-------------------------------------------------------------------------------------------*/
    // 500开头的表示服务器错误,服务器因为代码,或者什么原因终止运行
    // 服务端操作错误码:500 ~ 599 开头,后拼接 3 位
    // 500 - 内部服务器错误
    const SYSTEM_ERROR = [500001, '服务器错误'];
    const SYSTEM_UNAVAILABLE = [500002, '服务器正在维护,暂不可用'];
    const SYSTEM_CACHE_CONFIG_ERROR = [500003, '缓存配置错误'];
    const SYSTEM_CACHE_MISSED_ERROR = [500004, '缓存未命中'];
    const SYSTEM_CONFIG_ERROR = [500005, '系统配置错误'];

    // 业务操作错误码(外部服务或内部服务调用)
    const SERVICE_REGISTER_ERROR = [500101, '注册失败'];
    const SERVICE_LOGIN_ERROR = [500102, '登录失败'];
    const SERVICE_LOGIN_ACCOUNT_ERROR = [500103, '账号或密码错误'];
    const SERVICE_USER_INTEGRAL_ERROR = [500200, '积分不足'];

    //501 - 页眉值指定了未实现的配置
    //502 - Web 服务器用作网关或代理服务器时收到了无效响应
    //503 - 服务不可用。这个错误代码为 IIS 6.0 所专用
    //504 - 网关超时
    //505 - HTTP 版本不受支持
    /*-------------------------------------------------------------------------------------------*/
}

三,封装 API 返回的统一消息(ApiResponse)

在 app/Helpers 目录下创建 ApiResponse.php 文件

php 复制代码
<?php

namespace App\Helpers;

use App\Enum\ResponseEnum;
use App\Exceptions\BaseException;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;


trait ApiResponse
{
    /**
     * 成功
     * @param null $data
     * @param array $codeResponse
     * @return JsonResponse
     */
    public function success($data = null, $codeResponse = ResponseEnum::HTTP_OK): JsonResponse
    {
        return $this->jsonResponse('success', $codeResponse, $data);
    }

    /**
     * 失败
     * @param array $codeResponse
     * @param null $data
     * @param null $error
     * @return JsonResponse
     */
    public function fail($codeResponse = ResponseEnum::HTTP_ERROR, $data = null): JsonResponse
    {
        return $this->jsonResponse('fail', $codeResponse, $data);
    }

    /**
     * json响应
     * @param $status
     * @param $codeResponse
     * @param $data
     * @param $error
     * @return JsonResponse
     */
    private function jsonResponse($status, $codeResponse, $data): JsonResponse
    {
        list($code, $message) = $codeResponse;
        return response()->json([
            'status' => $status,
            'code' => $code,
            'message' => $message,
            'data' => $data ?? null,
        ]);
    }


    /**
     * 成功分页返回
     * @param $page
     * @return JsonResponse
     */
    protected function successPaginate($page): JsonResponse
    {
        return $this->success($this->paginate($page));
    }

    private function paginate($page)
    {
        if ($page instanceof LengthAwarePaginator) {
            return [
                'total' => $page->total(),
                'page' => $page->currentPage(),
                'limit' => $page->perPage(),
                'pages' => $page->lastPage(),
                'list' => $page->items()
            ];
        }
        if ($page instanceof Collection) {
            $page = $page->toArray();
        }
        if (!is_array($page)) {
            return $page;
        }
        $total = count($page);
        return [
            'total' => $total, //数据总数
            'page' => 1, // 当前页码
            'limit' => $total, // 每页的数据条数
            'pages' => 1, // 最后一页的页码
            'list' => $page // 数据
        ];
    }

    /**
     * 业务异常返回
     * @param array $codeResponse
     * @param string $info
     * @throws BaseException
     */
    public function throwBaseException(array $codeResponse = ResponseEnum::HTTP_ERROR, string $info = '')
    {
        throw new BaseException($codeResponse, $info);
    }
}

四,创建项目异常捕获 Exception 文件

异常分为两种,一种是要给前端返回展示的,比如表单验证,一种是不需要给前端展示的,比如服务器内部错误。

  1. 在 app/Exceptions 目录下创建 BaseException.php 文件用于服务器内部异常的抛出
php 复制代码
<?php

namespace App\Exceptions;

use Exception;
class BaseException extends Exception
{
    /**
     * 基础异常构造函数
     * @param array $codeResponse 状态码
     * @param string $info 自定义返回信息,不为空时会替换掉codeResponse 里面的message文字信息
     */
    public function __construct(array $codeResponse, $info = '')
    {
        [$code, $message] = $codeResponse;
        parent::__construct($info ?: $message, $code);
    }
}
  1. 自定义返回异常
    修改 app/Exceptions 目录下的 Handler.php 文件
php 复制代码
<?php

namespace App\Exceptions;

use App\Enum\ResponseEnum;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Throwable;
use App\Helpers\ApiResponse;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class Handler extends ExceptionHandler
{
    use ApiResponse;

    /**
     * A list of exception types with their corresponding custom log levels.
     *
     * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
     */
    protected $levels = [
        //
    ];

    /**
     * A list of the exception types that are not reported.
     *
     * @var array<int, class-string<\Throwable>>
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }


    public function render($request, Throwable $exception)
    {   
        //判断只接管api部分的异常
        if ($request->is('api/*')) {
            // 请求类型错误异常抛出
            if ($exception instanceof MethodNotAllowedHttpException) {
                $this->throwBaseException(ResponseEnum::CLIENT_METHOD_HTTP_TYPE_ERROR);
            } // 参数校验错误异常抛出
            elseif ($exception instanceof ValidationException) {
                $this->throwBaseException(ResponseEnum::CLIENT_PARAMETER_ERROR, $exception->getMessage());
            } // 路由不存在异常抛出
            elseif ($exception instanceof NotFoundHttpException) {
                $this->throwBaseException(ResponseEnum::CLIENT_NOT_FOUND_ERROR);
            } // 自定义错误异常抛出
            elseif ($exception instanceof BaseException) {
                return response()->json([
                    'status' => 'fail',
                    'code' => $exception->getCode(),
                    'message' => $exception->getMessage(),
                    'data' => null,
                ]);
                //系统异常
            } else {
                //生产模式
                if (config('app.debug')) {
                    return response()->json([
                        'status' => 'fail',
                        'code' => 500,
                        'message' => "服务器内部错误!",
                        'data' => null,
                    ]);
                } else {
                    return response()->json([
                        'status' => 'fail',
                        'code' => 500,
                        'message' => $exception->getMessage(),
                        'data' => null
                    ]);
                }

            }
        }

        return parent::render($request, $exception);

    }
}

五,使用

路由定义如下

php 复制代码
Route::prefix('v1')->name('api.v1.')->group(function() {
    Route::get('version', function() {
        return 'this is version v1';
    })->name('version');
    
    //限速,每分钟只能请求10次
    Route::get('test', [TestController::class,'test'])->middleware(['throttle:10,1']);
    
    Route::post('login', [TestController::class,'login']);
    Route::post('register', [TestController::class,'register']);
    Route::group(['middleware' => 'auth.jwt'], function () {
        Route::get('user', [TestController::class,'user']);
        Route::get('logout', [TestController::class,'logout']);
        Route::get('refresh', [TestController::class,'refresh']);
    });
});
相关推荐
ServBay2 天前
告别面条代码,PSL 5.0 重构 PHP 性能与安全天花板
后端·php
多厘3 天前
别再手写 psr-4 了!用 Composer 隐藏魔法干掉上千行烂配置
laravel
JaguarJack5 天前
FrankenPHP 原生支持 Windows 了
后端·php·服务端
BingoGo5 天前
FrankenPHP 原生支持 Windows 了
后端·php
JaguarJack6 天前
PHP 的异步编程 该怎么选择
后端·php·服务端
BingoGo6 天前
PHP 的异步编程 该怎么选择
后端·php
JaguarJack6 天前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay7 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954487 天前
CTF 伪协议
php
BingoGo10 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php