分享验证规则层的设计

今天看到了这个问题验证规则写在哪里比较好?,看了一圈下来,大家都有自己的处理方式,但是没有一个比较优雅的解决方案,我来分享一下自己的设计吧。

我们的项目使用Lumen做API开发,如果把验证规则都写在控制器里面,不仅干扰代码结构,而且规则比较分散,增加维护成本,为了解决这个问题,我设计了Validation层来完成所有的验证工作,下面是具体实现。

首先在Http目录下新建Validations目录存放所有的验证类:

app
├── Http
│   ├── Controllers
│   │   ├── Controller.php
│   │   └── ExampleController.php
│   └── Validations
│       └── ExampleValidation.php

ExampleController.phpExampleValidation.php在命名上和目录结构上是互相对应的,它们的代码:

<?php

namespace App\Http\Controllers;

class ExampleController extends Controller
{
    public function show()
    {
    }

    public function store()
    {
    }
}
<?php

namespace App\Http\Validations;

class ExampleValidation
{
    public function show()
    {
        return [
            'rules' => [
                'id' => 'required',
            ],
            'message' => [

            ],
        ];
    }

    public function store()
    {
        return [
            'rules' => [
                'id' => 'required',
            ],
        ];
    }
}

我们约定好,ExampleControllershowstore方法,使用ExampleValidation中对应的showstore方法中返回的规则来验证,下面最重要的是能让他们按照约定关联上,我们在ExampleController的父Controllerapp/Http/Controllers/Controller.php中实现这个关联:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Laravel\Lumen\Routing\Controller as BaseController;

class Controller extends BaseController
{
    public function __construct(Request $request)
    {
        $this->validateRequest($request);
    }

    protected function validateRequest(Request $request, $name = null)
    {
        if (! $validator = $this->getValidator($request, $name)) {
            return;
        }

        $rules    = array_get($validator, 'rules', []);
        $messages = array_get($validator, 'messages', []);

        $this->validate($request, $rules, $messages);
    }

    protected function getValidator(Request $request, $name = null)
    {
        list($controller, $method) = explode('@', $request->route()[1]['uses']);

        $method = $name ?: $method;

        $class = str_replace('Controller', 'Validation', $controller);

        if (! class_exists($class) || ! method_exists($class, $method)) {
            return false;
        }

        return call_user_func([new $class, $method]);
    }
}

getValidator ()方法会去检查有没有对应当前控制器方法的验证规则,如果有就使用该规则来验证参数,如果ExampleController中新增了方法destroy()需要验证规则,只需要在ExampleValidation中添加同名的destroy()方法,return相应的rules和messages即可。

另外,如果ExampleController中新增了方法update()需要复用ExampleValidation中的store()方法返回的规则,可以这样实现

public function update(Request $request)
{
    $this->validateRequest($request, 'store');

    //...
}

这个方案能在添加少量代码的情况下实现规则验证和控制器的分离,以及规则的复用,是目前我认为比较优雅的方案了,欢迎拍砖。

如果有其它好的思路,欢迎讨论。

本帖已被设为精华帖!
本帖由 Summer 于 7年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 10

挺好,个人感觉这其实与 FormRequest 比起来的话 FormRequest 更独立也更易于理解与重用。

7年前 评论

@overtrue 按照FormRequest的定义,它的职责范围应该是处理web表单提交请求,对于API的参数验证,用它显得不太合适

ps: Lumen中没有FormRequest,所以自己换种方式实现它,并且Validation层也比较复符合单一职责原则

7年前 评论
xcaptain

http://slides.com/howtomakeaturn/model#/9/...
这种方式我觉得更棒

7年前 评论

这个思路挺好的,根据不同的 action 来获取不同的验证规则,不知道有没有用仓储模式,https://github.com/andersao/l5-repository 这个仓储就实现了类似的功能,不过 validator 不是写在控制器中,而是写在一个额外的 validator 类里面,这样把验证从控制器中分离出来了,可以参考优化一下。

7年前 评论

思路不错,挺好的。但是有一个问题不知道博主怎么考虑的。
就是验证完成后的返回是如何处理的呢?
如果直接在__construct中返回,不是还是会走到业务逻辑的方法中去吗?

7年前 评论

@kaelli 几个月前的帖子都翻出来了:smile:

不是在construct返回,而是在construct中抛异常,然后在app/Exceptions/Handler.php handle 异常,输出response

后来优化成了中间件,实现了一个完全独立的验证层了。

7年前 评论

@song 学习了:thumbsup:

7年前 评论

@song array_get();为什么报错?

4年前 评论
sven_666 3年前
汪阿浠 (作者) 3年前

我觉得这个验证方式比较鸡肋,FromRequest可通过注入的方式在请求层做数据验证,这个方式同样适用于API.

2年前 评论

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