扩展/覆盖Laravel验证器类

3
在 Laravel 8.3中,他们引入了一个新特性 stopOnFirstFailure,一旦有规则失败,它就会完全停止验证。我想在 Laravel 7中使用这个特性。在检查Laravel 8的 vendor/laravel/framework/src/Validation/Validator.php 后,我发现 stopOnFirstFailure 只是在 Validator.php passes 函数中添加了一个 if 语句,如果受保护变量 stopOnFirstFailure 为 true,则会中断验证循环。通过扩展/覆盖 Validator.php 类,是否可以在 Laravel 7中实现这些呢?我一直在研究如何扩展核心 Laravel 类,并偶然发现了这篇文章,但它有点令人困惑,因为文章只展示了如何覆盖特定功能。在我的情况下,我需要声明一个受保护的变量,重写一个函数并声明一个新函数。

Laravel 8 Validator.php 代码:

声明变量:

/**
     * Indicates if the validator should stop on the first rule failure.
     *
     * @var bool
     */
    protected $stopOnFirstFailure = false;

stopOnFirstFailure function:

  /**
     * Instruct the validator to stop validating after the first rule failure.
     *
     * @param  bool  $stopOnFirstFailure
     * @return $this
     */
    public function stopOnFirstFailure($stopOnFirstFailure = true)
    {
        $this->stopOnFirstFailure = $stopOnFirstFailure;

        return $this;
    }

passes 函数:

/**
     * Determine if the data passes the validation rules.
     *
     * @return bool
     */
    public function passes()
    {
        $this->messages = new MessageBag;

        [$this->distinctValues, $this->failedRules] = [[], []];

        // We'll spin through each rule, validating the attributes attached to that
        // rule. Any error messages will be added to the containers with each of
        // the other error messages, returning true if we don't have messages.
        foreach ($this->rules as $attribute => $rules) {
            if ($this->shouldBeExcluded($attribute)) {
                $this->removeAttribute($attribute);

                continue;
            }

            if ($this->stopOnFirstFailure && $this->messages->isNotEmpty()) {
                break;
            }

            foreach ($rules as $rule) {
                $this->validateAttribute($attribute, $rule);

                if ($this->shouldBeExcluded($attribute)) {
                    $this->removeAttribute($attribute);

                    break;
                }

                if ($this->shouldStopValidating($attribute)) {
                    break;
                }
            }
        }

编辑:我的代码中使用了表单请求的验证器。 示例代码:

class UpdateRegistrationTagsRequest extends FormRequest
{
    protected $stopOnFirstFailure = true;
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'product_id' => ['required'],
            'product.*.type' => ['required','distinct'],
            'product.*.value' => ['required'],
            'product' => ['bail', 'required', 'array', new CheckIfArrayOfObj, new CheckIfProductTypeExists($this->product_id)]
        ];
    }

    protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator)
    {
        $response = new JsonResponse(['api' => [
            'header' => [
                'message' => 'The given data is invalid', 
                'errors' => $validator->errors()->first()   
            ],
            'body' => ''
                ]], 422);

        throw new \Illuminate\Validation\ValidationException($validator, $response);
    }
}

编辑:遵循 @thefallen 的建议,这是我的操作步骤。 我的 CustomValidator.php 类在 app 目录下的 CustomClass 中:

<?php 

namespace App\CustomClass;
use Illuminate\Validation\Validator;
use Illuminate\Support\MessageBag;

class CustomValidator extends Validator
{
    /**
     * Indicates if the validator should stop on the first rule failure.
     *
     * @var bool
     */
    protected $stopOnFirstFailure = true;

     /**
     * Instruct the validator to stop validating after the first rule failure.
     *
     * @param  bool  $stopOnFirstFailure
     * @return $this
     */
    public function stopOnFirstFailure($stopOnFirstFailure = true)
    {
        $this->stopOnFirstFailure = $stopOnFirstFailure;

        return $this;
    }

    /**
     * Determine if the data passes the validation rules.
     *
     * @return bool
     */
    public function passes()
    {
        $this->messages = new MessageBag;

        [$this->distinctValues, $this->failedRules] = [[], []];

        // We'll spin through each rule, validating the attributes attached to that
        // rule. Any error messages will be added to the containers with each of
        // the other error messages, returning true if we don't have messages.
        foreach ($this->rules as $attribute => $rules) {
            if ($this->shouldBeExcluded($attribute)) {
                $this->removeAttribute($attribute);

                continue;
            }

            if ($this->stopOnFirstFailure && $this->messages->isNotEmpty()) {
                break;
            }

            foreach ($rules as $rule) {
                $this->validateAttribute($attribute, $rule);

                if ($this->shouldBeExcluded($attribute)) {
                    $this->removeAttribute($attribute);

                    break;
                }

                if ($this->shouldStopValidating($attribute)) {
                    break;
                }
            }
        }
        return parent::passes();
    }
}

