Lumen 数据验证规范FormRequest

2019-10-24  本文已影响0人  Emma_371b

Lumen 数据验证规范

Author: EmmaShao
Version: 1.2

版本 时间 内容变更
v1.0 2019-05-09 -
v1.1 2019-05-20 -
v1.2 2019-09-23 FormRequest中统一处理rules方法,子类不再写rules方法,每个验证规则名为 ruleMethod,添加after钩子

前言

Laravel 提供了多种方法来验证请求输入数据,默认情况下,Laravel 的控制器基类使用 ValidateRequests trait, 该trait 提供了便捷方法通过各种功能强大验证规则来验证输入的HTTP请求。
Laravel 文档和网上的各种教程,会教授我们一个任务可以使用好几种方法来完成。对于框架设计来说,灵活是件好事,能提供给开发者不同的选项,能让框架适用更多的用户场景。但是对于团队的协同开发来说,大部分时候,更多的选项反而是累赘。

验证规范,使用场景有两种,http 请求和 excel 导入

目的意义

规范 兼备开发效率、程序执行效率、扩展性和安全性

laravel 提供的验证规53种,覆盖范围非常广
参考手册地址:https://laravelacademy.org/post/8798.html
参考规范:https://learnku.com/docs/laravel-specification/5.5/form-validation/507
参考package: https://github.com/urameshibr/lumen-form-request


代码实践

controller

原则:

在controller中进行参数验证,并没有错,但这并不是最好的实现方法,而且会让controller看起来很混乱,这种不满足单一职责原则,不推荐。controller应该只做一件事情,就是处理从route路由过来的数据,并且有一个合适的返回。

推荐写法与步骤

1. 创建一个 GrantRequest 类,路径 app/Http/Requests/GrantRequest.php

<?php
/**
 * created by emma
 * @2019-09-24
 */

namespace App\Http\Requests;


use App\Http\Requests\FormRequest;

class GrantRequest extends FormRequest
{
    public function ruleCreate()
    {
        $rule = [
            'code'                         => 'required|string|min:1|max:50', 
            'num'                          => 'required|int|min:0', // 数量
            'name'                         => 'required|string|min:2', // 员工姓名
            'user_id'                      => 'required|string|min:1', // 员工号
            'id_code'                      => 'required|string|min:2', // 证件号
        ];
        return $rule;
    }

    // 单个新增授予后的规则处理
    public function ruleCreateAfter($validator)
    {
        $vestList = $this->input('vest_schedule_detail');
        $grantNum = $this->input('num');

        $pendingNum = $this->input('pending_num');
        if ($grantNum == 0 && $pendingNum == 0) {
            $validator->errors()->add("num", '授予数量与已归属待行权数量不能同时为0');
        }

        $sum = 0;
        if ($vestList) {
            foreach ($vestList as $key => $detail) {
                $sum += intval($detail['value']);
            }
        }
        if ($sum != $grantNum) {
            $validator->errors()->add("num", "归属规则数量之和{$sum}与授予数量{$grantNum}不等");
        }
    }

    public function ruleQuery()
    {
        $rule = [
            'key'        => 'string|min:1|max:50', // 搜索员工关键字
            'code'       => 'string|min:1|max:50', // 搜索编号
            'page_num'   => 'required|integer|min:1', // 页数
            'page_size'  => 'required|integer|min:1|max:500', // 单页显示最多条数
            'start_date' => 'date|date_format:Y-m-d', // 开始日期
            'end_date'   => 'date|date_format:Y-m-d', // 截止日期
            'status'     => 'required|in:' . RsuGrant::DISABLE . ',' . RsuGrant::ENABLE, // 状态
        ];
        return $rule;
    }

    public function ruleQueryAfter($validator)
    {
        $startDate = $this->input('start_date');
        $endDate = $this->input('end_date');
        if ($startDate > $endDate) {
            $validator->errors()->add("end_date", "结束日期不能早于开始日期");
        }
    }
}

2. 在 app/Http/Controllers/Support/CompanyController.php

    /**
     * 新增单条授予数据
     */
    public function create(GrantRequest $request)
    {
        try {
            $param = $request->validated();
            // ....
        } catch (\Exception $e) {
        }
    }

