Laravel 5.4 - How to use multiple error messages for the same custom validation rule

后端 未结 4 1826
失恋的感觉
失恋的感觉 2021-02-19 21:40

In order to reuse code, I created my own validator rule in a file named ValidatorServiceProvider :

class ValidatorServiceProvider extends Servic         


        
4条回答
  •  情歌与酒
    2021-02-19 22:18

    Poor handling of custom validation rules is why I ditched laravel (well, it was one of many reasons, but it was the straw that broke the camel's back, so to speak). But anyway, I have a three part answer for you: a reason why you don't want to do this in this specific case, a quick general overview of the mess you have to deal with, and then the answer to your question in case you still want to do it.

    Important security concern

    Best security practices for managing logins dictate that you should always return one generic error message for login problems. The quintessential counter-example would be if you returned "That email is not registered with our system" for an email-not-found and "Wrong password" for a correct email with the wrong password. In the case where you give separate validation messages, you give potential attackers additional information about how to more effectively direct their attacks. As a result, all login-related issues should return a generic validation message, regardless of the underlying cause, something to the effect of "Invalid email/password combination". The same is true for password recovery forms, which often say something like, "Password recovery instructions have been sent to that email, if it is present in our system". Otherwise you give attackers (and others) a way to know what email addresses are registered with your system, and that can expose additional attack vectors. So in this particular case, one validation message is what you want.

    The trouble with laravel

    The issue you are running into is that laravel validators simply return true or false to denote whether or not the rule is met. Error messages are handled separately. You specifically cannot specify the validator error message from inside your validator logic. I know. It's ridiculous, and poorly planned. All you can do is return true or false. You don't have access to anything else to help you, so your pseudo code isn't going to do it.

    The (ugly) answer

    The simplest way to create your own validation messages is to create your own validator. That looks something like this (inside your controller):

    $validator = Validator::make($input, $rules, $messages);
    

    You would still have to create your validator on boot (your Valiator::Extend call. Then you can specify the $rules normally by passing them in to your custom validator. Finally, you can specify your messages. Something like this, overall (inside your controller):

    public function login( Request $request )
    {
        $rules = [
            'email' => 'bail|required|checkEmailPresenceAndValidity'
        ]
    
        $messages = [
            'checkEmailPresenceAndValidity' => 'Invalid email.',
        ];
    
        $validator = Validator::make($request->all(), $rules, $messages);
    }
    

    (I don't remember if you have to specify each rule in your $messages array. I don't think so). Of course, even this isn't very awesome, because what you pass for $messages is simply an array of strings (and that is all it is allowed to be). As a result, you still can't have this error message easily change according to user input. This all happens before your validator runs too. Your goal is to have the validation message change depending on the validation results, however laravel forces you to build the messages first. As a result, to really do what you want to do, you have to adjust the actual flow of the system, which isn't very awesome.

    A solution would be to have a method in your controller that calculates whether or not your custom validation rule is met. It would do this before you make your validator so that you can send an appropriate message to the validator you build. Then, when you create the validation rule, you can also bind it to the results of your validation calculator, so long as you move your rule definition inside of your controller. You just have to make sure and not accidentally call things out of order. You also have to keep in mind that this requires moving your validation logic outside of the validators, which is fairly hacky. Unfortunately, I'm 95% sure there really isn't any other way to do this.

    I've got some example code below. It definitely has some draw backs: your rule is no longer global (it is defined in the controller), the validation logic moves out of the validator (which violates the principle of least astonishment), and you will have to come up with an in-object caching scheme (which isn't hard) to make sure you don't execute your query twice, since the validation logic is called twice. To reiterate, it is definitely hacky, but I'm fairly certain that this is the only way to do what you want to do with laravel. There might be better ways to organize this, but this should at least give you an idea of what you need to make happen.

    checkLogin( $value ) === true ? true : false;
    
            });
        }
    
        public function checkLogin( $email ) {
            $user = User::where('email', $email)->first();
    
            // Email has not been found
            if (! $user) {
                return 'not found';
            }
    
            // Email has not been validated
            if (! $user->valid_email) {
                return 'invalid';
            }
    
            return true;
        }
    
        public function login( Request $request ) {
    
            $rules = [
                'email' => 'bail|required|checkEmailPresenceAndValidity'
            ]
    
            $hasError = $this->checkLogin( $request->email );
            if ( $hasError === 'not found' )
                $message = "That email wasn't found";
            elseif ( $hasError === 'invalid' )
                $message = "That is an invalid email";
            else
                $message = "Something was wrong with your request";
    
    
            $messages = [
                'checkEmailPresenceAndValidity' => $message,
            ];
    
            $validator = Validator::make($request->all(), $rules, $messages);
    
            if ($validator->fails()) {
                // do something and redirect/exit
            }
    
            // process successful form here
        }
    }
    

    Also, it is worth a quick note that this implementation relies on $this support for closures, which (I believe) was added in PHP 5.4. If you are on an old version of PHP you'll have to provide $this to the closure with use.

    Edit to rant

    What it really boils down to is that the laravel validation system is designed to be very granular. Each validation rule is specifically only supposed to validate one thing. As a result, the validation message for a given validator should never have to be changed, hence why $messages (when you build your own validator) only accepts plain strings.

    In general granularity is a good thing in application design, and something that proper implementation of SOLID principles strive for. However, this particular implementation drives me crazy. My general programming philosophy is that a good implementation should make the most common uses-cases very easy, and then get out of your way for the less-common use-cases. In this cases the architecture of laravel makes the most common use-cases easy but the less common use-cases almost impossible. I'm not okay with that trade off. My general impression of Laravel was that it works great as long as you need to do things the laravel way, but if you have to step out of the box for any reason it is going to actively make your life more difficult. In your case the best answer is to probably just step right back inside that box, i.e. make two validators even if it means making a redundant query. The actual impact on your application performance likely will not matter at all, but the hit you will take to your long-term maintainability to get laravel to behave the way you want it will be quite large.

提交回复
热议问题