分享验证规则层的设计

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

我们的项目使用 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 于 8年前 加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 10

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

8年前 评论

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

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

8年前 评论
xcaptain

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

8年前 评论

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

8年前 评论

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

8年前 评论

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

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

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

8年前 评论

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

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

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

3年前 评论