使用 Laravel 開發 API 時的前置準備

巴啦啦臭魔仙發表於2022-03-16

前言

使用 Laravel 有一段時間了,雖然公司專案使用的都是 Thinkphp 框架,但我個人還是比較偏好 Laravel,今天來總結我平時進行開發前的一些準備工作,如果有不合理的地方或者有更好的建議歡迎各位大佬指出糾正!

環境

PHP8 + MySQL5.7 + Nginx1.20
IDE:PhpStorm

搭建

安裝

推薦使用 composer 安裝 或者 Laravel 安裝器安裝:
Composer 安裝

composer create-project laravel/laravel example-app

Laravel 安裝器安裝

composer global require laravel/installer

laravel new example-app

具體詳見文件

配置

1、資料庫
配置根目錄下 .env 檔案

DB_CONNECTION=mysql
// host地址
DB_HOST=127.0.0.1
// 埠號
DB_PORT=3306
// 資料庫名
DB_DATABASE=laravel9
// 使用者名稱
DB_USERNAME=root
// 密碼
DB_PASSWORD=

2、時區
配置 config/app.php 檔案

// 時區修改,感覺兩者皆可,自己根據實際情況定義
'timezone' => 'PRC', // 大陸時間'timezone' => 'Asia/Shanghai' // 上海時間

3、設定 Accept 頭中介軟體
(1)生成中介軟體

php artisan make:middleware AcceptHeader

(2)修改為以下內容

<?php
namespace App\Http\Middleware;
use Closure;
class AcceptHeader
{
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }
}

(3)新增中介軟體

protected $middlewareGroups = [
    'web' => [
        ...
    ],
    'api' => [
        \App\Http\Middleware\AcceptHeader::class,
        ...
    ],
];

(4)效果
設定前:
會返回 HTML ,且響應時間和樣式很糟糕
使用 Laravel 開發 API 時的前置準備
設定後:
返回 JSON 型別,響應時間很快和格式一目瞭然
使用 Laravel 開發 API 時的前置準備

擴充套件包

只列出專案必備的幾個,如果有需要可以從下面連結裡看一下
下載量最高的 100 個 Laravel 擴充套件包推薦

1、程式碼提示工具

composer require barryvdh/laravel-ide-helper --dev

2、語言包

composer require caouecs/laravel-lang

使用:將 vendor/caouecs/laravel-lang/src/zh-CN 檔案放到 resources/lang 目錄下,如果是 laravel9 則直接放到根目錄 lang 目錄下

使用 Laravel 開發 API 時的前置準備
修改 config/app中:

'locale' => 'zh_CN',

3、開發除錯利器(debugbar 在 dev 環境安裝)

composer require barryvdh/laravel-debugbar --dev

Response 響應

這裡不使用 dingo 進行開發,個人感覺不怎好用,我們下面自己定義
首先,我們需要在 app 目錄下建立一個 Helpers 目錄

Laravel9 開發 API 總結

一、封裝統一狀態碼(ResponseEnum)
app/Helpers 目錄下建立 ResponseEnum.php 檔案

<?php

namespace App\Helpers;

class ResponseEnum
{
    // 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 版本不受支援
    /*-------------------------------------------------------------------------------------------*/
}

二、建立業務異常捕獲 Exception 檔案
app/Exceptions 目錄下建立 BusinessException.php 檔案用於業務異常的丟擲

<?php

namespace App\Exceptions;

use Exception;

class BusinessException extends Exception
{
    /**
     * 業務異常建構函式
     * @param array $codeResponse 狀態碼
     * @param string $info 自定義返回資訊,不為空時會替換掉codeResponse 裡面的message文字資訊
     */
    public function __construct(array $codeResponse, $info = '')
    {
        [$code, $message] = $codeResponse;
        parent::__construct($info ?: $message, $code);
    }
}

三、自定義返回異常
修改 app/Exceptions 目錄下的 Handler.php 檔案

<?php

namespace App\Exceptions;

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

class Handler extends ExceptionHandler
{
    use ApiResponse;

