Laravel’s Request Validation decoded
Are you looking for a slick way for validating your incoming requests on your PHP web application? Or you are already using Laravel’s Request Validation, but you want to have deeper understanding on what is going on? Then you are at the right place, because I will try to break this down and explain how the Laravel’s Request Validation works under the hood.
History and background
Before we get into details and concrete examples, I think it is a good idea to explain what is the Request Validation. In my opinion, the name is self-explanatory. Simply put, we have an incoming request to our application, and input we receive should match certain rules (validate it) in order to continue and process the request.
If this is a request on our login/registration page, we need to make sure that we have, for example, required values for username and password.
If this is a request on our administration dashboard, probably we will need some pagination parameters. It doesn’t matter of which CRUD method type is the request, at some point we will have to do some validation on it.
Let me provide a bit history. Before the time of modern frameworks, things were not that well organized. It required a bit more lines of code to be written in order to do simple validation of the incoming data (even though today there a more files of code instead of lines doing that for you, but at least you do not have to maintain it as part of your application). I have a simple registration attempt validation from a very old application that was refactored.
Here we can see simple validation for attempt to register a user using a simple form that consists of username
, password
and a password_confirmation
that should match the password
field. At first look this doesn't look that bad, but if we had 10 more fields, and validating for instance password
strength, length, username
rules to have only certain characters, length, to be unique, the options are limitless. You might have also seen this picture. It is valid representation, and I would highly recommend to avoid this.
If you don’t understand where are the $_POST
, $_SESSION
variables coming from, you should probably check this out and my suggestion would be to start learning plain PHP code, and then compare how things are improved and handled in modern frameworks. In my opinion, that is the way to learn how things work and understand why things are built in the way they are. Just my two cents.
Nowadays, most of the modern frameworks will include some sort of “validation package”, that will give you convenient way to validate the incoming requests. Since I have been working with Laravel for a while now, I will just show example how things improved for me when I switched over to Laravel 4.2 from validations similar to the example above. Please note that this example is for Laravel 4.2 (since 2014), and Laravel is now at version 8.x.
So framework does really help to have a good and easy validation. Everything looks nice and verbose. We are creating a $validator
instance, which will use the input from the request (basically using the data from $_POST
or $_GET
variables behind the scenes to take the values), and we apply some rules to check that username
and password
are mandatory fields, username
should be only with alphabetic characters and to not exists in the users
table (based on the unique:users
rule), the password
should be minimum 8 characters and confirmed with a password_confirmation
field. You can check the validation options here. So it is really big improvement. But this doesn't seem magical at all, since it makes sense what is going on, it is verbose. But let's see the latest implementation of the request validation.
So even less lines in the controller. Now it is clear when we first see the controller, that we just work with the data ($attributes
) which has been previously validated. And we can always trust this. But let's also see what do we have in the CreateNewUserRequest
, since we don't see the previously defined rules anywhere now.
So one method rules
which should return an array of rules. We have dedicated class that has only one job, to validate the request somehow. Now the RegisterController
doesn't know nothing about how the request is validated, how the Validator
is created. The controller can just take the validated input, and probably pass it to some business logic layer to register the user.
Please note that this is not the only way to validate using the Laravel framework, nor this is not the only validation strategy. You can check the Laravel’s Validation here.
But since we don’t see the rules()
method being called anywhere in the example, let's dig into the Laravel's source code and figure out how and when the request is validated.
Dependency Injection via method
If you are familiar with Dependency Injection, you are probably now ahead of the curve, and you are aware that the CreateNewUserRequest
is resolved using the Laravel's Service Container. And yes, that is correct, CreateNewUserRequest
is injected in the store
method using Dependency Injection, but we are never using the rules()
method.
You can check my post where I have explained how Dependency Injection works behind the scenes
So the rules()
method is nowhere called in our application, but if we try and hit our endpoint to register new user, it will fail due to validation error. Let's verify that.
First let’s define API endpoint that is calling the controller store
method. I am using API example just because I find it easier and more understandable to explain the error and validation instead of using redirects on error, but the example applies on both.
And for the sake of simplicity, the response would be validated attributes from the request.
So, just a quick conclusion, even though we do not need to call the rules()
method from the CreateNewUserRequest
, the request is being validated. So let's check if we provide correct attributes, just a sanity check.
Please note that the
unique:users
rule will require database connection set in order to check the database for theusername
value.
As seen above, everything seems to work properly. Request is being validated, and the validated attributes are returned as response. Please note that the just_extra_field
is not in the response since it has no validation defined in the rules()
method. That is just something that the validated()
method is doing for you.
Let’s see now how this magical feature works behind the hood
Laravel’s FormRequest class
Before we start digging into the Laravel’s source code, let’s just double check and see what we know from the code we wrote.
- Our
CreateNewUserRequest
class is extending theIlluminate\Foundation\Http\FormRequest
. This is what we know from Laravel's doc - We have
rules()
method that is being called somewhere when resolving theCreateNewUserRequest
. This validation is executed before thestore
method starts with execution from theRegisterController
- We have the
validated()
method called inside theRegisterController@store
method.
Okay so, let’s start by double checking the first obvious thing, the FormRequest
class from Laravel.
The first easy clue to track down and find, is the validated()
method. This method doesn't exists on our CreateNewUserRequest
, so that means it is coming from the FormRequest
. And we already know that we extend the FormRequest
with our CreateNewUserRequest
.
As it seems in the code, the validator instance is already there, and $this->validator->validated()
will only return the validated values of the rules we defined. But we are on the right path, since we found Laravel's Validator instance, so that means somewhere before this, the rules were used to create this instance. Let's check the usages of $this->validator
If you scroll bit down from the validated()
method inside FormRequest
there is the setValidator(Validator $validator)
method defined.
Since this method accepts whole instance, seems like tracking this method down will show how the validator instance was made, and where the rules()
method is used in the process. After checking the usages of the setValidator
method, it will give only one place where this method is used, and that is inside the getValidatorInstance()
method.
With a quick look at the method, seems like this is the place where the validator is being instantiated, and it seems like it is not that simple, since there are some if conditions. Let’s see what we have on our plate now.
- Seems like first there is a check if
$this->validator
is already instantiated. If it is, just return the instance, no need to re-create it again. - The
$factory
is the Laravel's Validator Factory, which know how the validator instance is being created. So this is needed to create the instance. - If there is a method on
$this
instance calledvalidator
, call that method, since Laravel will expect that we are going to create the validator on our own. In the process, is passing the$factory
as argument. - If there is no method called
validator
, just use the default validator creation process from Laravel using thecreateDefaultValidator
method. - After the validator is created, check if it the method
withValidator
exists. This is useful for applying extra configuration or tweaking on the created$validator
instance. - Then we see the method that led us here at the first place. The
$validator
property is being set. - Just return the created instance.
I know it is obvious, but I would like to state the obvious. The $this
variable is basically instance of CreateNewUserRequest
, but since we extend the FormRequest
, it inherits the capabilities.
Okay so, we know for a fact by just looking at the CreateNewUserRequest
class, that we do not have the method validator
. So we are not creating this instance on our own (even though Laravel is exposing this methods just to give more flexibility based on your needs, but we are not here for that). That means our validator instance is created using the createDefaultValidator
method. But let's do quick sanity check.
If I set a xDebug breakpoint, or just simply put a dd('here')
on line FormRequest:84
we are going to see that it will actually trigger this logic.
There we go. Let’s see the createDefaultValidator
method and finally see how the rules
method is being called.
This seems straight forward. We have the validator factory that if you check the Laravel’s Validation Documentation, you will see pretty much the same thing. In order to have a validation we need:
- Data that we need to validate. That is handled using the
validationData
method, from which we can see it is using everything that is passed as payload or query parameter calling theall()
method. - We need set of rules. And this is the part that should be explained better.
- Error messages when validation fails. Laravel is already having default message for each validation rule that they provide. In this case it is empty, since this is meant for custom messages for this validation only. You can override this method in your request class if you need some extra flexibility. The default ones are gonna be used by default from
resources/lang/en/validation.php
. You can read more here. - Custom attributes names. If you have a attribute called
really_long_and_non_meaningful_name
, you can provide a meaningful name for this attribute here, when the validation fails and maybe call itNice attribute
instead. You can override this method in your request class if you need some extra flexibility. This is empty since by default we do not need any custom attribute names. You can read more here.
We have all the cards on the table now, and we know what is going on here. We can focus on how the rules
method is being called right now. We can notice that the rules
method is being called using the $this->container->call()
method. That means Laravel's IoC Container (or Service Container) will decide how to make the call to this method. If you need some explanation on Dependency Injection and how the IoC Containers work behind the scenes, you can also check my other post where I go into details on this subject.
Let’s do quick sanity check. Let’s put dd(this->container->call([$this, 'rules']));
on line FormRequest:104
, and check the output. According to what we saw, this should return the array of validation rules defined inside CreateNewUserRequest@rules
.
So that is right. We see exactly what we expected. So why this is the case, why calling the method using the container, where you can simply do $this->rules()
and do the trick? It will give us the exactly same result.
Using the container vs direct call on the rules()
method
If we repeat the sanity check from above, and instead of dd($this->container->call([$this, 'rules']));
we simply put dd($this->rules())
the outcome is going to be 100% the same. The rules are going to be returned, and nothing will change. So what is so special about the $this->container->call()
method?
To explain that, let’s have a quick example with a small problem that should be solved. Let’s say we have config file config/settings.php
. In this file we just define some rules about our application, and for example we define that the only business types values we want to store to our database are private
and company
. We would like to validate for this values as well. But since having this in one place, it is easy, since you might hardcode this rules and not having the need of the config, but if this defined settings should be used in multiple request classes, than it is a problem, and the best way would be to use the config values as source of truth. So I am assuming as a Laravel user, first thing that will come in mind is to do the following:
And then the request
And that works just great. But there is one problem with this approach. To be honest, I really… let’s say I am not a fan of the Laravel’s global state. I think it is great to help beginners start building something awesome, learn, or just make fast MVPs. But I wrote previously about Dependency Injection, and now I am just using global functions. Not cool.
Let’s see how can we improve this, and explain the $this->container->call()
method that is used to get the rules to validate the request.
Much better. So, using the container to call the rules()
method from our CreateNewUserRequest
instance, is allowing us to easily use Dependency Injection on the rules()
method. If for some reason, Laravel's form request was using $this->rules()
instead of $this->container->call([$this, 'rules']);
, then this wouldn't be possible. And this is giving great flexibility while validating your request. In this example I am using Illuminate\Contracts\Config\Repository as ConfigRepository
for injection, but you can find the Laravel's contracts reference here, if you are not sure which interface you should inject. Or even check the facades reference here, but keep in mind that these are the concrete classes, you should check which interface are they implementing, if any. And of course, this also applies for your own classes or interfaces, you can inject anything that is resolvable by the container.
Okay so, quick recap before we continue.
- We define the validation rules in
CreateNewUserRequest@rules
method - We inject the
CreateNewUserRequest
intoRegisterController@store
method CreateNewUserRequest
is extendingFormRequest
FormRequest
is extending the main Laravel's request objectIlluminate\Http\Request
FormRequest
holds the logic to figure it out how it will create the validator (by calling child's classvalidator()
method if defined, or to create the default validator)FormRequest
while creating the validator will use the container to call theCreateNewUserRequest@rules
or any other class that is extending theFormRequest
- Since the container is responsible for calling the
rules()
methid, Dependency Injection is also supported.
But let’s see how the request validation is triggered without us having to do that manually. We saw that our only job is to inject the request class into the controller’s method, and that’s it. The request will be validated while the request class is being resolved, and the controller method will not even be triggered until the validation passes.
The magic behind the automatic request validation
In order to find out the logic responsible for request validation when the request class is being resolved, we are going to go through some of the Laravel’s Service Providers, or to be more specific Illuminate\Foundation\Providers\FormRequestServiceProvider
Let’s start with what is familiar for us, the FormRequest
class. We can see that inside the $this->app->resolving
method, it is defined that when the class name Illuminate\Foundation\Http\FormRequest
is resolving (since our request class is extending FormRequest
), the instance for this class name should be a FormRequest
class, which is created using the already existing incoming request instance (this is resolved before the resolving
method is called for FormRequest
). After the FormRequest
class is instantiated, container and redirector are passed to the instance using Dependency Injection via setter method. We already know one of the use case why the container is needed (calling the rules()
method).
So that seems straight forward, creating the instance, injecting some required instances, and that is our FormRequest
instance. But then, we never mentioned before the Illuminate\Contracts\Validation\ValidatesWhenResolved
interface, and we see that something is being done here with it. So let's see how this interface is actually used.
When we check the ValidatesWhenResolved
interface, there is only one method defined called validateResolved
. If we check the FormRequest
we are going to notice that it actually implements the ValidatesWhenResolved
interface, and if we try to find the validateResolved
method on the FormRequest
, we are going to find the implementation for this inside the trait Illuminate\Validation\ValidatesWhenResolvedTrait
that is also being used in the FormRequest
And the trait’s logic
prepareForValidation
acts like a before validation hook, we can extend this method for example onCreateNewUserRequest
if needed- Just like the
rules()
method,passesAuthorization
will try to callauthorize()
method on your request class, if this method is defined. You can check the usage here. - If the method
authorize()
exists, and return false, then thefailedAuthorization
will throw exception which results in403 Forbidden
HTTP response $instance
is the validator, that we explained above how it is instantiated. So it will take all data from the request and the rules defined in our request class, and check if the validation is correct.- If the validation fails,
failedException
will throw exception that will result in422 Unprocessable Entity
HTTP response. - Similar to
prepareForValidation
,passedValidation
acts like after validation hook.
Request validation Lifecycle
So just to sum it up. We define our route and the controller method that is responsible to handle the request. On our controller method, we inject the request class where we have defined our rules and authorization. We are now set to handle the request. When we have incoming request, Laravel’s router will try to resolve the controller’s method dependencies. One of the dependency is our request class, which when is resolved, based on the after resolving rule that is set in the FormRequestServiceProvider
for the ValidatesWhenResolved
interface, that is implemented by FormRequest
which is extended by our request class, it will call the validateResolved
method which is located in the ValidatesWhenResolvedTrait
and used in FormRequest
as well. The validateResolved
method will call the methods to try and authorize and validate the request. If any of this fails, it will results in exception. If the validation passes, then rest of the dependencies (if any) are going to be resolved from the controller's method, and the controller's method will eventually be executed. And that's basically it.
Summary
Please keep in mind that this is not the only method to validate request in Laravel. Always refer to the Laravel’s documentation and use whatever suits the best in your case. I find this request validation method really useful, and in my opinion, when broken down to tiny details it seems really simple, but it gives huge benefits while building your app. And yes, there is no magic into the request validation (or any other feature in general), only a lot of hours and effort to give that feel, since is really simple and easy to use.