修改 Laravel FormRequest 验证,实现场景验证

在Laravel 中,很多创建和编辑的的接口都是需要做数据验证的,对于数据验证一般有2种方方式

  • 在控制器里直接使用Request的validate方法

  • 使用自定义FormRequest类,该类集成自Http\Request

    如果使用第一种方法,会比较乱,看起来不够优雅

    但是如果使用第二种方式,那么针对每一种请求都要定义一个FormRequest

    比如:ArticleStoreRequestArticleUpdateRequest

    但是你会发现基本上验证规则是一样的,当然你可以在控制器方法里只注入一个Request,但是如果针对于一个Model 有多个Update的那种,比如用户模块,修改密码/修改昵称/修改头像/修改地址/修改。。。怎么处理呢

    所以这几天针对这种情况,改进了下Laravel的Request机制,加了一个场景验证

    • 第一步:先创建一个AbstractRequest的基类
    <?php
    
    namespace App\Http\Requests;
    
    use Illuminate\Foundation\Http\FormRequest;
    use Illuminate\Support\Str;
    
    /**
     * 使用方法:
     * Class AbstractRequest
     * @package App\Http\Requests
     */
    class AbstractRequest extends FormRequest
    {
        public $scenes = [];
        public $currentScene;               //当前场景
        public $autoValidate = false;       //是否注入之后自动验证
        public $extendRules;
    
        public function authorize()
        {
            return true;
        }
    
        /**
         * 设置场景
         * @param $scene
         * @return $this
         */
        public function scene($scene)
        {
            $this->currentScene = $scene;
            return $this;
        }
    
        /**
         * 使用扩展rule
         * @param string $name
         * @return AbstractRequest
         */
        public function with($name = '')
        {
            if (is_array($name)) {
                $this->extendRules = array_merge($this->extendRules[], array_map(function ($v) {
                    return Str::camel($v);
                }, $name));
            } else if (is_string($name)) {
                $this->extendRules[] = Str::camel($name);
            }
    
            return $this;
        }
    
        /**
         * 覆盖自动验证方法
         */
        public function validateResolved()
        {
            if ($this->autoValidate) {
                $this->handleValidate();
            }
        }
    
        /**
         * 验证方法
         * @param string $scene
         * @throws \Illuminate\Auth\Access\AuthorizationException
         * @throws \Illuminate\Validation\ValidationException
         */
        public function validate($scene = '')
        {
            if ($scene) {
                $this->currentScene = $scene;
            }
            $this->handleValidate();
        }
    
        /**
         * 根据场景获取规则
         * @return array|mixed
         */
        public function getRules()
        {
            $rules = $this->container->call([$this, 'rules']);
            $newRules = [];
            if ($this->extendRules) {
                $extendRules = array_reverse($this->extendRules);
                foreach ($extendRules as $extendRule) {
                    if (method_exists($this, "{$extendRule}Rules")) {   //合并场景规则
                        $rules = array_merge($rules, $this->container->call(
                            [$this, "{$extendRule}Rules"]
                        ));
                    }
                }
            }
            if ($this->currentScene && isset($this->scenes[$this->currentScene])) {
                $sceneFields = is_array($this->scenes[$this->currentScene])
                    ? $this->scenes[$this->currentScene] : explode(',', $this->scenes[$this->currentScene]);
                foreach ($sceneFields as $field) {
                    if (array_key_exists($field, $rules)) {
                        $newRules[$field] = $rules[$field];
                    }
                }
                return $newRules;
            }
            return $rules;
        }
    
        /**
         * 覆盖设置 自定义验证器
         * @param $factory
         * @return mixed
         */
        public function validator($factory)
        {
            return $factory->make(
                $this->validationData(), $this->getRules(),
                $this->messages(), $this->attributes()
            );
        }
    
        /**
         * 最终验证方法
         * @throws \Illuminate\Auth\Access\AuthorizationException
         * @throws \Illuminate\Validation\ValidationException
         */
        protected function handleValidate()
        {
            if (!$this->passesAuthorization()) {
                $this->failedAuthorization();
            }
            $instance = $this->getValidatorInstance();
            if ($instance->fails()) {
                $this->failedValidation($instance);
            }
        }
    
    }
    • 第二步:针对用户Request,我们只需要定义一个UserRequest继承AbstractRequest
    <?php
    
    namespace App\Http\Requests;
    
    class UserRequest extends AbstractRequest
    {
      public $scenes = [
          'nickname' => 'nickname',
          'avatar' => 'avatar',
          'password' => 'password',
          'address' => 'province_id,city_id'
      ];
    
      public function rules()
      {
          return [        //全部的验证规则
              'mobile' => [],
              'nickname' => [],
              'password' => [
                  'required', 'min:6', 'max:16'
              ],
              'avatar' => [],
              'province_id' => [],
              'city_id' => [],
              //...
          ];
      }
    
      public function passwordRules()
      {
          return [
              'password' => [
                  'required', 'min:6', 'max:16', 'different:$old_password'      //修改新密码不和旧密码相同,此处只是举例子,因为密码需要Hash处理才能判断是否相同
              ]
          ];
      }
    }
    • 控制器方法 UserController
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Http\Requests\UserRequest;
    
    class UserController
    {
    
        public function register(UserRequest $request)
        {
            $request->validate();   //默认不设置场景 全部验证
            //...
        }
    
        public function updateAddress($id, UserRequest $request)
        {
            $request->scene('address')->validate();
            //...
        }
    
        public function updateAvatar($id, UserRequest $request)
        {
            $request->validate('avatar');
            //...
        }
    
        public function updatePassword($id, UserRequest $request)
        {
            //设置password场景,只验证password字段,并且使用新的password规则替换原来的password规则
            $request->scene('password')
                ->with('password')
                ->validate();
            //...
        }
    }

    该方法没有修改Laravel的核心验证逻辑,只让在FormRequest在注入到Controller的时候不要做自动验证,当然,如果需要自动验证,那么设置$autoValidate = true即可。

    以上内容仅供参考。望亲喷。

    同时还有我也修改了ORM的场景验证规则,可以在model里设置经常,同时满足多场景创建和更新