    /**
     * 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 for 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)
    {
        // 如果是生產環境則返回500
        if (!config('app.debug')) {
            $this->throwBusinessException(ResponseEnum::SYSTEM_ERROR);
        }
        // 請求型別錯誤異常丟擲
        if ($exception instanceof MethodNotAllowedHttpException) {
            $this->throwBusinessException(ResponseEnum::CLIENT_METHOD_HTTP_TYPE_ERROR);
        }
        // 引數校驗錯誤異常丟擲
        if ($exception instanceof ValidationException) {
            $this->throwBusinessException(ResponseEnum::CLIENT_PARAMETER_ERROR);
        }
        // 路由不存在異常丟擲
        if ($exception instanceof NotFoundHttpException) {
            $this->throwBusinessException(ResponseEnum::CLIENT_NOT_FOUND_ERROR);
        }
        // 自定義錯誤異常丟擲
        if ($exception instanceof BusinessException) {
            return response()->json([
                'status'  => 'fail',
                'code'    => $exception->getCode(),
                'message' => $exception->getMessage(),
                'data'    => null,
                'error'  => null,
            ]);
        }
        return parent::render($request, $exception);
    }
}

四、封裝 API 返回的統一訊息(ApiResponse)
app/Helpers 目錄下建立 ApiResponse.php 檔案

<?php


namespace App\Helpers;

use App\Helpers\ResponseEnum;
use App\Exceptions\BusinessException;
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, null);
    }

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

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


    /**
     * 成功分頁返回
     * @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 BusinessException
     */
    public function throwBusinessException(array $codeResponse=ResponseEnum::HTTP_ERROR, string $info = '')
    {
        throw new BusinessException($codeResponse, $info);
    }
}

五、建立控制器基類
1、在 app/Http/controller 目錄下建立一個 BaseController.php 作為 Api 的基類,然後在 BaseController.php 這個基類中繼承 Controller,並引入封裝 API 返回的統一訊息(ApiResponse)

<?php

namespace App\Http\Controllers;

use App\Helpers\ApiResponse;

class BaseController extends Controller
{
    // API介面響應
    use ApiResponse;
}

六、使用
1、返回成功資訊

return $this->success($data);

2、返回失敗資訊

return $this->fail($codeResponse);

3、丟擲異常

$this->throwBusinessException($codeResponse);

4、分頁

return $this->successPaginate($data);

引數輸入校驗

一、建立
App\Helpers 目錄下建立 VerifyRequestInput.php 檔案,並引入 ApiResponse ,這樣可以更便捷地校驗表單引數,其中 verifyData 方法可以自定義校驗欄位及規則

<?php


namespace App\Helpers;


use App\Helpers\ResponseEnum;
use App\Exceptions\BusinessException;
use Illuminate\Validation\Rule;

trait VerifyRequestInput
{
    use ApiResponse;

    /**
     * 驗證ID
     * @param $key
     * @param null $default
     * @return mixed|null
     * @throws BusinessException
     */
    public function verifyId($key, $default=null)
    {
        return $this->verifyData($key, $default, 'integer|digits_between:1,20');
    }

    /**
     * 驗證是否為整數
     * @param $key
     * @param null $default
     * @return mixed|null
     * @throws BusinessException
     */
    public function verifyInteger($key, $default=null)
    {
        return $this->verifyData($key, $default, 'integer');
    }

    /**
     * 驗證是否為數字
     * @param $key
     * @param null $default
     * @return mixed|null
     * @throws BusinessException
     */
    public function verifyNumeric($key, $default=null)
    {
        return $this->verifyData($key, $default, 'numeric');
    }

    /**
     * 驗證是否為字串
     * @param $key
     * @param null $default
     * @return mixed|null
     * @throws BusinessException
     */
    public function verifyString($key, $default=null)
    {
        return $this->verifyData($key, $default, 'string');
    }

    /**
     * 驗證是否為布林值
     * @param $key
     * @param null $default
     * @return mixed|null
     * @throws BusinessException
     */
    public function verifyBoolean($key, $default=null)
    {
        return $this->verifyData($key, $default, 'boolean');
    }

    /**
     * 驗證是否為列舉
     * @param $key
     * @param null $default
     * @param array $enum
     * @return mixed|null
     * @throws BusinessException
     */
    public function verifyEnum($key, $default=null, array $enum=[])
    {
        return $this->verifyData($key, $default, Rule::in($enum));
    }

