Laravel validation and custom rules in Inspector

Valerio Barbera
Graphic with Valerio’s avatar a heading reading Laravel validation and custom rules in Inspector with red abstract art on the right

Hi, I’m Valerio Barbera, software engineer, founder and CTO at Inspector.

Data validation is one of the fundamental features in any application. And it is something developers manipulate almost every day. The value a software provides to users is often a function of the quality of data it collects and provides.

Laravel ships with many predefined validation rules you can immediately use in your controllers. But working on the Inspector backend we have identified some aspects of validating incoming data that impact the security and reliability of our application.

Also, thanks to custom rules, I’ll show you how you can easily extend the validation layer of your app with new functionalities provided by external services.

I’ll start with some context to clarify the validation layer’s role in a back-end service. Then I’ll show you our implementations.

If you have recently started using Laravel you can read a quick overview in this previos article: What is Laravel framework, and why is it so popular?

Laravel Validation layer

Data integrity and validation are essential aspects of web development because they define the app’s state. If the data is wrong, the application will not behave correctly.

It’s always important to validate data before storing them in the database and before doing anything else.

In the Laravel request lifecycle, an HTTP request sent by a client goes through middleware first. Middleware deals with a mix of things between authentication and security.

Before the request enters the application, its data must be validated.

Illustration of Laravel’s validation layer with icons left to right of client computer, Lavarvel middleware, data validation, and the controller.

There are two ways to do data validation in Laravel: Inside the controllers or using Form requests.

Data Validation in controller

The easiest way of validation is performing it in the controller. At the start of each controller method, you can first validate data:

namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
 
class UserController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|min:3',
            'email' => 'required|email|min:6',
        ]);
 
        // here we know data are valid so we can pass them to database or other services
    }
}

Laravel will take care to return a 422 response code to the client if the data is not valid.

Data validation using Form requests

If your validation rules are too complex, you may want to encapsulate them in reusable classes. This approach will avoid messing up the controller.

Laravel provides the ability to wrap validation in a dedicated component called FormRequest.

First, create a form request:

php artisan make:request StoreUserRequest

Than move your validation logic inside the rules method of the request class:

<?php
 
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
 
class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|string|min:3',
            'email' => 'required|email|min:6',
        ];
    }
}

You can type-hint this new request class in the controller method instead of the original Illuminate\Http\Request class, and remove the validation statement from the controller:

namespace App\Http\Controllers;
 
use App\Http\Requests\StoreUserRequest;
 
class UserController extends Controller
{
    public function store(StoreUserRequest $request)
    {
        // here we know data are valid so we can pass them to database or other services
    }
}

Custom validation rules

Laravel provided a well-developed validation layer. You can easily extend it by implementing custom rules to reuse in your code. Or you can increase the capability of your validation using external services.

Let me show you an example of one of the custom rules we implemented in Inspector.

First, create the class that represents a validation rule in Laravel:

php artisan make:rule SecurePassword

The idea is to verify if a password is in the list of well known insecure passwords. It will not pass the validation if it is, forcing the user to use a more secure string.

namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class SecurePassword implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return !in_array($value, [
            'picture1',
            'password',
            'password1',
            '12345678',
            '111111',
            ...
        ]);
    }
    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The chosen password is unsecure. Try again with a less common string.';
    }
}

To use it in your controller you have to create an instance as an item of validation rules applied to the field:

namespace App\Http\Controllers;
 
use App\Rules\SecurePassword;
use Illuminate\Http\Request;
 
class UserController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|min:3',
            'email' => 'required|email|min:6',
            'password' => ['required', new SecurePassword()],
        ]);
 
        // here we know data are valid so we can pass them to database or other services
    }
}

Integrate with external services

When exploring data validation, many SaaS services can bring new capabilities to your validation layer regarding the security and reliability of the data collected.

I recommend looking at apilayer.com, which provides excellent REST services to deal with data.

In Inspector, we use the mailboxlayer.com API to validate emails. The service can also detect fake email addresses, temporary addresses, and the actual existence of an email address using MX-Records and SMTP.

Add two configuration property to store the API key of the new service in the config/service.php file:

return [
    ...,
	
    'mailboxlayer' => [
        'key' => env('MAILBOXLAYER_KEY'),
    ],
	
];

Create the custom rule:

php artisan make:rule EmailSpam

Here is the complete code of the rule:

namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class EmailSpam implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param string $attribute
     * @param mixed $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        if (app()->environment('local')) {
            return true;
        }
        return !config('services.mailboxlayer.key') || $this->check($value);
    }
    /**
     * Perform email check.
     *
     * @param string $email
     * @return bool
     */
    protected function check(string $email): bool
    {
        try{
            $response = file_get_contents('https://apilayer.net/api/check?'.http_build_query([
                'access_key' => config('services.mailboxlayer.key'),
                'email' => '[mailbox-layer-account-email]',
                'smtp' => 1,
            ]));
            $response = json_decode($response, true);
            return $response['format_valid'] && !$response['disposable'];
        } catch (\Exception $exception) {
            report($exception);
            if (app()->environment('local')) {
                return false;
            }
            // Don't block production environment in case of apilayer error
            return true;
        }
    }
    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'Invalid email address.';
    }
}

Validation rule with external parameters

If you need to pass external parameters to your rule class you can pass them in the constructor.

Define the rule constructor with parameters you need:

namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class EmailSpam implements Rule
{
    /**
     * EmailSpam rule constructor.
     */
    public function __constructor(
        protected string $key,
        protected string $url
    ) {}
    /**
     * Determine if the validation rule passes.
     *
     * @param string $attribute
     * @param mixed $value
     * @return bool
     */
    public function passes($attribute, $value){...}
    /**
     * Perform email check.
     *
     * @param string $email
     * @return bool
     */
    protected function check(string $email): bool
    {
        // Use the paramters received in the constructor.
        $response = file_get_contents($this->url . '?' . http_build_query([
            'access_key' => $this->key,
            'email' => '[mailbox-layer-account-email]',
            'smtp' => 1,
        ]));
        ...
    }
    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'Invalid email address.';
    }
}

Now you can pass the required information during the creation of the instance in the constructor:

namespace App\Http\Controllers;
 
use App\Rules\EmailSpam;
use App\Rules\SecurePassword;
use Illuminate\Http\Request;
 
class UserController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|min:3',
            'email' => [
                'required', 
                'email', 
                'min:6', 
                new EmailSpam(
                    config('services.mailboxlayer.key'),
                    config('services.mailboxlayer.url')
                )
            ],
            'password' => ['required', new SecurePassword()],
        ]);
 
        // here we know data are valid so we can pass them to database or other services
    }
}

Laravel validation Tips & Tricks

Validate borders

Based on my experience, I can suggest you always validate the minimum and the maximum sizes of the incoming fields .

Don’t wait for database errors that truncate too long strings. Instead, help your users understand the limits of each field by the error messages returned during data validation.

Ask for the current password

Every critical action should require password confirmation. For example, you should always prompt the user to type the current password to authorize actions that can compromise the account accessibility.

Examples include changing email and changing password.

This feature will improve security because even having physical access to the computer with the Inspector dashboard opened on the screen; a malicious user can’t change access credentials without knowing the current password. So, he can’t shut you out.

Here is our implementation of the current password verification:

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class CurrentPassword implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return Hash::check($value, Auth::user()->password);
    }
    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'Your current password is incorrect.';
    }
}

Laravel application monitoring

If you found this post interesting and want to drastically change your developers’ life for the better, you can give Inspector a try.

Inspector is an easy to use Code Execution Monitoring tool that helps developers to identify bugs and bottlenecks in their application automatically. Before customers do.

screenshot inspector code monitoring timeline

It is completely code-driven. You won’t have to install anything at the server level or make complex configurations in your cloud infrastructure.

It works with a lightweight software library that you can install in your application like any other dependency. You can try the Laravel package, it’s free.

Create an account, or visit our website for more information: https://inspector.dev/laravel

Related Posts

php-iterators-inspector

PHP Iterators for walking through data structures – FastTips

PHP Iterators are essential tools for efficiently traversing and manipulating data structures like arrays, objects, and more. They provide a clean and memory-efficient way to work with large datasets without loading the entire dataset into memory at once. In this tutorial, we will explore PHP iterators and how to use them for walking through various

Adoption of AWS Graviton ARM instances (and what results we’ve seen)

Working in software and cloud services you’ve probably already heard about the launch of new the Graviton machines based on custom ARM CPUs from AWS (Amazon Web Services).  In this article you can learn the fundamental differences between ARM and x86 architecture and the results we’ve achieved after the adoption of Graviton ARM machines in

Announcing increased data retention for monitoring data

Long story short: In the last 2 months of work we’ve achieved great results in cost optimization by refactoring both our infrastructure and code architecture, and we want to pass this value to you in the form of a longer data retention for your monitoring data. Thanks to these changes we are increasing our computational