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:
- Your at least know how to install Laravel or have one application already.
- You have Pest PHP installed in your Laravel application. Refer to Pest PHP installation instructions.
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 ofZxcvbn.
Of course, we could register a binding in-service provider but let us keep things simple. - We implement the
DataAwareRule
interface. We use thesetData
to access other data undergoing validation data. We then pass this data to zxcvbn by callinggetUserInputs.
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.