Search code examples
phplaravellaravel-7laravel-8laravel-validation

Extending/Overriding Laravel Validator Class


In Laravel 8.3, they introduced a new feature, stopOnFirstFailure which stops the validation entirely once a rule fails. I would want to use this feature in Laravel 7. Upon checking the vendor/laravel/framework/src/Validation/Validator.php of Laravel 8, I found that stopOnFirstFailure simply adds an if statement in the passes function of the Validator.php which breaks the validation loop if the protected variable stopOnFirstFailure is true. Would it be possible to implement these in Laravel 7 by extending/overiding the Validator.php class? I've been researching about extending core Laravel classes and stumbled across this article but it is a bit confusing since the article only showed how to override a specific function. In my case I need to declare a protected variable, override one function and declare one new function.

Laravel 8 Validator.php code:

Declaring variable:

/**
     * 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 function:

/**
     * 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;
                }
            }
        }

EDIT: Validator is used in my code by Form Request. Sample of my code:

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);
    }
}

EDIT: Followed @thefallen's advice, here is what I did. My CustomValidator.php class in CustomClass inside app directory:

<?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();
    }
}

My ValidatorFactory inside CustomClass folder

<?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);
    }
}

My 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()
    {
        //
    }
}


Solution

  • You basically need to extend the Validator to make changes on that method and then make your own Validation Factory to create this new Validator instead of the default one. So step one with you own validator:

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

    Then you need a Validation Factory which will be creating this new class, this will also extend the default one:

    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);
        }
    }
    

    Finally inside app/Providers/AppServiceProvider.php in the register() method you need to swap the default factory with your custom one:

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

    Note that validator is the name of the binding (or alias) of Illuminate\Validation\Factory. And you should be good to go and be able to make any changes on the validator.