    /**
     * 自定義校驗引數
     * @param $key string 欄位
     * @param $default string 預設值
     * @param $rule string 驗證規則
     * @return mixed|null
     * @throws BusinessException
     */
    public function verifyData($key, $default, $rule)
    {
        $value = request()->input($key, $default);
        $validator = \Validator::make([$key => $value], [$key => $rule]);
        if (is_null($value)){
            $this->throwBusinessException(ResponseEnum::CLIENT_PARAMETER_ERROR);
        }
        if ($validator->fails()){
            $this->throwBusinessException(ResponseEnum::CLIENT_PARAMETER_ERROR, $validator->errors()->first());
        }
        return $value;
    }

}

2、使用
需要在 App\Http\Controllers\BaseController 這個控制器基類中引入 VerifyRequestInput

use App\Helpers\VerifyRequestInput;

// 驗證表單引數輸入請求
use VerifyRequestInput;

3、案例:
有一個 index 方法,我們在獲取引數時使用 verifyId 來校驗請求的引數

public function index()
{
    $id = $this->verifyId('id', null);
}

當我們請求時,因為傳入的引數是字串

http://127.0.0.1:8000/api/user/index?id=xddd

所以返回 The id must be an integer ,提示id必須為整數

Laravel9 開發 API 總結

建立服務層 Service

如果專案比較小,介面較少,業務邏輯放在 controller 和 model 層就可以。否則就需要建立一個 Service 層來存放一些較複雜些的業務邏輯。
一、在 app 目錄下,建立名叫 Services 的資料夾

Laravel 開發 API 心得

二、在新建立的 Services 目錄下建立基類 BaseService.php ,採用單例模式避免對記憶體造成浪費,也方便呼叫

<?php

namespace App\Services;

use App\Helpers\ApiResponse;

class BaseService
{
    // 引入api統一返回訊息
    use ApiResponse;

    protected static $instance;

    public static function getInstance()
    {
        if (static::$instance instanceof static){
            return self::$instance;
        }
        static::$instance = new static();
        return self::$instance;
    }

    protected function __construct(){}

    protected function __clone(){}

}

三、使用
例如要實現一個獲取使用者資訊的功能
1、在 Service 層建立一個 UserService.php 的檔案

<?php

namespace App\Services;

use App\Services\BaseService;

class UserService extends BaseService
{
    // 獲取使用者資訊
    public function getUserInfo()
    {
        return ['id' => 1, 'nickname' => '張三', 'age' => 18];
    }

}

2、在控制器 UserController 中增加一個 info 方法,並呼叫服務層中的 getUserInfo() 方法

use App\Services\UserService;

public function info()
{
    $user = UserService::getInstance()->getUserInfo();
    return $this->success($user);
}

3、返回

Laravel 開發 API 心得

監聽sql語句

1、建立監聽器

php artisan make:listener QueryListener

修改 handle 方法

public function handle(QueryExecuted $event)
{
    // 只在測試環境下輸出 log 日誌
    if (!app()->environment(['testing', 'local'])) {
        return;
    }
    $sql = $event->sql;
    $bindings = $event->bindings;
    $time = $event->time; // 毫秒
    $bindings = array_map(function ($binding) {
        if (is_string($binding)) {
            return (string)$binding;
        }
        if ($binding instanceof \DateTime) {
            return $binding->format("'Y-m-d H:i:s'");
        }
        return $binding;
    }, $bindings);
    $sql = str_replace('?', '%s', $sql);
    $sql = sprintf($sql, ...$bindings);
    Log::info('sql_log', ['sql' => $sql, 'time' => $time . 'ms']);
}

2、註冊監聽事件
在系統的服務提供者 App\Providers\EventServiceProvider 中註冊監聽事件

protected $listen = [
    ...
    QueryExecuted::class => [
        QueryListener::class,
    ],
];

3、執行 sql 檢視日誌
可以在日誌檔案中看到 sql 的執行時間、sql語句、毫秒數

[2022-05-08 22:45:04] local.INFO: sql_log {"sql":"select * from `user` where `user`.`id` = 3 limit 1","time":"51.59ms"} 

專案地址

gitee

不喜勿噴,如有錯誤或建議歡迎指出提出,持續更新中…

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

相關文章