Skip to content

Validate phone numbers in Laravel

Phone number validation in Laravel that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Laravel's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the PHP guide; the rest is Laravel's own validation hook, a few lines long.

1. The file

Take Phone.php from the PHP guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs PHP 8.1 or later; no dependency. Its parse reads a number as typed and returns the number with its E.164 form, region and line type, or null when no plan accepts it.

composer require laravel/framework   # an existing Laravel app already has it

2. The Laravel side

app/Rules/PhoneNumber.php

The rule, as Laravel generates them (php artisan make:rule PhoneNumber).

<?php

declare(strict_types=1);

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

/** A phone number readable in the plans of Phone.php; national numbers are read in $defaultRegion. */
final class PhoneNumber implements ValidationRule
{
    public function __construct(private readonly ?string $defaultRegion = null)
    {
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!is_string($value) || \Phone::parse($value, $this->defaultRegion) === null) {
            $fail('The :attribute field must be a valid phone number.');
        }
    }
}

In a controller

Validate, then store the E.164 form.

$data = $request->validate([
    'name' => ['required', 'string'],
    'phone' => ['required', 'string', 'max:40', new PhoneNumber('GB')],
]);

$e164 = \Phone::parse($data['phone'], 'GB')['e164'];

3. The tests

tests/PhoneNumberTest.php: The tests, with PHPUnit and Laravel's validator.

<?php

declare(strict_types=1);

use App\Rules\PhoneNumber;
use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;
use Illuminate\Validation\Factory;
use PHPUnit\Framework\TestCase;

final class PhoneNumberTest extends TestCase
{
    private function validate(string $phone): Illuminate\Validation\Validator
    {
        $factory = new Factory(new Translator(new ArrayLoader(), 'en'));
        return $factory->make(['phone' => $phone], ['phone' => ['required', 'string', 'max:40', new PhoneNumber('GB')]]);
    }

    public function testNationalNumberInTheDefaultRegion(): void
    {
        $this->assertTrue($this->validate('07400 123456')->passes());
    }

    public function testInternationalNumber(): void
    {
        $this->assertTrue($this->validate('+1 201-555-0123')->passes());
    }

    public function testFailsWithTheMessage(): void
    {
        $validator = $this->validate('+44 7400 12345');
        $this->assertTrue($validator->fails());
        $this->assertSame(['The phone field must be a valid phone number.'], $validator->errors()->get('phone'));
    }
}
php artisan test   # or vendor/bin/phpunit

Run before publishing with illuminate/validation 13.39 and the file generated for GB, US, FR, as 2026-10-25.

What is specific to Laravel

  • ValidationRule (Laravel 10 and later) is what make:rule writes: call $fail with a message and Laravel puts it on the field, :attribute replaced by the field's name or its translation.
  • Put Phone.php where Composer loads it: a classmap or files entry in composer.json, or in the App\Support namespace with a namespace line added at its top.
  • Validation answers yes or no; store Phone::parse(...)["e164"], not the input. A form request can do both in prepareForValidation and passedValidation.