博客 / 詳情

返回

是時候使用 Lumen 7 + API Resource 開發項目了!

使用 Lumen 7 + Api Resource  進行 API 開發

寫在前面

工作中使用 Laravel 開發 API 項目已經有些年頭了,發現每次啓動新的 Api 項目的時都會在 Laravel 基礎上進行一些預處理,包括針對 API 項目的結構設計,統一響應結構的封裝,異常的捕獲處理以及授權模塊的配置等。總是在做一些重複的工作,那索性將這些常用的基礎封裝做成一個「啓動模板」好了。

項目地址:戳這兒

更新內容

  • 實現了根據實際業務場景自定義響應碼和多語言的響應描述,目錄結構有參考 [@mingzaily](https://learnku.com/users/34707) 同學的實現(2020-06-05)
  • 更好的支持 Api Resource:使用with()$additonal 附加額外數據,響應返回前使用withResponse()對響應進行處理(2020-06-04)
  • 補充響應成功時的使用示例(2020-06-03)
  • 規範 Reponse 中的 message 提示(2020-06-02)

為什麼是 Lumen ?

現如今,中大型項目中通常使用前後端分離方式開發,前後端分別通過不同的代碼倉庫各自維護着項目代碼,Laravel 只負責項目中的 API 部分,提供 API 給前端調用。這種場景下,使用 Laravel 進行開發 API 稍微顯得有點“臃腫”了。

相比之下,Lumen 針對項目中的 API 開發場景,精簡了Laravel 中的很多部分,更適合 API 開發。有了 Laravel 使用經驗,切換到 Lumen 也較為容易。

概覽

  • 適配 Laravel 7 中新增的 HttpClient 客户端
  • 使用 Laravel 原生的 Api Resource
  • 規範統一的響應結構
  • 使用 Jwt-auth 方式授權
  • 支持日誌記錄到 MongoDB
  • 合理有效地『Repository & Service』架構設計(?)

規範的響應結構

摘選自:RESTful 服務最佳實踐

code——包含一個整數類型的HTTP響應狀態碼。
status——包含文本:"success","fail"或"error"。HTTP狀態響應碼在500-599之間為"fail",在400-499之間為"error",其它均為"success"(例如:響應狀態碼為1XX、2XX和3XX)。
message——當狀態值為"fail"和"error"時有效,用於顯示錯誤信息。參照國際化(il8n)標準,它可以包含信息號或者編碼,可以只包含其中一個,或者同時包含並用分隔符隔開。
data——包含響應的body。當狀態值為"fail"或"error"時,data僅包含錯誤原因或異常名稱。

説明

整體響應結構設計參考如上,相對嚴格地遵守了 RESTful 設計準則,返回合理的 HTTP 狀態碼。

考慮到業務通常需要返回不同的“業務描述處理結果”,在所有響應結構中都支持傳入符合業務場景的message

  • data:

    • 查詢單條數據時直接返回對象結構,減少數據層級;
    • 查詢列表數據時返回數組結構;
    • 創建或更新成功,返回修改後的數據;(也可以不返回數據直接返回空對象)
    • 刪除成功時返回空對象
  • status:

    • error, 客户端(前端)出錯,HTTP 狀態響應碼在400-599之間。如,傳入錯誤參數,訪問不存在的數據資源等
    • fail,服務端(後端)出錯,HTTP 狀態響應碼在500-599之間。如,代碼語法錯誤,空對象調用函數,連接數據庫失敗,undefined index等
    • success, HTTP 響應狀態碼為1XX、2XX和3XX,用來表示業務處理成功。
  • message: 描述執行的請求操作處理的結果;也可以支持國際化,根據實際業務需求來切換。
  • code: HTTP 響應狀態碼;可以根據實際業務需求,調整成業務操作碼

代碼實現

<?php


namespace App\Http;

use Illuminate\Http\Resources\Json\JsonResource;
use  \Illuminate\Http\Response as HttpResponse;

class Response
{
    public function errorNotFound($message = 'Not Found')
    {
        $this->fail($message, HttpResponse::HTTP_NOT_FOUND);
    }

    /**
     * @param  string  $message
     * @param  int  $code
     * @param  null  $data
     * @param  array  $header
     * @param  int  $options
     * @throws \Illuminate\Http\Exceptions\HttpResponseException
     */
    public function fail(string $message = '', int $code = HttpResponse::HTTP_INTERNAL_SERVER_ERROR, $data = null, array $header = [], int $options = 0)
    {
        $status = ($code >= 400 && $code <= 499) ? 'error' : 'fail';
        $message = (!$message && isset(HttpResponse::$statusTexts[$code])) ? HttpResponse::$statusTexts[$code] : 'Service error';

        response()->json([
            'status' => $status,
            'code' => $code,
            'message' => $message,// 錯誤描述
            'data' => (object) $data,// 錯誤詳情
        ], $code, $header, $options)->throwResponse();
    }

    public function errorBadRequest($message = 'Bad Request')
    {
        $this->fail($message, HttpResponse::HTTP_BAD_REQUEST);
    }

    public function errorForbidden($message = 'Forbidden')
    {
        $this->fail($message, HttpResponse::HTTP_FORBIDDEN);
    }

    public function errorInternal($message = 'Internal Error')
    {
        $this->fail($message, HttpResponse::HTTP_INTERNAL_SERVER_ERROR);
    }

    public function errorUnauthorized($message = 'Unauthorized')
    {
        $this->fail($message, HttpResponse::HTTP_UNAUTHORIZED);
    }

    public function errorMethodNotAllowed($message = 'Method Not Allowed')
    {
        $this->fail($message, HttpResponse::HTTP_METHOD_NOT_ALLOWED);
    }

    public function accepted($message = 'Accepted')
    {
        return $this->success(null, $message, HttpResponse::HTTP_ACCEPTED);
    }

    /**
     * @param  JsonResource|array|null  $data
     * @param  string  $message
     * @param  int  $code
     * @param  array  $headers
     * @param  int  $option
     * @return \Illuminate\Http\JsonResponse|JsonResource
     */
    public function success($data, string $message = '', $code = HttpResponse::HTTP_OK, array $headers = [], $option = 0)
    {
        $message = (!$message && isset(HttpResponse::$statusTexts[$code])) ? HttpResponse::$statusTexts[$code] : 'OK';
        $additionalData = [
            'status' => 'success',
            'code' => $code,
            'message' => $message
        ];

        if ($data instanceof JsonResource) {
            return $data->additional($additionalData);
        }

        return response()->json(array_merge($additionalData, ['data' => $data ?: (object) $data]), $code, $headers, $option);
    }

    /**
     * @param  JsonResource|array|null  $data
     * @param  string  $message
     * @param  string  $location
     * @return \Illuminate\Http\JsonResponse|JsonResource
     */
    public function created($data = null, $message = 'Created', string $location = '')
    {
        $response = $this->success($data, $message, HttpResponse::HTTP_CREATED);
        if ($location) {
            $response->header('Location', $location);
        }

        return $response;
    }

    public function noContent($message = 'No content')
    {
        return $this->success(null, $message, HttpResponse::HTTP_NO_CONTENT);
    }
}

使用

在需要進行 HTTP 響應的地方使用 \\App\\Traits\\Helpers\\App\\Http\\Response中封裝的響應方法進行調用。

通常使用是在 Controller 層中根據業務處理的結果進行響應,所以在 \\App\\Http\\Controllers基類中已經引入了 Helperstrait,可以直接在 Controller 中進行如下調用:

// 操作成功情況
$this->response->success($data,$message);
$this->response->success(new  UserCollection($resource),  '成功');// 返回 API Resouce Collection
$this->response->success(new  UserResource($user),  '成功');// 返回 API Resouce
$user  =  ["name"=>"nickname","email"=>"longjian.huang@foxmail.com"];
$this->response->success($user,  '成功');// 返回普通數組

$this->response->created($data,$message);
$this->response->accepted($message);
$this->response->noContent($message);

// 操作失敗或異常情況
$this->response->fail($message);
$this->response->errorNotFound();
$this->response->errorBadRequest();
$this->response->errorForbidden();
$this->response->errorInternal();
$this->response->errorUnauthorized();
$this->response->errorMethodNotAllowed();

操作成功時的響應結構

  • 返回單條數據
{
    "data": {
        "nickname": "Jiannei",
        "email": "longjian.huang@foxmail.com"
    },
    "status": "success",
    "code": 200,
    "message": "成功"
}
  • 返回列表數據
{
    "data": [
        {
            "nickname": "Jiannei",
            "email": "longjian.huang@foxmail.com"
        },
        {
            "nickname": "Qian",
            "email": "1234567891@foxmail.com"
        },
        {
            "nickname": "Turbo",
            "email": "123456789@foxmail.com"
        }
        // ...
    ],
    "links": {
        "first": "http://lumen-api.test/users?page=1",
        "last": null,
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "path": "http://lumen-api.test/users",
        "per_page": 15,
        "to": 13
    },
    "status": "success",
    "code": 200,
    "message": "成功"
}

操作失敗時的響應結構

{
    "status": "fail",
    "code": 500,
    "message": "Service error",
    "data": {}
}

異常捕獲時的響應結構

整體格式與業務操作成功和業務操作失敗時的一致,相比失敗時,data 部分會增加額外的異常信息展示,方便項目開發階段進行快速地問題定位。

  • 自定義實現了 ValidationException 的響應結構
{
    "status": "error",
    "code": 422,
    "message": "Validation error",
    "data": {
        "email": [
            "The email has already been taken."
        ],
        "password": [
            "The password field is required."
        ]
    }
}
  • NotFoundException異常捕獲的響應結構

關閉 debug 時:

{
    "status": "error",
    "code": 404,
    "message": "Service error",
    "data": {
        "message": "No query results for model [App\\Models\\User] 19"
    }
}

開啓 debug 時:

{
    "status": "error",
    "code": 404,
    "message": "Service error",
    "data": {
        "message": "No query results for model [App\\Models\\User] 19",
        "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
        "file": "/var/www/lumen-api-starter/vendor/laravel/lumen-framework/src/Exceptions/Handler.php",
        "line": 107,
        "trace": [
            {
                "file": "/var/www/lumen-api-starter/app/Exceptions/Handler.php",
                "line": 55,
                "function": "render",
                "class": "Laravel\\Lumen\\Exceptions\\Handler",
                "type": "->"
            },
            {
                "file": "/var/www/lumen-api-starter/vendor/laravel/lumen-framework/src/Routing/Pipeline.php",
                "line": 72,
                "function": "render",
                "class": "App\\Exceptions\\Handler",
                "type": "->"
            },
            {
                "file": "/var/www/lumen-api-starter/vendor/laravel/lumen-framework/src/Routing/Pipeline.php",
                "line": 50,
                "function": "handleException",
                "class": "Laravel\\Lumen\\Routing\\Pipeline",
                "type": "->"
            }
            // ...
        ]
    }
}
  • 其他類型異常捕獲時的響應結構
{
    "status": "fail",
    "code": 500,
    "message": "syntax error, unexpected '$user' (T_VARIABLE)",
    "data": {
        "message": "syntax error, unexpected '$user' (T_VARIABLE)",
        "exception": "ParseError",
        "file": "/var/www/lumen-api-starter/app/Http/Controllers/UsersController.php",
        "line": 34,
        "trace": [
            {
                "file": "/var/www/lumen-api-starter/vendor/composer/ClassLoader.php",
                "line": 322,
                "function": "Composer\\Autoload\\includeFile"
            },
            {
                "function": "loadClass",
                "class": "Composer\\Autoload\\ClassLoader",
                "type": "->"
            },
            {
                "function": "spl_autoload_call"
            }
           // ...
        ]
    }
}

特別説明:使用 Postman 等 Api 測試工具的使用需要添加 X-Requested-With:XMLHttpRequest或者Accept:application/jsonheader 信息來表明是 Api 請求,否則在異常捕獲到後返回的可能不是預期的 JSON 格式響應。

豐富的日誌模式支持

  • 支持記錄日誌(包括業務錯誤記錄的日誌和捕獲的異常信息等)到 MongoDB,方便線上問題的排查
  • 記錄到 MongoDB 的日誌,支持以每日、每月以及每年按表進行拆分
  • 支持記錄 sql 語句

Repository & Service 模式架構

使用了andersao/l5-repository 進行進行項目結構設計,補充添加了 Service 層。

職責説明

待補充。

規範

命名規範:待補充

使用規範:待補充

Packages

  • guzzlehttp/guzzle
  • jenssegers/mongodb
  • tymon/jwt-auth
  • prettus/l5-repository
  • overtrue/laravel-query-logger

其他

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

如果有任何批評建議,通過郵箱(longjian.huang@foxmial.com)的方式(如果我每天堅持看郵件的話)可以聯繫到我。

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

參考

  • RESTful API 最佳實踐(感謝 [@liuqing_hu](https://learnku.com/users/17343) 提供了非常詳細的思維導圖:+1:)
  • RESTful 服務最佳實踐
  • DingoApi
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.