5

So I have a Laravel Application, which has many Controllers to handle various aspects of the applications.

Now each controller has various methods. Most of the methods have validations rules defined such as:

$validationArray = [
    'id'=>'required|integer',
    'status'=>'required|string'
];
$validator = Validator::make($request->all(),$validationArray);
if ($validator->fails()){
    return Response::json(['response'=>implode(', ',$validator->messages()->all())],422);
}

Now the following line:

return Response::json(['response'=>implode(', ',$validator->messages()->all())],422);

actually returns whatever is wrong with the validation rules.

My question is: Is there any way to get all possible error messages programmatically?

Of course, one way to do it is going around the rule by rule and make a list manually but there are hundreds of the methods scattered over various controllers.

So, if anyone could point me in the direction of taking all the error messages in some easier way, would be much appreciated.

Thank you in advance!

UPDATE

So to clear further I need a list of all possible errors, like for above code the list will be like:

['id is required', 'id must be an integer', 'status is required', 'status must be an string']

UPDATE 2

Please keep in mind that there are hundreds of methods and also I do not want to change the final response of the method but to have some sort of external script which can help me getting the error messages without interfering with the controllers much.

Abhay Maurya
  • 11,819
  • 8
  • 46
  • 64
  • You mean so that you don't have to have this line all over the place? `Response::json(['response'=>implode(', ',$validator->messages()->all())],422);` – Oluwatobi Samuel Omisakin Oct 10 '19 at 07:21
  • No, that is all over the place :) thats how we handle validation errors in most of controller methods. I want to get a list of all possible errors which a controller method can generate. So if a controller has code like I mentioned in my question, how can i get all possible errors from it like a list of all errors like ['id is required', 'id must be an integer', 'status is required', ..so on] – Abhay Maurya Oct 10 '19 at 07:25
  • Did you mean the validation messages Laravel uses? Look at `resources/lang/en/validation.php` this is where Laravel keeps all the validation messages matching their rules. Is this what you're referring to? – Oluwatobi Samuel Omisakin Oct 10 '19 at 09:20
  • @OluwatobiSamuelOmisakin it sounded simpler in my mind I believe as everyone is having hard time understanding the request. I try again to explain. Yes I am reffering to the validation messages Laravel uses. but i dont need those static ones, I want to have validation error messages for the validation i do in my controller method. And I want to have a way to extract those messages for each of my controller methods. I even gave an example in my question – Abhay Maurya Oct 10 '19 at 09:25
  • 1
    hmm. I really do not understand. If you don't mind, what exactly are you trying to achieve? There might be simpler way to get there. – Oluwatobi Samuel Omisakin Oct 10 '19 at 09:37
  • yup never mind, I literally can't explain it more... I even gave example like for what validation rule, what result I expect..i am sorry. – Abhay Maurya Oct 10 '19 at 09:59
  • You can't. The validator will stop at the first error, so all messages won't be there. For example: `status => 'required|string'` will only throw the message 'status is required' if it's not present. It won't bother to test if it's a string or not since the validation already failed – IGP Oct 17 '19 at 02:01
  • @IGP I think it depends. If you start your validation string with `bail` that is when the validation will halt any time it fails. 'required' validation may have different behavior though. – Oluwatobi Samuel Omisakin Oct 21 '19 at 07:11

6 Answers6

5

In order to do that you have to extend Validator class and write a method that will iterate all rules and explicitly add error messages as if they failed.

First, create a new file app\Http\Custom\Validator.php:

<?php

namespace App\Http\Custom;

use Illuminate\Contracts\Validation\Rule as RuleContract;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationRuleParser;
use Illuminate\Validation\Validator as BaseValidator;

class Validator extends BaseValidator {

  /** @var MessageBag */
  protected $errorMessages;

  /** @var array */
  protected $hasExplicitFileErrorMessage;

  protected $explicitFileRules = [
    'File', 'Image', 'Mimes', 'Mimetypes', 'Dimensions',
  ];

  function availableErrors()
  {
    $this->errorMessages = new MessageBag();
    $this->hasExplicitFileErrorMessage = [];

    foreach($this->rules as $attribute => $rules) {
      $attribute = str_replace('\.', '->', $attribute);
      foreach($rules as $rule) {
        [$rule, $parameters] = ValidationRuleParser::parse($rule);

        if($rule == '') {
          continue;
        }
        if(($keys = $this->getExplicitKeys($attribute)) &&
          $this->dependsOnOtherFields($rule)) {
          $parameters = $this->replaceAsterisksInParameters($parameters, $keys);
        }
        // explicitly add "failed to upload" error
        if($this->hasRule($attribute, $this->explicitFileRules) && !in_array($attribute, $this->hasExplicitFileErrorMessage)) {
          $this->addFailureMessage($attribute, 'uploaded', []);
          $this->hasExplicitFileErrorMessage[] = $attribute;
        }

        if($rule instanceof RuleContract) {
          $messages = $rule->message() ? (array)$rule->message() : [get_class($rule)];
          foreach($messages as $message) {
            $this->addFailureMessage($attribute, get_class($rule), [], $message);
          }
        } else {
          $this->addFailureMessage($attribute, $rule, $parameters);
        }
      }
    }

    return $this->errorMessages->all();
  }