3. 引入validate,会担忧 $request 不能trim,表慌,我们在routes中启用中间件 TrimStrings 即可, 在 /app/Http/routes.php

   //提供给中台系统的数据接口-需要有登陆态
   $app->group(
       [
           'namespace' => 'Support',
           'middleware' => [ 'TrimStrings']
       ],
       function () use ($app) {
           $app->get('/modify-middle/get-company-list', 'ModifyController@getCompanyList');
       }

4. 验证错误的语言提示包问题

语言包设计有些麻烦,数据验证前端本就拦截过一遍,大约后端的中英文翻译显得就没那么必要了。一旦发现有这样的问题在后端被拦截下来,应通知前端修正并拦截。

5. request中的rule规则不能覆盖的校验规则,请在Request类中添加after钩子函数

6. 接口数据格式定义时要规范,要么是json格式,要么是键值对格式。不混合格式传输
有时候发现无法转发到service,大约是头部没有添加 'Content-Type' => 'application/json'

rule 规则使用

推荐使用:

禁止使用与数据库查询结合的规则:

查询数据表的,不能使用,数据加密存储,这种写法能用到的地方不多,另外,接下来的逻辑处理还需要用到此次查询的结果,所以放在controller或Logic或Trait 处理这种数据库查询操作

Request 基类添加

本规范,是参考 Laravel 框架推荐的规范,FormRequest 在Laravel中天然支持,但在 Lumen 中移除了所以参考了Laravel框架,与composer包 urameshibr/lumen-form-request,添加了文件 如下:

适用于 lumen5.3, lumen5.5

  1. 添加文件app/Http/Requests/FormRequest.php
<?php

namespace App\Http\Requests;

use App\Exceptions\ExceptionCode;
use App\Http\Common\UlsLog;
use FutuWeb\Monitor\Reporter;
use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Contracts\Validation\ValidatesWhenResolved;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\UnauthorizedException;
use Illuminate\Validation\ValidatesWhenResolvedTrait;
use Laravel\Lumen\Http\Redirector;

class FormRequest extends Request implements ValidatesWhenResolved
{
    use ValidatesWhenResolvedTrait;
    /**
     * The container instance.
     *
     * @var \Illuminate\Container\Container
     */
    protected $container;
    /**
     * The redirector instance.
     *
     * @var \Laravel\Lumen\Http\Redirector
     */
    protected $redirector;
    /**
     * The route to redirect to if validation fails.
     *
     * @var string
     */
    protected $redirectRoute;
    /**
     * The controller action to redirect to if validation fails.
     *
     * @var string
     */
    protected $redirectAction;
    /**
     * The key to be used for the view error bag.
     *
     * @var string
     */
    protected $errorBag = 'default';
    /**
     * The input keys that should not be flashed on redirect.
     *
     * @var array
     */
    protected $dontFlash = ['password', 'password_confirmation'];
    /**
     * Get the validator instance for the request.
     *
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function getValidatorInstance()
    {
        $factory = $this->container->make(ValidationFactory::class);
        if (method_exists($this, 'validator')) {
            return $this->container->call([$this, 'validator'], compact('factory'));
        }
        return $factory->make(
            $this->validationData(), $this->container->call([$this, 'rules']), $this->messages(), $this->attributes()
        )->after(function($validator){
            $this->after($validator);
        });
    }
    /**
     * Get data to be validated from the request.
     *
     * @return array
     */
    protected function validationData()
    {
        return $this->all();
    }
    /**
     * Handle a failed validation attempt.
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     * @return void
     *
     * @throws \Illuminate\Http\Exceptions\HttpResponseException
     */
    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException($this->response([
                "result" => ExceptionCode::ERROR_PARAMETER,
                "msg" => $firstErrorMsg
            ]
        ));
    }
    /**
     * Determine if the request passes the authorization check.
     *
     * @return bool
     */
    protected function passesAuthorization()
    {
        if (method_exists($this, 'authorize')) {
            return $this->container->call([$this, 'authorize']);
        }
        return false;
    }
    /**
     * Handle a failed authorization attempt.
     *
     * @return void
     *
     * @throws \Illuminate\Http\Exceptions\HttpResponseException
     */
    protected function failedAuthorization()
    {
//        throw new HttpResponseException($this->forbiddenResponse());
        throw new UnauthorizedException($this->forbiddenResponse());
    }
    /**
     * Get the proper failed validation response for the request.
     *
     * @param  array  $errors
     * @return \Illuminate\Http\JsonResponse
     */
    public function response(array $errors)
    {
        return new JsonResponse($errors, 200);
    }
    /**
     * Get the response for a forbidden operation.
     *
     * @return \Illuminate\Http\Response
     */
    public function forbiddenResponse()
    {
        return new Response('Forbidden', 403);
    }
    /**
     * Format the errors from the given Validator instance.
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     * @return array
     */
    protected function formatErrors(Validator $validator)
    {
        return $validator->getMessageBag()->toArray();
    }
    /**
     * Set the Redirector instance.
     *
     * @param  \Laravel\Lumen\Http\Redirector  $redirector
     * @return $this
     */
    public function setRedirector(Redirector $redirector)
    {
        $this->redirector = $redirector;
        return $this;
    }
    /**
     * Set the container implementation.
     *
     * @param  \Illuminate\Container\Container  $container
     * @return $this
     */
    public function setContainer(Container $container)
    {
        $this->container = $container;
        return $this;
    }
    /**
     * Get custom messages for validator errors.
     *
     * @return array
     */
    public function messages()
    {
        return [];
    }
    /**
     * Get custom attributes for validator errors.
     *
     * @return array
     */
    public function attributes()
    {
        return [];
    }

    public function validated()
    {
        $rules = $this->container->call([$this, 'rules']);

        return $this->only(collect($rules)->keys()->map(function ($rule) {
            return explode('.', $rule)[0];
        })->unique()->toArray());
    }

    public function setJson($json)
    {
        $this->json = $json;
        return $this;
    }

    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        list($controllerName, $actionName) = explode('@', substr(strrchr($this->route()[1]['uses'], '\\'), 1));
        $actionName = 'rule' . ucfirst($actionName);
        return $this->$actionName();
    }

    public function after($validator)
    {
        list($controllerName, $actionName) = explode('@', substr(strrchr($this->route()[1]['uses'], '\\'), 1));
        $actionName = 'rule' . ucfirst($actionName) . 'After';
        if (method_exists($this, $actionName)) {
            $this->$actionName($validator);
        }
    }
}


  1. 修改文件 bootstrap/app.php, 追加 FormRequestServiceProvider
