Adding custom validation errors to Laravel form

There is one problem with the accepted answer (and Laravel's Validator in general, in my opinion) - the validation process itself and validation status detection is merged into one method.

If you blindly render all validation messages from the bag, it's no big deal. But if you have some additional logic that detects if the validator has failed or not and does additional actions (such as feeding international text messages for current validated form fields), then you have a problem.

Demonstration:

    // let's create an empty validator, assuming that we have no any errors yet
    $v = Validator::make([], []);

    // add an error
    $v->errors()->add('some_field', 'some_translated_error_key');
    $fails = $v->fails(); // false!!! why???
    $failedMessages = $v->failed(); // 0 failed messages!!! why???

Also,

    $v->getMessageBag()->add('some_field', 'some_translated_error_key');

yields the same results. Why? Because if you look into Laravel's Validator code, you will find the following:

public function fails()
{
    return ! $this->passes();
}

public function passes()
{
    $this->messages = new MessageBag;

As you can see, fails() method essentially clears away the bag losing all the messages you have appended, and thus making the validator assume that there are no errors.

There is no way to append errors to existing validator and make it fail. You can only create a new validator with custom errors like this:

    $v = Validator::make(['some_field' => null],
            ['some_field' => 'Required:some_translated_error_key']);
    $fails = $v->fails(); // true
    $failedMessages = $v->failed(); // has error for `required` rule

If you don't like the idea of abusing required validation rule for custom appended errors, you can always extend Laravel Validator with custom rules. I added a generic failkey rule and made it mandatory this way:

    // in custom Validator constructor: our enforced failure validator
    array_push($this->implicitRules, "Failkey");

    ...


/**
 * Allows to fail every passed field with custom key left as a message
 * which should later be picked up by controller
 * and resolved with correct message namespaces in validate or failValidation methods
 *
 * @param $attribute
 * @param $value
 * @param $parameters
 *
 * @return bool
 */
public function validateFailkey($attribute, $value, $parameters)
{
    return false; // always fails
}

protected function replaceFailkey($message, $attribute, $rule, $parameters)
{
    $errMsgKey = $parameters[0];

    // $parameters[0] is the message key of the failure
    if(array_key_exists($errMsgKey, $this->customMessages)){
        $msg = $this->customMessages[$parameters[0]];
    }       
    // fallback to default, if exists
    elseif(array_key_exists($errMsgKey, $this->fallbackMessages)){
        return $this->fallbackMessages[$parameters[0]];
    }
    else {
        $msg = $this->translator->trans("validation.{$errMsgKey}");
    }

    // do the replacement again, if possible
    $msg = str_replace(':attribute', "`" . $this->getAttribute($attribute) 
            . "`", $msg);

    return $msg;
}

And I can use it like this:

    $v = Validator::make(['some_field' => null],
            ['some_field' => 'failkey:some_translated_error_key']);
    $fails = $v->fails(); // true
    $failedMessages = $v->failed(); // has error for `Failkey` rule

Of course, that's still a hacky way to work around the issue.

Ideally, I would redesign the Validator to clearly separate its validation phase from status detection (separate methods for validate() and passes() or better isValid()) and also add convenience methods to manually fail specific field with specific rule. Although that also might be considered hacky, but still we have no other choice if we want to use Laravel validator not only with Laravel's own validation rules, but also our custom business logic rules.


See Darren Craig's answer.

One way to implement it though.

// inside if(Auth::validate)
if(User::where('email', $email)->first())
{
    $validator->getMessageBag()->add('password', 'Password wrong');
}
else
{
    $validator->getMessageBag()->add('email', 'Email not found');
}