本帖已被设为精华帖!
本帖由系统于 4年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 22

nark一下,回头研究

4年前 评论

思路比较简单

  • 第一个是ValidatesWhenResolvedTrait 这个里面的 validateResolved 方法。该方法会在反射注入之后自动调用,也就是自动验证,所以我们覆写这方法,让他不自动执行
  • 第二个 FormRequest 里的 getValidatorInstance方法会优先查找是否有自定义的validator,如果有则使用自定义的validator
  • 综上所述 : 禁止validateResolved 自动执行,重新定义validator,获取所需的场景和规则,生成新的验证器
4年前 评论

第二种可以通过路由名称或者http请求来辨别是新增或者更新,这样就不要创建多个request文件了

4年前 评论
pslxx (楼主) 4年前
Clusteramaryllis

我觉得社区项目开发规范里的更简单一些。

表单验证《 Laravel 项目开发规范》

4年前 评论
pslxx (楼主) 4年前
Clusteramaryllis (作者) 4年前
Epona

如果是存在时再验证,可以试试 sometimes 的验证方法

4年前 评论
pslxx (楼主) 4年前
Epona (作者) 4年前

学习了,你的这种验证场景在项目中会经常遇到!

4年前 评论

一直在找lavavel这种场景验证的demo,很实用 :+1:

4年前 评论

楼主大大你好,最近我一直在写关于input失去焦点时,使用xhr发送this.value给Request类验证是否为唯一性。但是我想实现判断$request是name?还是email来return 验证规则,请问要怎么写。看了很多例子都尝试自己写,但都没有成功。

3年前 评论

楼主 在Middleware 中获取Request 报错 ReflectionException: Class App\Http\Requests\Admin\LoginPost does not exist in file /Users/lee/Code/businessaccessapi/vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php on line 25

3年前 评论

在这里留言一下回头过来研究一下。

3年前 评论

可以理解为 ThinkPHP 中的场景验证,不过这里有个问题,假如同一个字段,不同的验证规则该如何处理呢?

例如 nickname 在注册的时候是至少6位数,但在登陆的时候要求是4位数

建议参考:场景规则重写

3年前 评论

验证了不返回数据

2年前 评论

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