gepur-it/phone-number-advanced
A Laravel package for advanced phone number handling, with parsing, formatting, validation, and country-aware utilities to keep numbers consistent across your app. Integrates with Eloquent models and Blade views for cleaner input and display.
1. Install and confirm discovery
composer require gepur-it/phone-number-advanced:^5.3
Use ^5.3 or later: v5.3.0 is the first release with PHP 8 support (release notes: upd php 8). If you are on PHP 8.0+, this is the minimum version to target. Before writing code, open composer.json and check for extra.laravel.providers / extra.laravel.aliases to confirm auto-discovery. If none is present, register the service provider and facade manually in config/app.php.
2. Publish and set the default region first
Phone parsing is region-dependent: "333 123 4567" is ambiguous until you tell the library it is Italian. Find the config file (php artisan vendor:publish --provider="GepurIt\PhoneNumber\..." — check the provider namespace in src/) and set the default region from an env var:
// config/phone-number.php
'default_region' => env('PHONE_DEFAULT_REGION', 'IT'),
This is the single most important setup step. Most "the package is broken" reports on libphonenumber wrappers trace back to a missing or wrong default region.
3. First use case: normalize before storage
The canonical day-one workflow is: accept a messy user string, parse it once, store E.164, render it formatted later.
use GepurIt\PhoneNumber\Facades\PhoneNumber; // verify facade name in src/
$phone = PhoneNumber::parse($request->input('phone'), $request->input('country', 'IT'));
$phone->getE164(); // +393331234567 -> store this
$phone->getInternational(); // +39 333 123 4567 -> show this
$phone->isValid(); // bool
The package is a thin Laravel wrapper over giggsey/libphonenumber-for-php, so the object semantics you get back follow libphonenumber's PhoneNumber/PhoneNumberUtil API. If the facade methods differ, src/ is ~a handful of files — read them end to end in ten minutes.
4. Where to look, in order
README.md — likely sparse; skim for the facade name and config key.src/ — the service provider tells you what is bound in the container; the facade tells you the surface area.config/*.php — default region, maybe metadata path.composer.json — confirm PHP 8 is now allowed (v5.3.0) and check whether Laravel 9/10/11 are within the constraint. Also pin down which libphonenumber-for-php version is pulled in; the PHP 8 bump does not by itself guarantee the metadata dependency was refreshed.Validate in a Form Request, store canonical form
Use the package for parsing/validation, but keep a single canonical representation in the database.
class StoreContactRequest extends FormRequest
{
public function rules(): array
{
return [
'phone' => ['required', 'string', 'max:32'],
'country' => ['required', 'string', 'size:2'],
];
}
}
// Controller
$phone = PhoneNumber::parse($request->phone, $request->country);
abort_unless($phone->isValid(), 422, 'Invalid phone number');
$contact->update(['phone_e164' => $phone->getE164()]);
If the package ships a validation rule (check src/Rules/), prefer it over hand-rolling; otherwise wrap the parse in a custom Rule.
class PhoneNumberRule implements ValidationRule
{
public function __construct(private ?string $region = null) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$parsed = PhoneNumber::parse((string) $value, $this->region ?? config('phone-number.default_region'));
} catch (\Throwable) {
$fail('The :attribute is not a valid phone number.');
return;
}
if (! $parsed->isValid()) {
$fail('The :attribute is not a valid phone number.');
}
}
}
Model accessors: E.164 in, display format out
Store E.164 (or E.164 + region), never store the formatted string as the source of truth.
public function setPhoneAttribute(string $value): void
{
$this->attributes['phone_e164'] = PhoneNumber::parse($value, $this->country)->getE164();
}
public function getPhoneDisplayAttribute(): string
{
return PhoneNumber::parse($this->phone_e164)->getNational();
}
This means a later switch to propaganistas/laravel-phone or raw libphonenumber only touches the mutator/accessor — not every view and controller.
Split country + national input in the UI
Best practice for international forms: a country <select> plus a plain national-format input. Pass the selected ISO code as the second arg to parse(). This avoids asking users to type + and avoids region ambiguity entirely.
Display formatting per context
getE164() or getInternational() (unambiguous).getNational().tel: links → always E.164.Keep one helper (PhonePresenter) so the format decision lives in one place rather than being re-litigated in every Blade file.
Bulk import / CSV normalization
For imports, always pair the number column with a country column; fall back to the default region only when the country is blank. Collect failures into a rejects file rather than aborting the whole import:
$parsed = rescue(fn () => PhoneNumber::parse($row['phone'], $row['country'] ?? null), null, false);
if (! $parsed?->isValid()) { $rejects[] = $row; continue; }
SMS vs voice routing
If you route SMS, check the number type before sending; landlines parse and validate fine but cannot receive SMS.
if ($phone->getType() !== 'mobile') { /* fall back to voice or skip */ }
(Exact type constant/method name depends on the wrapper — verify in src/.)
Testing
Phone formatting is deterministic, which makes it perfect for data providers. Test the parse → store → display round trip with a fixture of numbers from each market you serve (IT, US, GB, DE at minimum), plus known-invalid strings.
The default region is not optional. Parsing "020 7946 0018" without GB in context will either throw or silently produce a wrong result. Always pass the region explicitly at the call site, even if a default is configured — it makes the code self-documenting and survives config changes.
Catch parse exceptions. libphonenumber throws NumberParseException on unparseable input. Any controller path that calls parse() without a try/catch is a 500 waiting to happen. Wrap it once in a service and return null on failure.
isValid() vs "possible". A number can be possible (right length/shape) but not valid (not in an assigned range). For signup forms prefer isValid(). Reach for the looser check only when you intentionally want to accept numbers from regions where metadata lags.
Metadata staleness. libphonenumber's data changes as countries reallocate ranges. The v5.3.0 release notes mention only a PHP 8 update, so there is no explicit evidence the libphonenumber metadata was refreshed. Check the resolved libphonenumber-for-php version in composer.lock; if accurate validation in fast-changing markets matters, you may need to bump the underlying library independently of this package (composer replace/alias, or fork).
Maintenance risk is still the headline, but PHP 8 is no longer the blocker. v5.3.0 (upd php 8) removes the most common installation failure on modern stacks, but the package remains low-activity (1 star, zero dependents) and the release notes are a single terse line. Before adopting:
^5.3 and verify the composer.json constraints actually allow your Laravel version — a PHP 8 bump does not guarantee Laravel 10/11 are tested or allowed.composer.json PHP constraint, which tells you exactly how much to trust the "PHP 8 support" claim.App\Support\Phone) from day one. That adapter is your escape hatch to propaganistas/laravel-phone (actively maintained, same underlying lib) with a one-file change.Don't store formatted strings as lookup keys. Store E.164, index it, and format on read. Searching by "333 123 4567" will miss "+393331234567".
Extensions and short codes. parse() handles extensions (ext. 12) but your DB column and validation rule need to decide whether to keep them. Service numbers and short codes (e.g. 112, 190) frequently fail isValid() — whitelist them explicitly if your product needs them.
Config caching. If you rely on the package's config, remember php artisan config:cache in production; a runtime config() read of a non-cached key will behave differently between local and prod.
Debugging. When a number misbehaves, dump the parsed object's getCountryCode(), getRegionCode(), and the raw national number rather than guessing. Nine times out of ten the region was inferred wrong, not the number.
Extension points. If the facade is Macroable (common for Laravel packages), add project-specific macros (PhoneNumber::macro('forDisplay', ...)) instead of forking. If not, a thin App\Support\Phone wrapper gives you the same benefit and keeps you portable.
Vendor-lock escape plan. The PHP 8 release improves the odds you can install it, but does not change the package's long-term trajectory. Treat it as a convenience wrapper, not a foundation. Keep PhoneNumber:: calls confined to one or two classes so a swap to direct libphonenumber-for-php usage is a refactor of an afternoon, not a sprint.
How can I help you explore Laravel packages today?