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

Phonenumber Laravel Package

brick/phonenumber

Brick\PhoneNumber is a PHP library for parsing, formatting, and validating international phone numbers. A port of Google's libphonenumber, it offers robust utilities for formatting, validation, and type detection in PHP applications.

Deep Wiki
Context7

Getting Started

brick/phonenumber is a pure-PHP port of Google's libphonenumber. It does one job well: parse, validate, format, and classify phone numbers. It ships no country metadata, so you almost always install the companion data package.

composer require brick/phonenumber brick/phonenumber-metadata

First use case: accept messy user input, validate it, and store it as E.164.

use Brick\PhoneNumber\PhoneNumberFormat;
use Brick\PhoneNumber\PhoneNumberParseException;
use Brick\PhoneNumber\PhoneNumberUtil;

$util = PhoneNumberUtil::getInstance();

try {
    $number = $util->parse('+1 (415) 555-2671', 'US');
} catch (PhoneNumberParseException $e) {
    // unparseable input (letters, too short, nonsense)
}

$number->isValidNumber();                            // true
(string) $number;                                   // "+14155552671" (E.164)
$number->format(PhoneNumberFormat::NATIONAL);       // "(415) 555-2671"
$number->format(PhoneNumberFormat::INTERNATIONAL);  // "+1 415-555-2671"

Where to look first:

  • PhoneNumberUtil — parsing, region lookups, example numbers (getExampleNumber('US')), country-code helpers.
  • PhoneNumber — immutable value object; every per-number operation lives here (isValidNumber, getNumberType, format, getRegionCode, getExtension).
  • PhoneNumberFormat — the four output constants: E164, NATIONAL, INTERNATIONAL, RFC3966.
  • PhoneNumberType — constants used by getNumberType() (MOBILE, FIXED_LINE_OR_MOBILE, TOLL_FREE, VOIP, …).

The core mental model: parse() returns a PhoneNumber or throws; isValidNumber() is a separate check on the parsed object.

Implementation Patterns

Store E.164, format on read

The lowest-friction convention: persist (string) $number (E.164, e.g. +14155552671), and keep the user's region in a separate column if you need to render a national format later.

public function setPhoneAttribute(?string $value): void
{
    if ($value === null || $value === '') {
        $this->attributes['phone'] = null;
        return;
    }

    $number = PhoneNumberUtil::getInstance()->parse($value, $this->phone_region ?? 'US');

    if (! $number->isValidNumber()) {
        throw ValidationException::withMessages(['phone' => 'Invalid phone number.']);
    }

    $this->attributes['phone'] = (string) $number; // E.164
}

Form request / validation rule

Use a ValidationRule rather than a closure so the region is explicit and the rule is reusable:

final class PhoneNumberRule implements ValidationRule
{
    public function __construct(private string $region = 'US') {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        try {
            $number = PhoneNumberUtil::getInstance()->parse((string) $value, $this->region);
        } catch (PhoneNumberParseException) {
            $fail('The :attribute is not a phone number.');

            return;
        }

        if (! $number->isValidNumber()) {
            $fail('The :attribute is not a valid number for the '.$this->region.' region.');
        }
    }
}

Then in a form request: 'phone' => ['required', new PhoneNumberRule($request->user()->country)].

Eloquent cast for reads

Pair the E.164 write with a read-side cast so controllers deal in PhoneNumber objects, not strings:

final class PhoneNumberCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): ?PhoneNumber
    {
        if ($value === null) {
            return null;
        }

        try {
            return PhoneNumberUtil::getInstance()->parse($value, 'ZZ');
        } catch (PhoneNumberParseException) {
            return null;
        }
    }

    public function set($model, string $key, $value, array $attributes): array
    {
        if ($value === null) {
            return [$key => null];
        }

        $number = $value instanceof PhoneNumber
            ? $value
            : PhoneNumberUtil::getInstance()->parse((string) $value, 'US');

        return [$key => (string) $number];
    }
}

Route by number type

getNumberType() answers "is this a mobile?" which drives SMS vs voice decisions:

use Brick\PhoneNumber\PhoneNumberType;

$smsCapable = in_array($number->getNumberType(), [
    PhoneNumberType::MOBILE,
    PhoneNumberType::FIXED_LINE_OR_MOBILE,
], true);

Treat FIXED_LINE_OR_MOBILE as "mobile-capable" in markets where portability blurs the line (US, UK, etc.).

Display to an international audience

Use formatOutOfCountryCallingNumber($viewerRegion) when showing a number to someone who has to dial it from another country — it inserts the correct IDD prefix (e.g. 00 1 415… for a UK caller) rather than a hardcoded +.

$number->formatOutOfCountryCallingNumber($request->user()->country ?? 'US');

Keep metadata out of the hot path

PhoneNumberUtil::getInstance() is a singleton and caches its metadata source, so the load cost is paid once per request/process. For containers where the full metadata package is too heavy, generate a trimmed metadata source containing only your supported regions and inject it:

$util = new PhoneNumberUtil($customMetadataSource);

Bind that instance as a singleton in a service provider and inject PhoneNumberUtil rather than calling the static everywhere — it makes test substitution possible and keeps the dependency visible.

Gotchas and Tips

  • parse() throwing is not the same as invalid. A number can parse successfully and still be a fake/invalid number. Always follow parse() with isValidNumber(). isPossibleNumber() is only a cheap length check — useful for progressive UI validation, useless as a final gate.
  • The default region is load-bearing. Parsing "020 7946 0958" without GB will succeed against the wrong region or fail. Always pass the user's country; never default to US for a globally-served app.
  • Catch PhoneNumberParseException everywhere. It's an exception, not a boolean return, and it silently bubbles past validation-only code paths. A missing try/catch in a model mutator will 500 the request.
  • E.164 does not include the extension. (string) $number drops it. If extensions matter, store $number->getExtension() alongside, or use RFC3966 formatting (tel:+1-415-555-2671;ext=123).
  • Extensions parse but don't always validate. parse('+14155552671 ext. 123', 'US') succeeds; validate the base number and treat the extension as opaque data.
  • Compare numbers after normalizing. +1 415 555 2671 and (415) 555-2671 are the same number; raw string comparison is wrong. Compare E.164 strings, or use isEqualTo().
  • Metadata is the biggest dependency. brick/phonenumber-metadata is thousands of generated PHP files; it dominates vendor size and can show up in cold-start profiling. Trim it in production or at least verify your deploy strategy handles it.
  • Metadata drifts. Numbering plans change and the library's validation is only as current as the last metadata release. Pin and update brick/phonenumber-metadata on a schedule; stale metadata produces false negatives on new ranges.
  • Short codes and emergency numbers will never validate. isValidNumber() is false for 911, 112, shortcodes, and internal extensions. If your app accepts those, don't route them through the same rule.
  • getInstance() is a static singleton — mildly hostile to tests. Wrap it. Tests that need deterministic parsing should construct new PhoneNumberUtil($source) with a stub MetadataSource rather than mutating global state.
  • getRegionCode() can return null. Numbers on international/toll-free ranges don't always map to a single region; don't assume a non-null region for display logic.
  • 'ZZ' is your friend for E.164 reads. When reparsing a stored value that already begins with +, the default region is ignored, so pass 'ZZ' to make that explicit.
  • getExampleNumber('US') is excellent for factories and seeders. It returns a structurally valid number per region — far better than hardcoding +15551234567 in test data.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console