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 it2. 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/phpunitRun 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 whatmake:rulewrites: call$failwith a message and Laravel puts it on the field,:attributereplaced by the field's name or its translation.- Put Phone.php where Composer loads it: a
classmaporfilesentry in composer.json, or in theApp\Supportnamespace with anamespaceline added at its top. - Validation answers yes or no; store
Phone::parse(...)["e164"], not the input. A form request can do both inprepareForValidationandpassedValidation.