Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

davestewart's avatar

Custom Validation issues

I'm having a bit of a mare with custom Validators in 5.0; two issues actually.

Issue 1 - A custom Validator won't pick up extend()ed rules

Because 5.0 is missing JSON validation that only comes in in 5.1, I have to use Validator::extend() to add a custom method. However, my own custom Validator does not seem to pick it up.

I don't want to be forced to add JSON validation to the custom validator, as that's not its job; I just need my own Validator to pick the extended functionality automatically.

Issue 2 - I can't work out how to use multiple validators

I'm also not really clear on how the Validator::resolve() method is supposed to work - the docs don't indicate how in the end, your code is supposed to target the custom validator you pass to the method.

In my case I require 2 different validators as I need separate sets of error messages, due to the context in how they will be used (one by the controls, the original at the top of the page) but it seems that the issue of multiple validators is a bit thorny, as outlined by this thread, which seemed to have to hack a solution in the end:

Can anyone (@bestmomo, @thepsion5, @imJonBon, @thepsion) shed any light, a year later?

Maybe there is another way to skin this cat?

0 likes
3 replies
davestewart's avatar

So I've finally got to the bottom of this, and I think it's a all a bit over-engineered - though I would love to hear differently - it may be that I'm just cross having spent the last 4 hours working all this out.

The key to this whole problem is that Validators HAVE to be made using Validator::make() Factory facade which creates the validator instance, then sets a bunch of properties on it, which otherwise could not be set - including imbuing new instances with the custom user validation callbacks I mentioned above.

This means you cannot just create a Validator instance using the "new" keyword - unless of course you start copying chunks of code from various classes, and including them in your custom validator in the constructor. If you don't do this, all you get a vanilla validation instance, without all the custom goodness.

This would be fine if there was a way to specify a Validator class to be used, for example a makeCustom(), but you can't - as internally, the make() method checks for an internal property resolver which is a callback that has to be set via Validator::resolver()at some earlier point in time.

This point of this closure is to give the user control to return a new Validator instance back to the factory, so it can have all these new properties added to it.

This is your one and only way to create a new Validator instance.

The callback has various parameters passed to it by the factory, all of which SHOULD be passed to your custom Validator, which then SHOULD be passed to the parent validator constructor (unless you want to override them, like I did to get custom messages):

function($translator, $data, $rules, $messages)
{
    return new CustomValidator($translator, $data, $rules, $messages);
}

Yes, this is all documented, but it's not particularly clear.

What also isn't clear is if you DO need a choice of validators, this is the ONLY place you have to make that choice - you get one closure for your app, and the choice MUST be made here. The problems with this are at least:

  • There's no context to help you decide what validator to create
  • As there's only one single resolver callback, there's no way to have different parts of your app, or different packages, update this function

The only ways round this I can see now are:

  • pass a dummy key in with the form $data and use this to decide the validator
  • call a mediating class, that is used externally to return a Validator instance
  • call the Validator::resolver() method immediately before calling the Validator::make() method, changing each time you need a new Validation instance

It seems to me that there are two solutions that would offer more flexibility:

  • Give the core Validator instance an initialize() method which sets up all the machinery, and lets you create "new" Validation instances
  • Add a public initialize() method to the factory that allows you to initialize any validation instance
  • Add a custom ::make()method to the factory that allows you to pass in a class reference to be "new"ed on your behalf

It just seems to me that this is yet another area which should be simple, or should have better / simpler documentation, or better source code comments.

Maybe I'm the first one to notice :(

davestewart's avatar
davestewart
OP
Best Answer
Level 11

I'm going to outline the full way to build multiple custom validators in this reply. Stand by...

EDIT: OK, so this is my final solution, rather than fighting against the wind (resolver function):

  • just extend the Validation Factory
  • allow a class reference to be passed in
  • reorder some of the existing functionality
  • expose some of this via some new public methods

Here' the code:

<?php namespace davestewart\resourcery\classes\validation;

use Illuminate\Contracts\Container\Container;
use Illuminate\Validation\Validator;
use Symfony\Component\Translation\TranslatorInterface;

