Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phone Laravel Package

laravel-validation-rules/phone

Add reliable phone number validation to your Laravel apps with dedicated rules that verify international formats and reject invalid numbers. Simple, expressive syntax for form requests and validators keeps user data clean.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the rule via Composer (it pulls in giggsey/libphonenumber-for-php transitively, so no extra setup is needed):

composer require laravel-validation-rules/phone

If the package ships a config file, publish it once so region defaults are version-controlled:

php artisan vendor:publish --tag=phone-config

First use case — a signup form. Drop the rule into any validator, FormRequest, or $request->validate() call. No string alias required:

use LaravelValidationRules\Phone\Phone;

$request->validate([
    'phone' => ['required', 'string', new Phone()],
]);

That single line will accept numbers in E.164 (+14155552671), national ((415) 555-2671), and loose formats, while rejecting anything libphonenumber doesn't recognise as a real, assigned number.

Where to look first:

  • config/phone.php — global default country/region and any allow-list of countries. If your app is single-region, pin it here so you don't repeat the country on every rule.
  • The Phone rule class itself — it's small and just wraps PhoneNumberUtil::isValidNumber(), so reading it once tells you exactly what passes.
  • The vendor copy of libphonenumber-for-php — authoritative source of truth if you're debugging why a specific number is rejected.

Sanity-check before writing UI: open php artisan tinker and run app(Phone::class) or Validator::make(['p' => '+441632960961'], ['p' => new Phone('GB')])->passes() against a handful of expected numbers. It's the fastest way to confirm your default region is set correctly before wiring up forms.

Implementation Patterns

FormRequest — the standard home for the rule. Keep the rule on the request, not scattered in controllers, and pair it with format-only rules (string, max) so error messages stay readable:

public function rules(): array
{
    return [
        'phone' => ['required', 'string', 'max:20', new Phone($this->input('country'))],
    ];
}

Passing $this->input('country') lets the user tell you which region to interpret a national-format number in — critical for forms with a country dropdown.

Multi-region / international apps. Pass an array of ISO-3166 alpha-2 codes when you want to accept numbers from a specific set of countries (e.g. EU-only signups):

new Phone(['DE', 'FR', 'IT', 'ES', 'NL'])

Omit the argument (or pass null) to accept any valid number globally — appropriate for a "contact us" form where the country isn't collected.

Normalise before persisting. Validation only guarantees the number is valid, not that it's stored consistently. Convert to E.164 before saving so all downstream consumers (SMS, Twilio, CRM sync) get the same shape:

$phone = phone($request->input('phone'), $request->input('country'))->formatE164();
// or via libphonenumber directly
$e164 = PhoneNumberUtil::getInstance()
    ->parse($request->input('phone'), $request->input('country'))
    ->format(PhoneNumberFormat::E164);

Store a raw phone column and the E.164 version if you ever need to display numbers back in the user's format; otherwise just store E.164.

API + web parity. Wrap validation in a small DTO or attribute cast so the same rule and the same E.164 normalisation apply to JSON and HTML requests. Laravel's Rule::when() composes nicely here — require phone for phone-based login, but make it optional on profile-completion forms.

Livewire / Inertia. The rule runs server-side only; on the client, set type="tel" inputmode="tel" and optionally wire in intl-tel-input for formatting. Do not attempt to mirror the validation client-side — libphonenumber's data set is far too large to ship, and divergences between client and server cause the worst kind of support tickets.

Testing. Prefer Laravel's assertValid / assertInvalid in feature tests, and use the fixture set from libphonenumber (libphonenumber/test/metadata) rather than rolling your own numbers:

$this->assertInvalid(['phone' => '555-555-5555'], ['phone' => new Phone('US')]); // 555-555-5555 is not a real US number
$this->assertValid(['phone' => '+14155552671'], ['phone' => new Phone()]);

For factories, use fake()->e164PhoneNumber() (via fakerphp/faker) — but be aware it only generates plausible numbers, not necessarily valid ones. Validate once at seed time if production data must pass the same rule.

Custom messages. Override per-attribute for clearer feedback:

public function messages(): array
{
    return ['phone.phone' => 'Please enter a valid phone number for the selected country.'];
}

Gotchas and Tips

Empty strings still pass unless you add required or nullable. new Phone() on its own is a format check; combine it with required for mandatory fields, and be explicit about nullable on optional ones — otherwise '' slips through and hits your DB.

possible vs valid is the #1 confusion. libphonenumber has two checks: isPossibleNumber (correct length/prefix) and isValidNumber (actually assigned in the numbering plan). This rule uses the strict one. That means +1 555 555 5555 — the number every test fixture in the world uses — will be rejected. Use +1 415 555 2671 or a reserved-for-fiction range (+1 202 555 0100) instead.

Fake/test numbers are the biggest source of "why is this failing?" support tickets. Before blaming the rule, paste the number into https://libphonenumber.appspot.com and confirm it's an allocated range. Update the dependency (composer update giggsey/libphonenumber-for-php) if you need recently allocated ranges — the bundled metadata ships with the library, not with this rule, and drifts over time.

National format needs a region — always. 020 7946 0018 is only valid with 'GB' supplied. Without a region, libphonenumber falls back to the package's configured default (or may guess from the request IP in some configurations). For deterministic behaviour in tests, pass an explicit country and set a default in config/phone.php.

Region codes are ISO 3166-1 alpha-2, uppercase. 'us', 'USA', and 'United States' will all fail. If you're feeding values from a country dropdown or a countries table, cast to uppercase and truncate to two characters at the boundary.

The + prefix is load-bearing. +442071838750 parses cleanly with no region; 442071838750 (no plus) may be interpreted as a European national number. Strip everything except + and digits before validation, or document clearly whether your UI collects the +.

default_country in config is not a security boundary. It just helps ambiguous national numbers parse. Setting it to your main market means a user in another country entering a national-format number will be misinterpreted — which is why collecting a country dropdown alongside the phone field is worth the extra UI for international apps.

Performance in loops. libphonenumber is heavier than a regex. If you're bulk-importing thousands of rows, validate once up front and cache the parsed PhoneNumber object; don't instantiate PhoneNumberUtil per row if you can help it. The rule itself is fine for per-request use.

Don't max:16 your way out of a bug. E.164 can be 16 characters (+ plus 15 digits), but formatted input can be longer. Use max:20 or max:255 on the input column and normalise to E.164 before persisting — don't reject legitimate numbers because the user typed spaces.

Extension points. The rule is deliberately small — if you need extras (e.g. reject premium-rate ranges, accept short codes, enforce a specific PhoneNumberType::MOBILE), subclass it or wrap it in a custom rule that calls PhoneNumberUtil first and then delegates. This keeps your custom logic out of vendor code and testable in isolation.

Debugging: bypass the validator when confused. When a number unexpectedly fails, run this in tinker:

PhoneNumberUtil::getInstance()->parse('+1 415 555 2671', 'US');

If parsing throws NumberParseException, the problem is the input or region. If parse succeeds but isValidNumber() is false, the number isn't allocated. That two-step check saves hours.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route