我的ValidatorFactory位于CustomClass文件夹内

<?php 

namespace App\CustomClass;
use Illuminate\Validation\Factory;
use App\CustomClass\CustomValidator;

class ValidatorFactory extends Factory
{
    protected function resolve( array $data, array $rules, array $messages, array $customAttributes )
    {
        if (is_null($this->resolver)) {
            return new CustomValidator($this->translator, $data, $rules, $messages, $customAttributes);
        }

        return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
    }
}

我的 AppServiceProvider:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\CustomClass\ValidatorFactory;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->extend('validator', function () {
            return $this->app->get(ValidatorFactory::class);
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}


你能添加一下你目前如何使用验证器的代码吗?你有一个请求类还是手动调用它? - thefallen
@thefallen 你好,我添加了我的代码。谢谢。 - cjdy13
“通过扩展/覆盖Validator.php类,能否在Laravel 7中实现这些功能?”是你的问题吗?是的,我想是的。 - bhucho
3个回答

6
你需要扩展验证器(Validator)以在该方法上进行更改,然后创建自己的验证工厂(Validation Factory)以创建这个新验证器而不是默认验证器。因此,在使用自己的验证器时,第一步是:
use Illuminate\Validation\Validator;

class CustomValidator extends Validator
{
    public function passes()
    {
        //TODO make changes on that loop
        return parent::passes();
    }
}

那么您需要一个验证工厂来创建这个新类,它也将扩展默认类:
use Illuminate\Validation\Factory;

class ValidatorFactory extends Factory
{
    protected function resolve( array $data, array $rules, array $messages, array $customAttributes )
    {
        if (is_null($this->resolver)) {
            return new CustomValidator($this->translator, $data, $rules, $messages, $customAttributes);
        }

        return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
    }
}

最后,在app/Providers/AppServiceProvider.php文件的register()方法中,您需要将默认工厂替换为自定义工厂:

$this->app->extend('validator', function () {
    return $this->app->get(ValidatorFactory::class);
});

请注意,validatorIlluminate\Validation\Factory 的绑定名称(或别名)。您应该可以轻易地对验证器进行任何更改。

感谢您的回复@thefallen。我尝试了您建议的方法,我在我的应用程序目录中创建了一个名为CustomClass的文件夹,然后创建了两个类CustomValidator.php和ValidatorFactory.php。我将Laravel 8代码复制到CustomValidator.php中,并复制了您的代码以用于ValidatorFactory,并在AppServiceProvider中注册了该工厂,但似乎没有起作用,尽管我没有收到任何错误信息。 - cjdy13
@cjdy13,这很奇怪,因为如果你按照步骤进行表单请求和手动验证,它应该可以工作。无论如何,我不想建议的另一种解决方案是在你的表单请求类中拥有一个validator()方法,在那里你可以返回你的类app(CustomValidator::class)。缺点是这只会针对这个表单请求调用。 - thefallen
让我更新一下我的帖子,这样我就可以向您展示我做了什么。 - cjdy13
我已经更新了我的帖子,附上了最新的代码。谢谢! - cjdy13
1
嗨@thefallen,我的错,我复制的passes函数不完整,我只是复制了缺失的代码片段,现在它可以工作了!非常感谢你的帮助! :) - cjdy13
显示剩余3条评论

0
可能有点晚了,但我遇到了与Laravel 6卡住的同样问题。我不想扩展/覆盖验证器的当前正常行为。所以我做了这个。
public function validateResolved()
{
    if (!$this->authorize()) {
        $this->failedAuthorization();
    }
    foreach ($this->rules() as $key=>$val) {
        $validator = app('validator')->make(
            request()->all(),
            [$key=>$val],
            $this->messages()
        );
        if ($validator->fails()) {
            throw \Illuminate\Validation\ValidationException::withMessages($validator->errors()->messages());
        }
    }
}

0
来自Laracasts用户@davestewart的回答:

The supposed correct way of using (resolving) custom validators is to use the Validator::resolver method:

App::make('validator')->resolver(function($translator, $data, $rules, $messages)
{
    // return your custom validator here
    return new MyCustomValidator($translator, $data, $rules, $messages);
});

In theory, when the FormRequest class calls getValidatorInstance() it should resolve your custom validator.

我的个人感觉是这种做法非常脆弱,但我只使用这个框架大约9个月左右,所以我认为有很好的理由使它变得如此复杂。

附言:该实现已在商业项目中进行了测试,并完全满足要求。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接