$app->register(\App\Providers\AppServiceProvider::class);
// emmashao 2019-05-09 加入
$app->register(App\Providers\FormRequestServiceProvider::class);

TrimStrings 中间件添加

参考 Laravel 框架,将 TrimStrings 复制到 lumen5.3中。lumen5.5天然支持,无需自己添加

  1. 添加文件 app/Http/Middleware/TransformsRequest.php
<?php

namespace App\Http\Middleware;

use Closure;
use Symfony\Component\HttpFoundation\ParameterBag;

class TransformsRequest
{
    /**
     * The additional attributes passed to the middleware.
     *
     * @var array
     */
    protected $attributes = [];

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, ...$attributes)
    {
        $this->attributes = $attributes;

        $this->clean($request);

        return $next($request);
    }

    /**
     * Clean the request's data.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     */
    protected function clean($request)
    {
        $this->cleanParameterBag($request->query);

        if ($request->isJson()) {
            $this->cleanParameterBag($request->json());
        } else {
            $this->cleanParameterBag($request->request);
        }
    }

    /**
     * Clean the data in the parameter bag.
     *
     * @param  \Symfony\Component\HttpFoundation\ParameterBag  $bag
     * @return void
     */
    protected function cleanParameterBag(ParameterBag $bag)
    {
        $bag->replace($this->cleanArray($bag->all()));
    }

    /**
     * Clean the data in the given array.
     *
     * @param  array  $data
     * @return array
     */
    protected function cleanArray(array $data)
    {
        return collect($data)->map(function ($value, $key) {
            return $this->cleanValue($key, $value);
        })->all();
    }

    /**
     * Clean the given value.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function cleanValue($key, $value)
    {
        if (is_array($value)) {
            return $this->cleanArray($value);
        }

        return $this->transform($key, $value);
    }

    /**
     * Transform the given value.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function transform($key, $value)
    {
        return $value;
    }
}

  1. 添加文件 app/Http/Middleware/TrimStrings.php
<?php
namespace App\Http\Middleware;

class TrimStrings extends TransformsRequest
{
    /**
     * The attributes that should not be trimmed.
     *
     * @var array
     */
    protected $except = [
        'password',
        'password_confirmation',
    ];
    /**
     * Transform the given value.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function transform($key, $value)
    {
        if (in_array($key, $this->except, true)) {
            return $value;
        }
        return is_string($value) ? trim($value) : $value;
    }
}


Job

ESOP有大量的业务需要导入excel,excel导入均使用的异步导入。步骤如下:

  1. 上传excel,直接存入excel存储 表,返回id,生产此id的消息,入队列
  2. 前端根据此 id ,查询处理结果
  3. 后端消费消息,数据校验与存储,校验失败要输出错误详细信息,完全通过需要进行数据存储

Excel的数据校验内容繁多,包括

  1. 是否缺失表单(表单数量是否正确)
  2. 表单内容是否可以为空(多表单数据,往往第二个及以后的表单内容可以为空,如上传授予含历史数据)
  3. 是否缺列(查看excel第一行数据与定义的头部信息是否一致)
  4. 基本的单表单格式验证(比如必填项,整数、小数位等的校验,不耗时)
  5. 多表单的联合校验(比如授予信息与归属信息的对应数量校验)
  6. 与数据库的匹配校验

据过往写导入的经验,往往数据校验极有可能花费代码千行,如果按照一天150行代码的产出,大约需要六天的样子,难调试,bug多,此时使用 Validator 就显得非常必要,据开发者反馈,三天的工作量可以降到一天。代码可读性、灵活性、健壮性都得到了提高。

异步导入 job 必须定义的函数:

// 获取表头
abstract protected function getMeta();

// 数据格式校验规则
abstract protected function getValidateFormatRule();

// 获取自定义错误信息
abstract protected function getValidateCustomMessages();

// 数据逻辑校验,与数据库匹配
abstract protected function validateLogic();

// 验证通过的数据做的处理,一般是落库保存
abstract protected function storeData();

Ps: 查询数据库才能校验的一定放后单独处理,因为比较耗时,运营经常反馈excel导入太过缓慢,如果数据有问题,比如那种不查数据库都能看到的问题,要优先反馈出来,让他们做修正,而不是逐行与数据库匹配,然后到了最后一行发现少了一个必填项。

上一篇下一篇

猜你喜欢

热点阅读