  function addFailureMessage($attribute, $rule, $parameters = [], $rawMessage = null)
  {
    $this->errorMessages->add($attribute, $this->makeReplacements(
      $rawMessage ?? $this->getMessage($attribute, $rule), $attribute, $rule, $parameters
    ));
  }

  // we have to override this method since file-type errors depends on data value rather than rule type
  protected function getAttributeType($attribute)
  {
    if($this->hasRule($attribute, $this->explicitFileRules)) {
      return 'file';
    }
    return parent::getAttributeType($attribute);
  }
}

Next, let's register this class in Validation factory:

<?php

namespace App\Providers;

use App\Http\Custom\Validator; // <-- our custom validator
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider {

  public function boot()
  {
    app('validator')->resolver(function ($translator, $data, $rules, $messages) {
      return new Validator($translator, $data, $rules, $messages);
    });
  }

}

And... that's all. Let's test it:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class HomeController extends Controller {

  function index(Request $request)
  {
    $rules = [
      'id'      => 'required|int|between:2,10',
      'status'  => 'required_with:nonexisting|string|email',
      'avatar'  => 'required|file|mimes:png|max:1000',
      'company' => 'required_without:id|unique:companies,id'
    ];

    $validator = Validator::make([], $rules);

    dump($validator->availableErrors());
  }

}
array:13 [▼
  0 => "The id field is required."
  1 => "The id must be an integer."
  2 => "The id must be between 2 and 10."
  3 => "The status field is required when nonexisting is present."
  4 => "The status must be a string."
  5 => "The status must be a valid email address."
  6 => "The avatar failed to upload."
  7 => "The avatar field is required."
  8 => "The avatar must be a file."
  9 => "The avatar must be a file of type: png."
  10 => "The avatar may not be greater than 1000 kilobytes."
  11 => "The company field is required when id is not present."
  12 => "The company has already been taken."
]
Styx
  • 9,863
  • 8
  • 43
  • 53
  • This is first useful answer I got so far :) so thank you! However, this requires to go to each controller method and dump the errors, so if I have 100 methods in the whole app for instance, I will have to edit 100 files to get the dump. So, I was hoping to have some sort of solution which could be more easily implementable. I will wait for 3 more days or so and if nothing better comes along, I will upvote and accept your answer and award the bounty reputation. Thanks again for the answer, I will also try to generalize the process suggested by you. – Abhay Maurya Oct 16 '19 at 12:31
  • @Learner Ah, when you said "to get all possible errors" you meant "from all methods of all controllers"? You need some code that will iterate over all methods of all controllers and collect all possible errors from their validation rules? – Styx Oct 16 '19 at 16:02
  • @Learner - I also found solution as per your need but it will require to make changes at two places in one core file which is obvious not recommended. If it is okay for you, I can share. – Sachin Vairagi Oct 17 '19 at 06:31
  • @Learner Got it. Well, it won't be too hard to expand my method to cover that. I'll do that in a few of hours. – Styx Oct 17 '19 at 06:41
  • no core file changes of course...thanks for the help, would be checking your updated answer once its done, please leave a comment here when you are done – Abhay Maurya Oct 17 '19 at 06:56
  • and would prefer if i didnt need to do any changes in the methods itself as I said there are a lot of them, but i understand if there is no other way – Abhay Maurya Oct 17 '19 at 06:59
  • @Learner No, no core changes or method changes. All from outside. I'll leave a comment when I do this. – Styx Oct 17 '19 at 07:19
  • Even though I have written an answer to the question I have to admit that @Styx solution does not require creating Service provider instead registering inside the `boot` method. This answer might be perfect if you could just override `messages()` method in that custom Validation class. – Oluwatobi Samuel Omisakin Oct 20 '19 at 01:10
  • Even though I will need to modify this answer in order to suit my need, but this answer goes the closest to what I actually needed. So Thanks, awarding bounty reputation – Abhay Maurya Oct 22 '19 at 08:42
1

It isn't pretty but here's my shot:

$validationArray = [
    'id'=>'required|integer',
    'status'=>'required|string'
];