/**
 * Custom Validation Class Factory
 *
 * @see \Illuminate\Validation\Factory
 * @see \Illuminate\Validation\ValidationServiceProvider
 *
 * @package davestewart\resourcery\classes\validation
 */
class Factory extends \Illuminate\Validation\Factory
{

    /**
     * Custom Validator Factory constructor
     *
     * @param TranslatorInterface $translator
     * @param Container           $container
     */
    function __construct(TranslatorInterface $translator, Container $container)
    {
        parent::__construct($translator, $container);
        $this->setPresenceVerifier(app('validation.presence'));
    }

    /**
     * Create a new Validator instance.
     *
     * @param   array       $class
     * @param   array       $messages
     * @param   array       $attributes
     * @return  Validator
     */
    public function makeCustom($class = Validator::class, array $messages = [], array $attributes = [])
    {
        /** @var Validator $validator */
        $validator = new $class($this->translator, [], []);
        $validator->setCustomMessages($messages);
        $validator->setAttributeNames($attributes);

        return $this->initialize($validator);
    }

    /**
     * Initialize any Validator instance from outside of the Factory
     *
     * @param Validator $validator
     * @return Validator
     */
    public function initialize(Validator $validator)
    {
        if ( ! is_null($this->verifier) ){
            $validator->setPresenceVerifier($this->verifier);
        }
        if ( ! is_null($this->container) ){
            $validator->setContainer($this->container);
        }
        $this->addExtensions($validator);

        return $validator;
    }

}

Usage:

// use
use davestewart\resourcery\classes\validation\Factory;

// create instance
$validator = app(Factory::class)->makeCustom(MyCustomValidator::class); // you can also set messages and attributes here

// set data and validate using standard methods
$validator->setData($input);
$validator->passes();

You can set / resolve the Factory as a singleton as well; Im just calling it via app() here as it needs to have constructor parameters injected.

Hope that helps folks :)

d3xt3r's avatar

I am lazy and wants my application to be the same, i.e. i don't want to register all the validators at once, but have a common class that validates my request and call the method accordingly.

A simple example using L5.2

//ApplicationServiceProvider.php

public function boot()
    {
        ValidatorFacade::extend('validate', function($attribute, $value, $parameters, $validator) {
            return  MyValidator::validate($attribute,$value,$parameters,$validator);
        });

        ValidatorFacade::replacer('validate', function($message, $attribute, $rule, $parameters) {
            return MyValidator::message($message, $attribute, $rule, $parameters);
        });
    }

// MyValidator class

class MyValidator
{
    /**
     * @var Validator
     */
    protected static $_instance;

    /**
     * @return Validator
     */
    protected static function getInstance()
    {
        if(static::$_instance === null)
            static::$_instance = new MyValidator();

        return static::$_instance;
    }

    /**
     * Validate against given values and parameters
     *
     * @param $attribute
     * @param $value
     * @param $parameters
     * @param $validator
     * @return bool
     */
    public static function validate($attribute, $value, $parameters, $validator)
    {
        $func = isset($parameters[0]) ? $parameters[0] : null;
        return $func !== null ? static::getInstance()->$func($value) : false;
    }

    /**
     * Generate validation error message for given attribute and
     * parameter
     *
     * @param $message
     * @param $attribute
     * @param $rule
     * @param $parameters
     * @return string
     */
    public static function message($message, $attribute, $rule, $parameters)
    {
        $rule = isset($parameters[0]) ? $parameters[0] : null;
        return trans('validation.extended.'.$rule,['attribute' => str_replace("_"," ",Str::ucfirst($attribute))]);
    }

    /**
     * Validate the phone number
     *
     * @param $value
     * @return bool
     */
    protected function phone($value)
    {
        try {

            $phone = PhoneNumberUtil::getInstance()->parse($value,null);
           return true;

        } catch (NumberParseException $e) {

            return false;
        }
    }
}

// and whenever required 

Validator::make($request->all(), [
    'phone' => 'validate:phone']);

Please or to participate in this conversation.