封裝 Laravel 自定義表單請求

zxr615發表於2022-07-15

背景

Laravel 提供的自動表單驗證請求類,通常一個 class 是應用到一個 Action 上的,雖說可以應用到多個 Action 上,但驗證引數很少說完全一樣,粒度太細了,如果一個 Controller 有 10 個 Action 那就得對應建立10個驗證規則類,會導致檔案太多,所以可以封裝一下 Request ,把粒度由 Action 變成 Controller 級別得粒度,這樣一個 Controller 就只用建立一個表單請求類了, 實現效果如下:

封裝 Laravel 自定義表單請求

原有驗證方式

建立驗證規則

app/Http/Requests

├── Requests
│   ├── DeleteBlog.php
│   ├── StoreBlog.php
│   └── UpdateBlog.php

為了方便展示,放在了一個檔案內

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreBlogRequest extends FormRequest {
    public function rules() {
        return [
            'title'   => 'required|max:100',
            'content' => 'required|max:1000'
        ];
    }

    public function messages() {return []; }
}

class UpdateBlogRequest extends FormRequest {
    public function rules() {
        return [
            'id'      => 'required|integer',
            'title'   => 'required|max:100',
            'content' => 'required|max:1000'
        ];
    }

    public function messages() {return []; }
}

class DeleteBlogRequest extends FormRequest {
    public function rules() {
        return [
            'id' => 'required|integer',
        ];
    }

    public function messages() {return []; }
}

使用驗證規則

app/Http/Comtrollers/PostController

namespace App\Http\Controllers;

use App\Http\Requests\DeleteBlogRequest;
use App\Http\Requests\StoreBlogRequest;
use App\Http\Requests\UpdateBlogRequest;

class PostsController
{
    public function store(StoreBlogRequest $request) { /*...*/ }
    public function update(UpdateBlogRequest $request) { /*...*/ }
    public function delete(DeleteBlogRequest $request) { /*...*/ }
}

問題

3 個介面分別對應 StoreBlogRequest UpdateBlogRequest DeleteBlogRequest ,30個介面得對應30個 XxxRequest 檔案太多。

封裝

想法是一個 Controller 對應一個 RequestRequest 識別需要的返回的 Rules

達到的效果

class BlogController extends Controller
{
    // BlogRequest 自動驗證 store 規則
    public function store(BlogRequest $request) { /*...*/ }
    // BlogRequest 自動驗證 update 規則
    public function update(BlogRequest $request) { /*...*/ }
    // BlogRequest 自動驗證 delete 規則
    public function delete(BlogRequest $request) { /*...*/ }
}

實現方法

namespace App\Http\Requests;

class BlogRequest extends FormRequest
{
    public function rules() {
        // 獲取請求對應的ActionMethod(); e.g. store/update/delete
        $actionMethod = $this->route()->getActionMethod();

        if (!method_exists($this, $actionMethod)) {
            return [];
        }

        // e.g. $this->>store();
        return $this->$actionMethod();
    }

    public function store() {
        return [
            'title'   => 'required|max:100',
            'content' => 'required|max:1000'
        ];
    }

    public function delete() {
        return [
            'id' => 'required|integer',
        ];
    }
}

這樣就可以透過定義一個Request 規則對應一個 Controller

但問題緊接著也來了,如果要定義自定義 message authorize 怎麼實現呢?

根據上面的實現方式,可以抽象出一個 BaseRequest 去繼承 FormRequest 重寫對應的方法,然後自定義的 Request 再繼承 BaseRequest 專注定義驗證規則即可

BaseRequest

class BaseRequest extends FormRequest 
{
    public function authorize(): bool {
        $actionMethod = $this->route()->getActionMethod() . 'Authorize';

        if (!method_exists($this, $actionMethod)) {
            return true;
        }

        return $this->$actionMethod();
    }

    public function rules(): array {
        $actionMethod = $this->route()->getActionMethod() . 'Rules';

        if (!method_exists($this, $actionMethod)) {
            return [];
        }

        return $this->$actionMethod();
    }

    public function messages(): array {
        $actionMethod = $this->route()->getActionMethod() . 'Messages';

        if (!method_exists($this, $actionMethod)) {
            return [];
        }

        return $this->$actionMethod();
    }
}