$validator = Validator::make($request->all(), $validationArray);
if ($validator->fails()) {
    $messages = [];
    $invalid_fields = array_keys($validator->messages()->toArray());
    $rules = $v->getRules();

    foreach($invalid_fields as $invalid_field) {
        foreach($rules[$invalid_field] as $rule) {
            if(str_contains($rule, ':') {
                // complex rules that have parameters (min, between, size, format) 
                // are more difficult to work with. I haven't figured out how to do them yet
                // but you should get the idea. 
                continue;
            } else {
                $messages[] = str_replace(':attribute', $invalid_field, $validator->getTranslator()->get("validation.$rule"));
            }
        }
    }

    return Response::json(['response' => implode(', ', $messages)], 422);
}
IGP
  • 14,160
  • 4
  • 26
  • 43
  • Thank you for the response, good one. However, if you had multiple methods, you will need to go and add those line of codes in every method, plus this will also change the final result of each method which i dont want. please have a look at @Styx answer: https://stackoverflow.com/a/58409390/6935763 , you will notice that his approach is also similar. I will update the question to clear it better. But so far yours and styx answer were to the point at least – Abhay Maurya Oct 17 '19 at 06:10
  • You don't need to add those lines of code to every method. This is a proof of concept. You could put it all in a private/protected method on the BaseController so every controller has access to it. Or in a trait. I don't understand what you mean by 'change the final result' – IGP Oct 17 '19 at 08:22
1

Number 1: Like I mentioned in my comment under the question, what you're trying to achieve may be done in simpler way.

Number 2: Since you do not want to change your already written code where you got ->messages() then you could do the following. I will list the steps and provide an example code.

  • We need to override Laravel's validator, (Validation) Factory, and ValidationService provider classes.
  • In App\Services folder you can create two classes Validator and ValidationFactory
  • in App\Providers create a class ValidationServiceProvider
  • Go into config/app.php file and under providers replace Illuminate\Validation\ValidationServiceProvider::class with App\Providers\ValidationServiceProvider::class

Validator class looks like so:

namespace App\Services;

use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationRuleParser;
use Illuminate\Contracts\Translation\Translator;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Illuminate\Contracts\Validation\Rule as RuleContract;

class Validator extends \Illuminate\Validation\Validator
{
    /**
     * @var MessageBag $all_messages
     */
    protected $all_messages;

    public function __construct(Translator $translator, array $data, array $rules, array $messages = [], array $customAttributes = [])
    {
        parent::__construct($translator, $data, $rules, $messages, $customAttributes);

        $this->all_messages = new MessageBag;
        $this->getAllFormattedMessages();
    }

    public function makeAllRulesMessages($attribute, $rule, $parameters)
    {
        $this->all_messages->add($attribute, $this->makeReplacements(
            $this->getMessage($attribute, $rule), $attribute, $rule, $parameters
        ));
    }

    public function messages(bool $validated_rules_messages = false)
    {
        return $validated_rules_messages
            ? $this->validatedMessages()
            : $this->all_messages;
    }

    /**
     * This is here in case the true validated messages are needed
     *
     * @return MessageBag
     */
    public function validatedMessages()
    {
        return parent::messages();
    }

    public function getAllFormattedMessages()
    {
        // We'll spin through each rule and add all messages to it.
        foreach ($this->rules as $attribute => $rules) {
            $attribute = str_replace('\.', '->', $attribute);

            foreach ($rules as $rule) {
                // First we will get the correct keys for the given attribute in case the field is nested in
                // an array. Then we determine if the given rule accepts other field names as parameters.
                // If so, we will replace any asterisks found in the parameters with the correct keys.

                [$rule, $parameters] = ValidationRuleParser::parse($rule);

                if (($keys = $this->getExplicitKeys($attribute)) &&
                    $this->dependsOnOtherFields($rule)) {
                    $parameters = $this->replaceAsterisksInParameters($parameters, $keys);
                }

                $value = $this->getValue($attribute);

                if ($value instanceof UploadedFile && $this->hasRule($attribute, array_merge($this->fileRules, $this->implicitRules))
                ) {
                    $this->makeAllRulesMessages($attribute, 'uploaded', []);
                } elseif ($rule instanceof RuleContract) {
                     $this->makeCustomRuleMessage($attribute, $rule);
                } else {
                    $this->makeAllRulesMessages($attribute, $rule, $parameters);
                }

            }
        }
    }

    /**
     * @param $attribute
     * @param \Illuminate\Contracts\Validation\Rule  $rule $rule
     */
    public function makeCustomRuleMessage($attribute, $rule)
    {
        $this->failedRules[$attribute][get_class($rule)] = [];

        $messages = (array)$rule->message();

        foreach ($messages as $message) {
            $this->all_messages->add($attribute, $this->makeReplacements(
                $message, $attribute, get_class($rule), []
            ));
        }
    }
}

This class does one thing in summary, get all the messages of the passed rules into $all_messages property of the class. It extends and allows the base validation class run, and simply overrides messages() method to make all the collected rules available for use.

ValidationFactory overrides Illuminate\Validation\Factory and it looks like so:

namespace App\Services;

use Illuminate\Validation\Factory;

class ValidationFactory extends Factory
{
    /**
     * Resolve a new Validator instance.
     *
     * @param  array  $data
     * @param  array  $rules
     * @param  array  $messages
     * @param  array  $customAttributes
     * @return \Illuminate\Validation\Validator
     */
    protected function resolve(array $data, array $rules, array $messages, array $customAttributes)
    {
        if (is_null($this->resolver)) {
            return new \App\Services\Validator($this->translator, $data, $rules, $messages, $customAttributes);
        }

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

This class does only one thing, overrides resolve() method in this class by making use of the instance of our custom \App\Services\Validator class instead.

ValidationServiceProvider extends Illuminate\Validation\ValidationServiceProvider and overrides registerValidationFactory() method and it looks like so:

namespace App\Providers;

use App\Services\ValidationFactory;
use Illuminate\Validation\ValidationServiceProvider as BaseValidationServiceProvider;

class ValidationServiceProvider extends BaseValidationServiceProvider
{

    protected function registerValidationFactory()
    {
        $this->app->singleton('validator', function ($app) {
            $validator = new ValidationFactory($app['translator'], $app);

            // The validation presence verifier is responsible for determining the existence of
            // values in a given data collection which is typically a relational database or
            // other persistent data stores. It is used to check for "uniqueness" as well.
            if (isset($app['db'], $app['validation.presence'])) {
                $validator->setPresenceVerifier($app['validation.presence']);
            }

            return $validator;
        });
    }
}

What the above class does is also to instruct the provide to make use of our App\Services\ValidationFactory whenever the app requires one.

And we are done. All validation messages will be shown even if one of our validation rules failed.

Caveats

In order to achieve this, we needed to make a lot of changes and overriding. Except really critical this may signal that something about the app's design looks wrong.

Laravel validation implementation may change in future release and therefore may become a problem maintaining these changes.

I cannot tell if there are other side effects that might happen for overriding Laravel's default validation implementation or if all the rules return the right messages.

Normally you only want to return failed validation messages to user rather than all the possible failures.

Community
  • 1
  • 1
  • I came to realise that there are few differences between my answer and this https://stackoverflow.com/a/58409390/5704410 but even if the answer doesn't get accepted I'd still leave it here for reference purpose. – Oluwatobi Samuel Omisakin Oct 20 '19 at 01:11
  • 1
    Thank you for the effort but the other one came the closest to my requirements so I have accepted that one..but I have upvoted this too. Thank you again! – Abhay Maurya Oct 22 '19 at 08:43
  • Good to know. But I hope you realise that overriding `messages()` method would ensure you don't have to change anything in already written validation code? – Oluwatobi Samuel Omisakin Oct 22 '19 at 12:26
  • I know but I cant do that as it will change the errors which API returns to the front end and I do not want to do that. The answer which I accepted is not perfect either..i will have to work further to actually adapt it the way i need it – Abhay Maurya Oct 22 '19 at 12:33
0

I think that functions failed() (get the failed validation rules) or errors() (get the message container for the validator) may help you. If it does not - go to https://laravel.com/api/5.8/Illuminate/Validation/Validator.html and I hope that you find needed function.

0

I think you are looking for a way to have custom error messages. if this is the case then the answer is like this:

$messages = [
    'id.required' => 'id is required',
    'id.integer' => 'id must be an integer',
    'status.required' => 'status is required',
    'status.string'=> 'status must be an string'
];

$validationArray = [
    'id'=>'required|integer',
    'status'=>'required|string'
];

$validator = Validator::make($request->all(),$validationArray, $messages);

more info you can find here.

I hope this is what you are looking for and my answer is helping you:)

Morpheus_ro
  • 503
  • 7
  • 23
  • Thank you for your help, I wish it was that simple and no I am not looking for custom error messages, I am looking for getting all the default ones in one place. – Abhay Maurya Oct 16 '19 at 06:00
0

Based on the Laravel Form Validation Procedure you can write the statement by following:

 $validationArray = [
   'id'=>'required|integer',
   'status'=>'required|string'
 ];

 $validator = Validator::make($request->all(),$validationArray);

 if ($validator->fails()){
  return Response::json(['response'=> validator->errors())],422);
 }

Where errors() method return all the errors as associative array where the message will be associate with the field name accordingly and that's how you can get the errors.

HOSSAIN AZAD
  • 104
  • 4