Data verification is a key component of any web application. It helps prevent security vulnerabilities, data corruption, and various other problems that may arise when using user input.
This article will explore what data verification is and why it is so important. We will compare client-side verification with server-side verification and explain why client-side verification should not be relied on.
We will then introduce some convenient verification rules that I commonly use in my Laravel application. Finally, we will learn how to create our own validation rules and test them to make sure they work as expected.
Data verification is a process of checking the validity of data before trying to use it. This can be a simple item to check for, for example, whether required fields exist in the request, or more complex checks such as whether the fields match a specific pattern or whether they are unique in the database.
Usually, when verifying data in a web application, if the data is invalid, you need to return an error message to the user.
This helps prevent security vulnerabilities, data corruption and improves data accuracy. Therefore, we will continue to process the request only if the data is valid.
Remember, you cannot trust any data from the user (at least before you verify it!).
There are many reasons why data verification is important, including:
One of the most important reasons to verify data in your application is to improve security. By verifying the data before using it, you can reduce the possibility that malicious input is used to attack your app or users.
Imagine that we expect a field to be an integer, but the user passes a file. This can cause various problems when we try to use that data elsewhere in the app.
To give another example, suppose you are building a web application that allows users to vote on voting. Voting can only be voted between AppModelsPoll
time and opens_at
time specified on the closes_at
model. What happens if someone accidentally sets the closes_at
time before the opens_at
time when setting up a voting? Depending on how you handle this in your app, this can cause various problems.
By validating data before storing it to the model, we can improve data accuracy in our application and reduce the possibility of incorrect data being stored.
In addition to being able to verify the data passed in HTTP requests, you can also verify the Artisan command. This prevents developers from accidentally entering invalid values and causing problems with the application.
Usually, you can use two types of verification in your application: client-side verification and server-side verification.
Client verification is verification performed in the browser before sending data to the server. It can be implemented using JavaScript or even HTML attributes.
For example, we can add some simple validation to the number field in HTML to ensure that the number entered by the user is between 1 and 10:
<input type="number" min="1" max="10" required>
This input field has four separate parts, which are useful for client verification:
type="number"
: This tells the browser that the input should be a number. On most browsers, this will prevent the user from entering anything other than the number. On mobile devices, it may even bring up a numeric keyboard instead of a regular keyboard, which is very beneficial for the user experience. min="1"
: This tells the browser that the number entered must be at least 1. max="10"
: This tells the browser that the number entered must be up to 10. required
: This tells the browser that the field is required and must be filled in before submitting the form. In most browsers, if the user tries to submit a form with an invalid value (or no value at all), the browser will block the form submission and display an error message or prompt to the user.
This is very beneficial for guiding users and improving the overall user experience of the application. But that's just that it should be considered: a guide. You should not rely solely on client authentication as the only form of verification in your application.
If someone opens the developer tools in their browser, they can easily remove and bypass the client verification you have set.
In addition, it is important to remember that when malicious users try to attack your app, they will usually use automated scripts to send requests directly to your server. This means that the client verification you have set will be bypassed.
Server-side verification is verification that runs in the application backend on the server. In the context of a Laravel application, this is usually a validation that runs in a controller or form request class.
Since the verification is on your server, the user cannot change it, this is the only way to really make sure that the data sent to the server is valid.
Therefore, be sure to always enable server-side verification in your app. Ideally, each field you try to read from the request should be verified before trying to use it to perform any business logic.
Now that we have understood what verification is and why it is important, let's see how to use it in Laravel.
If you've been using Laravel for a while, you'll know that Laravel has an amazing verification system built into the framework. Therefore, it is very easy to get started with verification in your app.
There are several common ways to verify data in Laravel, but we will cover two of the most common ways:
To manually verify the data (for example in the controller method), you can use the IlluminateSupportFacadesValidator
facade and call the make
method.
We can then pass two parameters to make
Method:
data
- The data we want to verifyrules
- The rules we want to verify data based on Side note: The make
method also accepts two optional parameters: messages
and attributes
. These can be used to customize error messages returned to the user, but we will not cover them in this article.
Let's look at an example where you might want to verify two fields:
<input type="number" min="1" max="10" required>
In the example above, we can see that we are validating two fields: title
and body
. We have hardcoded the values of these two fields to make the example clearer, but in real projects you usually get these fields from the request. We are checking whether the title
field is set, is a string, and has a maximum length of 100 characters. We also check if the description
field is set, is a string, and has a maximum length of 250 characters.
After creating the validator, we can call the method on the returned IlluminateValidationValidator
instance. For example, to check if the verification fails, we can call the fails
method:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
Similarly, we can also call validate
method on the validator instance:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
If verification fails, this validate
method will raise IlluminateValidationValidationException
. Laravel will automatically handle this exception based on the type of request made (assuming that you did not change the default exception handling in your app). If the request is a web request, Laravel will use errors in the session to redirect the user back to the previous page for you to display. If the request is an API request, Laravel returns a 422 Unprocessable Entity
response containing a JSON representation of the verification error as follows:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
Another common way to verify data in a Laravel application is to use the form request class. The form request class is a class with an extension IlluminateFoundationHttpFormRequest
that is used to run authorization checks and validation on incoming requests.
I find them a great way to keep the controller method tidy, because Laravel automatically runs verification on the data passed in the request before running the code of the controller method. Therefore, we don't need to remember to run any method on the validator instance ourselves.
Let's look at a simple example. Suppose we have a basic AppHttpControllersUserController
controller with a store
method that allows us to create a new user:
<input type="number" min="1" max="10" required>
In the controller method, we can see that we accept the AppHttpRequestsUsersStoreUserRequest
form request class (which we will introduce later) as method parameters. This will indicate to Laravel that we want to automatically run the verification in this request class when this method is called over HTTP request.
Then, we use the validated
method on the request instance in the controller method to get the verified data from the request. This means it will only return verified data. For example, if we try to save a new profile_picture
field in the controller, we must also add it to the form request class. Otherwise, the validated
method will not return it, so $request->validated('profile_picture')
will return null
.
Let's take a look at AppHttpRequestsUsersStoreUserRequest
Form request class:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
We can see that the request class contains two methods:
authorize
: This method is used to determine whether the user has the right to make a request. If the method returns false
, a 403 Forbidden
response is returned to the user. If the method returns true
, the verification rule will be run. rules
: This method is used to define the validation rules that should be run on the request. This method should return an array of rules that should be run on the request. In the rules
method, we specify that the name
field must be set, must be a string, and the maximum length must be 100 characters. We also specify that the email
field must be set, must be an email, and must be unique in the users
table (on the email
column). Finally, we specify that the password
field must be set and must pass the default password verification rules we have set (we will cover password verification later).
As you can see, this is a great way to separate the verification logic from the controller logic, and I found it makes the code easier to read and maintain.
As I already mentioned, the Laravel verification system is very powerful and can easily add verification to your app.
In this section, we will quickly introduce some convenient verification rules that I like, which I think most users will find useful in their apps.
If you are interested in viewing all the rules available in Laravel, you can find them in the Laravel documentation: https://www.php.cn/link/45d5c43856059a4f97d43d6534be52d0
One common type of validation you need to run is the validation array. This can be from verifying whether the passed ID array is all valid, to verifying whether the object array passed in the verification request has certain fields.
Let's look at an example of how to validate an array, and then we'll discuss what's being performed:
<input type="number" min="1" max="10" required>
In the example above, we are passing an array of objects, each with a name
and email
fields.
For verification, we first define that the users
field is set and is an array. Then, we specify that each item of the array (using users.*
direction) is an array containing the name
and email
fields.
Then, we specify that the name
field (using users.*.name
direction) must be set, must be a string and cannot exceed 100 characters. We also specify that the email
field (using users.*.email
directions) must be set, must be an email, and must be unique on the users
column of the email
table.
By being able to use the *
wildcard in the verification rules, we can easily verify the data array in our application.
Laravel provides some convenient date verification rules that you can use. First, to verify that a field is a valid date, you can use date
Rule:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
If you prefer to check if a date is in a specific format, you can use date_format
Rules:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
You may need to check if the date is earlier or later than another date. For example, suppose your request contains the opens_at
and closes_at
fields, and you want to make sure closes_at
is later than opens_at
and opens_at
is later than or equal to today. You can use after
Rules:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
In the example above, we can see that we have passed today
as a parameter to the opens_at
rule of the after
field. Laravel will try to convert this string into a valid strtotime
object using the DateTime
function and compare it to that object.
For the closes_at
field, we pass opens_at
as a parameter to the after_or_equal
rule. Laravel will automatically detect that this is another field being validated and compare the two fields to each other.
Similarly, Laravel also provides before
and before_or_equal
rules that you can use to check if a date is earlier than another date:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
As web developers, our job is to help users safe online. One way we can do this is to promote good password practices in our applications, such as requiring passwords to be of a certain length, contain certain characters, etc.
Laravel simplifies our work by providing a IlluminateValidationRulesPassword
class that we can use to verify passwords.
It comes with some methods we can link together to build the password verification rules we want. For example, suppose we want the user's password to meet the following criteria:
Our verification may look like this:
<input type="number" min="1" max="10" required>
As shown in the example, we are using a linkable method to build the password verification rules we want. But what happens if we use these rules in multiple different places (e.g., register, reset password, update password on your account page, etc.) and we need to change this verification to enforce at least 12 characters? We need to iterate through everything where these rules are used and update them.
To simplify this, Laravel allows us to define a default set of password verification rules that we can use throughout our application. We can define a set of default rules by using the AppProvidersAppServiceProvider
method like this in our boot
method: Password::defaults()
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
in the validation rule and use the rules we specified in Password::defaults()
for: AppServiceProvider
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
In the past, I had to use regular expressions (I admit I don't know much about it) to verify that the color is a valid color in hex format (e.g.
). However, Laravel now has a convenient #FF00FF
to use: hex_color
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
Suppose you want to allow users to upload PDF (.pdf) or Microsoft Word (.docx) files. The verification may look like this:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
method to specify the file type we want to allow. types
The
and min
methods can also accept strings containing other suffixes that indicate file size units. For example, we can also use: max
10kb
10mb
10gb
10tb
method on the IlluminateValidationRulesFile
class to make sure the file is an image: image
<input type="number" min="1" max="10" required>
In the example above, we are verifying that the file is an image, setting some minimum and maximum file size limits, and setting some maximum sizes (500 x 500 pixels).
You may want to take a different approach to file uploads in your app. For example, you might want to upload directly from the user's browser to cloud storage (e.g. S3). If you prefer to do this, you may want to check out my Upload Files in Laravel article using FilePond, which shows you how to do this, the different verification methods you may need to take, and how to test it.
Another common check you might want to do is to make sure that a value exists in the database.
For example, suppose you have some users in your app and you have created a route so you can batch assign them to teams. Therefore, in your request, you may need to verify that the user_ids
passed in the request exists in the users
table.
To do this, you can use the exists
rule and pass the table name you want to check if the value exists in it:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
In the example above, we are checking whether each ID passed in the user_ids
array exists in the users
column of the id
table.
This is a great way to make sure the data you are using is valid and exists in the database before trying to use it.
If you want to go one step further, you can apply the where
clause to the exists
rule to further filter the running queries:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
In the example above, we are checking whether each ID passed in the user_ids
array exists in the users
column of the id
table, and the user's is_verified
column is set to true
. Therefore, if we pass an unverified user ID, the verification will fail.
Similar to the exists
rule, Laravel also provides a unique
rule that you can use to check if the values in the database are unique.
For example, suppose you have a users
table and you want to make sure the email
field is unique. You can use unique
Rules:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
In the example above, we are checking whether the email
field is set, is an email, and is unique on the users
column of the email
table.
However, what happens if we try to use this verification on a profile page where the user can update their email address? Verification will fail because there is a row in the users
table containing the email address the user attempted to update to. In this case, we can use the ignore
method to ignore the user ID when checking uniqueness:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
If you do choose to use the ignore
method, you should be sure to read this warning in the Laravel documentation:
"You should never pass any user-controlled request input into the ignore
method. Instead, you should only pass a unique ID generated by the system, such as a self-increment ID or UUID from an Eloquent model instance. Otherwise, your application will be vulnerable to SQL injection attacks."
It may also be that sometimes you want to add additional unique
clauses to the where
rule. You may need to do this to make sure that the email address is unique to a specific team (which means another user in a different team can use the same email). You can do this by passing the closure to the where
method:
<input type="number" min="1" max="10" required>
Although Laravel comes with a lot of built-in validation rules, you may need to create custom validation rules to suit specific use cases.
Thank goodness, this is also easy to do in Laravel!
Let's see how to build custom validation rules, how to use it, and then how to write tests for it.
For the purposes of this article, we are not very concerned about what we are verifying. We just want to understand the general structure of creating custom validation rules and how to test them. Therefore, we will create a simple rule to check if the string is palindrome.
If you don't know, a palindrome is a sequence of words, phrases, numbers, or other characters that read the same in the forward and reverse directions. For example, "racecar" is a palindrome because if you invert the string, it is still "racecar". And "laravel" is not a palindrome, because if you invert the string it will be "levaral".
To start, we will first create a new validation rule by running the following command in the project route:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
This should create a new App/Rules/Palindrome.php
file for us:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
Laravel will automatically call the validate
method when running the rule. This method accepts three parameters:
$attribute
: The name of the property being verified. $value
: The value of the property being verified. $fail
: The closure you can call if the verification fails. Therefore, we can add our verification logic to the validate
method as follows:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
In the above rule, we just check if the value passed to the rule is the same as the value it reverses. If not, we will call the $fail
closure with the error message. This will cause the field to fail to verify. If the verification passes, the rule will not perform anything and we can continue to use our application.
Now that we have created the rule, we can use it in our app like this:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
Although this is a simple rule we created for demonstration purposes, I hope this will give you an idea of how to build more complex rules for your application.
Just like any other code in your app, it is important to test your validation rules to make sure they work as expected. Otherwise, you may risk using rules that do not work as expected.
To understand how to do this, let's take a look at how to test the palindrome rules we created in the previous section.
For this specific rule, we want to test two situations:
You may have more situations in more complex rules, but for the purposes of this article, we keep it simple.
We will create a new test file named tests/Unit/Rules
in the PalindromeTest.php
directory.
Let's take a look at the test file, and then we'll discuss what's being performed:
<input type="number" min="1" max="10" required>
In the above test file, we define two tests: rule_passes_with_a_valid_value
and rule_fails_with_an_invalid_value
.
As the test name implies, the first test ensures that the rule passes when the value is palindrome, and the second test ensures that the rule fails when the value is not palindrome.
We use the PHPUnitFrameworkAttributesDataProvider
attribute to provide a list of valid and invalid values for the test for testing. This is a great way to keep your tests neat and be able to check multiple values with the same test. For example, if someone adds a new valid value to the validValues
method, the test will automatically run against that value.
In the rule_passes_with_a_valid_value
test, we use valid values to call the validate
method on the rule. We pass the closure to the fail
parameter (this parameter is called if the internal validation of the rule fails). We have specified that if the closure is executed (i.e., the verification failed), the test should fail. If we reach the end of the test without executing the closure, then we know that the rule has passed and we can add a simple assertion assertTrue(true)
to pass the test.
In the rule_fails_with_an_invalid_value
test, we are the same as the first one, but this time we pass the invalid value to the rule. We have specified that if the closure is executed (i.e., the verification failed), the test should pass because we expect the closure to be called. If we reach the end of the test without executing the closure, no assertions are executed and PHPUnit should trigger a warning for us. However, if you prefer to make sure that the test fails more explicitly than just giving errors, you may need to take a slightly different approach to writing the test.
In this article, we examine what verification is and why it is important. We compared client-side verification with server-side verification and explored why client-side verification should not be used as the only form of verification in the application.
We also covered some convenient verification rules that I like to use in my Laravel app. Finally, we explore how to create your own validation rules and test them to make sure they work as expected.
Hope you should now be confident enough to start using more verification to improve the security and reliability of your application.
The above is the detailed content of The ultimate guide to Laravel Validation. For more information, please follow other related articles on the PHP Chinese website!