可以看到,在 BaseRequest 中,方法以 ActionMethod + 規則 實現

BlogRequest

class BlogRequest extends BaseRequest {
    public function storeRules() {
        return ['title' => 'required|max:100', 'content' => 'required|max:1000'];
    }

    public function storeMessages() {
        return ['title.required' => '標題不能為空', 'content.required' => '內容不能為空'];
    }

    public function updateRules() {
        return ['id' => 'required|integer', 'title' => 'max:100', 'content' => 'max:1000'];
    }

    public function deleteRules() {
        return ['id' => 'required|integer',];
    }

    public function deleteAuthorize() {
        return false;
    }
}

BolgController

class BlogController extends Controller
{
    // BlogRequest 自動驗證 store 規則
    public function store(BlogRequest $request) { /*...*/ }

    // BlogRequest 自動驗證 update 規則
    public function update(BlogRequest $request) {
        return response()->json([
            'status'  => 200,
            'message' => 'success',
        ],200, [], JSON_UNESCAPED_UNICODE);
    }

    // BlogRequest 自動驗證 delete 規則
    public function delete(BlogRequest $request) { /*...*/ }
}

啟動服務驗證: php artisan serve

$ curl -s -d 'title=test' -X POST '127.0.0.1:8000/api/blog/after/store' | jq .
{
  "status": 400,
  "message": "內容不能為空"
}

$ curl -s -d 'id=1' -X POST '127.0.0.1:8000/api/blog/after/update' | jq .
{
  "status": 200,
  "message": "success"
}

$ curl -s -d 'id=1' -X POST '127.0.0.1:8000/api/blog/after/delete' | jq .
{
  "status": 403,
  "message": "您沒有許可權訪問" // 為什麼資訊是這個,下面會說到。
}

當然,你還可以在 BaseReqeust 中定義錯誤返回格式等

/** 引數驗證失敗返回處理 */
protected function failedValidation(Validator $validator): HttpResponseException
{
    $actionMethod = $this->route()->getActionMethod() . 'FailedValidation';

    // 使用自定義錯誤格式,但通常不會在具體規則類裡面重寫,因為錯誤格式應該要保持一致
    // 或許需要與外部系統互動之類特殊情況就就可以重寫此方法
    if (method_exists($this, $actionMethod)) {
        $this->$actionMethod();
    }

    // 預設錯誤格式
    $err = $validator->errors()->first();

    throw new HttpResponseException(response()->json([
        'status'  => 400,
        'message' => $err,
    ], 400, [], JSON_UNESCAPED_UNICODE));
}

請求授權驗證未透過時

/** 請求授權驗證未透過時(authorize方法 return false; 未透過時) */
protected function failedAuthorization()
{
    $actionMethod = $this->route()->getActionMethod() . 'FailedAuthorization';

    if (method_exists($this, $actionMethod)) {
        return $this->$actionMethod();
    }

    throw new HttpResponseException(response()->json([
        'status'  => 403,
        'message' => '您沒有許可權訪問',
    ], 403, [], JSON_UNESCAPED_UNICODE));
}

總結

  1. 這樣就可以實現一個 Controller 對應一個 Request 了,不過有利有弊,減少了檔案數量的同時帶來的就是修改對應規則的時候需要找到對應的規則。
  2. failedValidationfailedAuthorization 統一返回錯誤格式也可以透過判斷他們 Exception 來實現,因為它們分別丟擲的異常是 ValidationExceptionAuthorizationException
  3. 文件只能看簡單的使用方法,遇到問題得多去上層看看原始碼,找到些另闢蹊徑的處理方法。

Code

./app/Http/Controllers
├── Controllers
│   ├── AfterBlogController.php
│   ├── BeforeBlogController.php
│   ├── ...

./app/Http/Requests
├── Requests
│   ├── BaseRequest.php
│   ├── BlogRequest.php
│   ├── DeleteBlogRequest.php
│   ├── StoreBlogRequest.php
│   └── UpdateBlogRequest.php

Route

$ php artisan route:list | grep "blog"
POST | api/blog/after/delete 
POST | api/blog/after/store  
POST | api/blog/after/update 

POST | api/blog/before/delete
POST | api/blog/before/store 
POST | api/blog/before/update

github.com

github.com/zxr615/rewrite-pay-modu...

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

相關文章