DEV Community

Cover image for 3 custom Laravel validation rules to reuse and make your code cleaner
lukapg
lukapg

Posted on

3 custom Laravel validation rules to reuse and make your code cleaner

When working with forms and user input in general, validating common fields in various projects, we can extract that field validation logic into a reusable custom validation rule. Using validation rules makes our code easier to read and maintain. We can make them even more powerful by using parameters inside our custom validation rules, allowing us to create pretty complex validation logic where needed.

Consider the scenario in which, as often is the case, we need to validate the user input of a registration form which contains the name, email, password, age and ZIP code fields. In this post, I'll give 3 examples of custom validation rules we can use to validate the password, age and ZIP code fields.

Note: You can find these and more Laravel tips at laravelbit.

Rule #1: Legal age validation rule

Imagine we needed to make a validation rule which validates the legal age of our users, based on a date input. Since the legal age varies around the world, we can pass the legal age as a parameter to our custom rule. Our legal age custom validation rule would look something like this:

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Carbon\Carbon;

class LegalAgeRule implements Rule
{
    public $legalAge = 18;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct($age)
    {
        $this->legalAge = $age;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $formattedValue = new Carbon($value);
        $legalAge = Carbon::now()->subYears($this->legalAge);
        return $formattedValue < $legalAge;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'You must be at least ' . $this->legalAge . 'years old!';
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that we used a class variable $legalAge to store the parameter value passed to use when calling this rule. Also, we used Carbon to compare the date received from the request and the date which we calculated to be exactly 18 years ago. Now, imagine we needed to actually use this rule somewhere in a form request. That would look like this:

public function rules()
{
    return [
        'name' => 'required|string',
        'email' => 'required|email',
        'password' => 'required|min:8',
        'age' => ['required', new LegalAgeRule(18)],
        'zip' => 'required'
    ];
}
Enter fullscreen mode Exit fullscreen mode

You can see that we passed the age parameter of 18 to the LegalAgeRule custom validation rule when calling it.

Rule #2: Password strength validation rule

Often, we need to validate passwords in our applications. Instead of writing the same (or similar) logic every time, we can extract a custom password validation rule. In this example, we will use one parameter, the password length, in our custom rule. You can always expand this example to include more parameters so that you can always configure this rule when calling, without touching the rule code itself.

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Str;

class PasswordStrengthRule implements Rule
{

    public $length = 10;

    public $lengthCheck = false;

    public $uppercaseCheck = false;

    public $numericCheck = false;

    public $specialCharacterCheck = false;

    public function __construct($length)
    {
        $this->length = $length;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $this->lengthCheck = Str::length($value) >= $this->length;
        $this->uppercaseCheck = Str::lower($value) !== $value;
        $this->numericCheck = (bool) preg_match('/[0-9]/', $value);
        $this->specialCharacterCheck = (bool) preg_match('/[^A-Za-z0-9]/', $value);

        return ($this->lengthCheck && $this->uppercaseCheck && $this->numericCheck && $this->specialCharacterCheck);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The :attribute must be at least ' . $this->length . ' 10 characters and contain at least one uppercase character, one number and one special character.';
    }
}
Enter fullscreen mode Exit fullscreen mode

We use several class variables to store different password strength checks. The password length check is passed as a parameter when calling the rule, while other rules simply state that the password must contain at least one uppercase character, one number and one special characters.

Using this rule is quite similar to our last example:

public function rules()
{
    return [
        'name' => 'required|string',
        'email' => 'required|email',
        'password' => ['required', new PasswordStrengthRule(8)],
        'age' => ['required', new LegalAgeRule(18)],
        'zip' => 'required'
    ];
}
Enter fullscreen mode Exit fullscreen mode

Rule #3: ZIP code validation

The last example is also the simplest. Since ZIP codes are (mostly) 5 digits in length and can contain zeros at the start (unlike regular numeric, integer values), we simply combine these two conditions in a regular expression.

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class ZipCodeRule implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return preg_match('/\b\d{5}\b/', $value);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'ZIP code must contain 5 digits!';
    }
}
Enter fullscreen mode Exit fullscreen mode

It is also the only rule out of the 3 which does not use any parameters. Finally, we can add this rule to our example form request validation as well:

public function rules()
{
    return [
        'name' => 'required|string',
        'email' => 'required|email',
        'password' => ['required', new PasswordStrengthRule(8)],
        'age' => ['required', new LegalAgeRule(18)],
        'zip' => ['required', new ZipCodeRule()]
    ];
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
darkain profile image
Vincent Milum Jr

Just a small note: outside of the United States, postal codes follow different formats, and this is something that generally should be taken into consideration. Canada for example uses a mixed letter/number scheme that is 6 characters long. As soon as you think "oh, we'll only deal with USA addresses", that will be broken eventually.