Working on a project involving dealerships and cars, I needed a way to validate car VIN (Vehicle Identification Number) so I came up with a custom Laravel validation rule.

Because the app deals mostly with cars manufactured in Europe, validation using the check digit method won’t work. So I settled with the following, more lenient, rules:

  • 17 chars maximum
  • only accept letters and numbers
  • O, I, Q are forbidden because they can be confused with 0 and 1

Here’s the resulting code:


namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class VIN implements Rule
{
    /** @var string */
    private $message = 'cars.validation.invalidVIN';

    /** @var int */
    private $vinLength = 17;

    /**
     * Determine if the validation rule passes.
     *
     * @param  string $attribute
     * @param  mixed $vin
     * @return bool
     */
    public function passes($attribute, $vin)
    {
        if ($this->invalidLength($vin)) {
            $this->message = 'cars.validation.invalidVINLength';
            return false;
        }

        if ($this->containsForbiddenCharacters($vin)) {
            $this->message = 'cars.validation.invalidVINForbiddenChars';
            return false;
        }

        return true;
    }

    /**
     * @param $vin
     * @return bool
     */
    private function invalidLength($vin): bool
    {
        return strlen($vin) !== $this->vinLength;
    }

    /**
     * @param string $vin
     * @return bool
     */
    private function containsForbiddenCharacters(string $vin): bool
    {
        preg_match('/([OIQ])/', $vin, $invalidLetters);
        preg_match('/([^A-Za-z0-9])/', $vin, $invalidChars);

        return (count($invalidLetters) > 0) || count($invalidChars) > 0;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message(): string
    {
        return trans($this->message);
    }
}