Identifying validation duplication0:00
Hello everyone. In our example application we have a UserController with two controller actions, a store action to create a new User and an update action to update an existing User. Both actions include some validation code to validate the input coming from the request using the Laravel validator and then we have some custom checks, one to make sure that the language the User selected is actually supported by our application and the other to make sure that the email provided is a business email address. And finally we store the model. The update controller action is basically the same, run validation, custom validation rules, and then we fill the attributes and store the model. This controller looks okay, but let's see if we can make it even better. We have one obvious problem here that there is a lot of duplicated code, especially the code related
This controller looks okay, but let's see if we can make it even better. We have one obvious problem here that there is a lot of duplicated code, especially the code related to validation. And the other problem is that the custom validation rules that we have runs after the Laravel validator. And that means if the user, for example, provided an invalid email address as well as an invalid language, he will get an error message first telling him the email address is invalid. He will fix it and submit the form again and then he will get another message saying the language is invalid, which is not the best user experience. It's better to share with the user all the errors we have in one go so he does the fix in one go. So to fix that, I'm going to extract the validation code into a different class. I'm going to create a new directory in our app directory and call it validators. Inside
Creating a UserValidator1:25
In one go. So to fix that, I'm going to extract the validation code into a different class. I'm going to create a new directory in our app directory and call it validators. Inside the validators directory, I'm going to create a PHP class and call it UserValidator. Inside the UserValidator, we will add two methods, a rules method and a validate method. Inside the rules method, we are going to return an array of rules and we will get these rules from the UserController in a bit. Inside the validate method, we are just going to call the validator Laravel helper, provide the data and the rules and then call the validate method. Now let's open the UserController and copy the validation rules from there. And before we do anything else, I'm going to replace this validation rules with the array form. I'm not sure about you, but this looks much cleaner and much easier to change.
Extracting ValidLanguage rule2:09
And before we do anything else, I'm going to replace this validation rules with the array form. I'm not sure about you, but this looks much cleaner and much easier to change in the future. Another thing we're going to do is to replace the string form here with the unique helper, which is much easier to read. Now in the UserController, we can replace the validation code with the call to the UserValidator, provide the attributes from the request and the validator will take care of validating those attributes. Now let's take a look at the first custom validation rule. Instead of just copying this code to the UserValidator, we are going to use a custom validation rule. And to create a custom validation rule, all we need to do is run php artisan make:rule ValidLanguage and we'll call it ValidLanguage. Now if we open the ValidLanguage class, we can see that there is a passes method and a message
all we need to do is run php artisan make:rule and we'll call it ValidLanguage. Now if we open the ValidLanguage class, we can see that there is a passes method and a message method. In the passes method, we are going to copy our validation code and replace the reference to request by the value. We will also replace the validation message. Now back to the UserValidator, we can add the new custom rule to the language attributes. Now we can remove this from our controller. If we take a look at the second custom rule, it seems like we can use a custom validation rule as well. However, this rule involves two attributes, not one. And for that we are going to use the after validation hook that's provided by the Laravel validators. Inside this hook, we are just going to call a new method called validateBusinessEmail, and we'll copy the validation rule to there. And
Handling update validation differences3:34
provided by the Laravel validators. Inside this hook, we are just going to call a new method called validateBusinessEmail, and we'll copy the validation rule to there. And we will replace references to $request by using the $data parameter. Now we can remove this code as well. Much better. Let's move to the update method and see if we can do the same. Let's close the UserValidator for a second and see what we have here. There are several differences in the update method. First we use a sometimes flag when this flag is added, we inform the Laravel validator to not require this attribute if it's not provided. So for example, if the User provided a name, he must fill that name, he cannot provide an empty string. However, if he didn't provide a name at all, we will not force him to provide one that way we can only ask the users to provide the attributes that they want to update. Another
string. However, if he didn't provide a name at all, we will not force him to provide one that way we can only ask the users to provide the attributes that they want to update. Another thing is the unique rule here we are ignoring the existing User ID so that the user can provide his existing email address and the Laravel validator will not error. Also in the rule where we check the business email address, we make sure that we fall back to the existing values if we do not have one coming from the request. That way, if the user provided an email only or our business website only, the validation will run giving the existing new value as well as any existing value. And finally, only an admin User can update the admin attribute of another User. In order to use the User validator with the update method, we need to provide an instance of the existing User to be able to make decisions inside the User
of another User. In order to use the user validator with the update method, we need to provide an instance of the existing User to be able to make decisions inside the user validator about what to do if the User is an existing one or a newly created one. So what we are going to do is go to the validate method of the user validator and require a user attribute, pass it to the rules method and also to the custom validation rule. Now we need to conditionally add the sometimes rule to the attributes that are required based on whether or not the User exists. So we are going to add this here and on every other attribute that is required. Now to fix the unique rule, we are going to ignore the user ID if the User exists, so the User can provide the same email address. And finally, for our custom validation rule here, we are going to fall back to the data already stored in
Using validated data for fill5:49
ID if the User exists, so the User can provide the same email address. And finally, for our custom validation rule here, we are going to fall back to the data already stored in an existing User. Now let's go back to the UserController and provide an instance of the User model to our validator. So here we can remove that. And in the update method, we are going to replace all the validation code with calls to our UserValidator and pass the User instance here. Now the controller looks much slimmer. One final thing I would like to do here, and I'm not sure about you, but here we need to fill each and every single attribute explicitly so we don't hit the mass assignment protection of our User models. I think it would be much better to disable the mass assignment protection at the model level and provide the validated data only to the User model. Inside the validate method
I think it would be much better to disable the mass assignment protection at the model level and provide the validated data only to the User model. Inside the validate method of our UserValidator, we return the value coming from the validate method of the Laravel validator. The validate method of the Laravel validator is going to return the validated attributes, meaning that it will only return the attributes that we added in our rules method. We can use that to get only the validated attributes. And instead of filling each attribute explicitly, we are just going to call the fill method, provide the validated data and save, and we are going to do that in our update method as well. The only exception in the update method is that we want to only pass the isAdmin attribute if the User is an admin. So we will use the except method on the request to exclude the isAdmin attribute from the
Disabling mass assignment guard7:19
update method is that we want to only pass the isAdmin attribute if the User is an admin. So we will use the except method on the request to exclude the isAdmin attribute from the model if the User is not an administrator. And so that's our final result, a much better, much cleaner controller. And if we have another controller that interacts with the User, let's say a RegisterController, we can replace all the duplicated code with our UserValidator. And if for example, you are adding Users to your database from a spreadsheet, you can use the same UserValidator to validate the input. So you won't have duplicated code. One final thing that we need to do here is to disable the mass assignment protection in our User model. So we are replacing the fillable property with a guarded property and only guard the UserID that way all the other fields are going to be fillable.
in our User model. So we are replacing the fillable property with a guarded property and only guard the userId that way all the other fields are going to be fillable.
