封装 Laravel 自定义表单请求

背景

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 协议》,转载必须注明作者和本文链接
本帖由系统于 1天前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 10

分久必合,合久必分 :flushed:

1周前 评论

我觉得我的参数验证封装类比你的优雅 :joy:

file

1周前 评论
sunny123456 1周前
jdzor (作者) 1周前
sunny123456 1周前
jdzor (作者) 1周前
sunny123456 1周前
kinge

首先在一个控制器里写30个接口就是不正确的行为

1周前 评论

一般直接判断请求方式或者控制器对应方法名 file

6天前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!