a black and white photo of a njoguamos in a suit and with a serious look on his face

Create and Test a Custom Laravel Validation Rule

Created 20 Aug 2023 | Updated 25 Sep 2023

LaravelLaravel HerdPestPHP

The validation rules provided by Laravel may be sufficient to cover most of the use cases. Laravel also makes it possible to extend the validation rules with custom rules. There are two ways to create custom validation rules: rule objects or closures. I prefer the rule object because it can easily be tested in isolation.

What we're going to test

We will create a custom password validation rule that uses zxcvbn, a realistic password strength estimation developed by Dropbox. The strength of passwords calculated by zxcvbn ranges from 0 to 4 i.e.

  • 0 - too guessable and therefore risky password
  • 1 - very guessable password
  • 2 - somewhat guessable password
  • 3 - safely unguessable password
  • 4 - very unguessable password

To learn more about zxcvbn, see the links at the end of this article.

We are going to create a custom password rule and use Pest PHP to test it.

Prerequisites

Before we proceed, ensure that:

Install zxcvbn

Start by installing a zxcvbn wrapper for PHP.

composer require bjeavons/zxcvbn-php

Creating Custom Password Validation Rule

Next create a custom validation rule.

php artisan make:rule StrongPasswordRule

The command will create a new file at app/Rules/StrongPasswordRule.php. We are now ready to define it behaviour as follows:

<?php
 
namespace App\Rules;
 
use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Arr;
use ZxcvbnPhp\Zxcvbn;
 
class StrongPasswordRule implements ValidationRule, DataAwareRule
{
    public Zxcvbn $zxcvbn;
 
    protected array $data = [];
 
    const MIN_PASSWORD_SCORE = 3;
 
    public function __construct()
    {
        $this->zxcvbn = new Zxcvbn();
    }
 
    public function setData($data): self|static
    {
        $this->data = $data;
 
        return $this;
    }
 
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if ($this->isWeakPassword($value)) {
            $fail(trans(key: trans(key: 'validation.custom.password.strong')));
        }
    }
 
    public function isWeakPassword(string $value): bool
    {
        return $this->zxcvbn->passwordStrength(
            password: $value,
            userInputs: $this->getUserInputs()
            )['score'] < self::MIN_PASSWORD_SCORE;
    }
 
    protected function getUserInputs(): array
    {
        return array_values(
            array: Arr::except(
                array: $this->data,
                keys: ['password','password_confirmation']
            )
        );
    }
}

We also create a custom validation message translation `lang/en/validation.php.

<?php
 
return [
    'custom' => [
        'password' => [
            // OTHER RULES
            'strong'   => 'Password too simple. Try something more complex. Uncommon words are better.',
        ],
    ],
];

Here are a couple of things going on:

  • When the StrongPasswordRule class is called, we create a new instance of Zxcvbn. Of course, we could register a binding in-service provider but let us keep things simple.
  • We implement the DataAwareRule interface. We use the setData to access other data undergoing validation data. We then pass this data to zxcvbn by calling getUserInputs. Accessing additional data prevents the user from creating a password containing the user's email, name, phone, etc.
  • We then get the user password value and pass it to the zxcvbn passwordStrength method, which calculates password strength via non-overlapping minimum entropy patterns. You can set the minimum password strength score of your choice. I prefer at least three, but you can also put 4, the maximum.
  • When validation fails, we call a custom translation message located at lang/en/validation.php.

Implementing a Custom Validation Rule

Once you have defined the custom password rule, you may attach it to a validator by passing an instance of the rule object with your other validation rules.

<?php
 
namespace App\Http\Requests;
 
use App\Rules\StrongPasswordRule;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
 
class RegisterRequest extends FormRequest
{
    /** @return array<string, Rule|array|string> */
    public function rules(): array
    {
        return [
            'name'      => ['required', 'string', 'min:5', 'max:128'],
            'email'     => [ 'required', 'email', 'unique:users', 'max:128'],
            'password'  => ['required', Password::min(size: 8)->uncompromised(),
                new StrongPasswordRule(), 'max:50'
            ],
        ];
    }
}

Coupled with min and uncompromised rules, you can allow your users to create strong and secure passwords.

Testing a Custom Validation Rule

We create a custom expectation to test the invokable rule in Laravel, as Freek Van der Herten described in his article - How to test Laravel invokable rules. Modify your Pest.php file.

<?php
 
use Illuminate\Contracts\Validation\ValidationRule;
 
// Other code
 
expect()
    ->extend(name: 'toPassWith', extend: function (mixed $value) {
        $rule = $this->value;
 
        if (!$rule instanceof ValidationRule) {
            throw new Exception(message: 'Value is not an invokable rule');
        }
 
        $passed = true;
 
        $fail = function () use (&$passed) {
            $passed = false;
        };
 
        $rule->validate(attribute: 'attribute', value: $value, fail: $fail);
 
        expect(value: $passed)->toBeTrue();
    });

Next, we create a test file, Feature/Rules/StrongPasswordRuleTest.php, and add the tests. You can add tests to the scope you are comfortable with.

<?php
 
use App\Rules\StrongPasswordRule;
 
beforeEach(function () {
    $this->rule = new StrongPasswordRule();
});
 
it(description: 'fails for password with score less than 3 `zxcvbn score`', closure: function (string $password) {
    expect(value: $this->rule)->not()->toPassWith($password);
})->with([
    '0 score' => ['p@$$word'],
    '1 score' => ['qwER43@!'],
    '2 score' => ['njogu'],
]);
 
it(description: 'passes for password with a `zxcvbn score` of 3 or 4', closure: function () {
    expect(value: $this->rule)->toPassWith('correct horse battery staple');
});
 
// Using examples.
it(description: 'fails when password includes user data', closure: function (string $password, array $data) {
    $this->rule->setData($data);
 
    expect(value: $this->rule)->not()->toPassWith($password);
})->with([
    'user name' => [ 'Amos Njogu 12', ['name' => 'Amos Njogu'] ],
    'user email' => ['njoguamos', ['email' => '[email protected]'] ],
    'company names' => ['acmebrick', ['name' => 'Acme Brick Co'] ]
]);
 
// Using example passwords. The list is exhaustive
it(description: 'fails for most common passwords', closure: function (string $password) {
    expect(value: $this->rule)->not()->toPassWith($password);
})->with([
    '!@#$%^&*', '111111', '123123', '12345', '123456', '1234567', '12345678', '12345678',
    '123456789', '1234567890', '1q2w3e', '654321', '666666', 'aa123456', 'abc123',
    'admin', 'charlie', 'donald', 'football', 'iloveyou', 'monkey', 'password'
]);
 
// Using example passwords. The list is exhaustive
it(description: 'passes for strong password', closure: function (string $password) {
    expect(value: $this->rule)->toPassWith($password);
})->with([
    'gruel-sepal-capsize-cytolog','Took-gasp-halibut-property',
    'Liverish prelate executor outs', 'hurtle_impious_atypical_mediate'
]);

Conclusion

I hope you now have an idea of how you can test a custom Laravel validation rule. You can take a step further and extract the validation rule into a composer package for others. In addition, the article demonstrates techniques such as using data sets, the beforeAll method, and adding a custom expectation.

Reach out

Let’s talk about working together.

Available for new opportunities