allbox / phone-number
An immutable, framework-independent phone number value object with optional Laravel integrations.
Requires
- php: ^8.2
- giggsey/libphonenumber-for-php: ^9.0
Requires (Dev)
- captainhook/captainhook: ^5.29
- friendsofphp/php-cs-fixer: ^3.80
- illuminate/database: ^12.0 || ^13.0
- illuminate/translation: ^12.0 || ^13.0
- illuminate/validation: ^12.0 || ^13.0
- larastan/larastan: ^3.10
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan-strict-rules: ^2.0.12
- phpunit/phpunit: ^11.5 || ^12.5 || ^13.0
- spatie/laravel-data: ^4.0
Suggests
- illuminate/database: Required for the Eloquent cast.
- illuminate/translation: Required for translated Laravel validation messages.
- illuminate/validation: Required for the native Laravel validation rule.
- spatie/laravel-data: Required for the Laravel Data cast and validation attribute.
This package is auto-updated.
Last update: 2026-08-13 08:48:44 UTC
README
An immutable international phone-number value object backed by
giggsey/libphonenumber-for-php,
with optional integrations for Eloquent, Laravel validation, and Spatie Laravel Data.
The core package has no framework dependency. Phone numbers are stored, stringified, and JSON-serialized in E.164 format.
Requirements
- PHP 8.2 or newer
giggsey/libphonenumber-for-php9.x
Installation
composer require allbox/phone-number
Core usage
use AllBox\PhoneNumber\ImmutablePhoneNumber; $phone = ImmutablePhoneNumber::parse('0912 123 4567', 'IR'); echo $phone->formatE164(); // +989121234567 echo $phone->formatInternational(); // +98 912 123 4567 echo $phone->formatNational(); // 0912 123 4567 echo $phone->formatRfc3966(); // tel:+98-912-123-4567 $phone->getCountryCode(); // '98' $phone->getRegionCode(); // 'IR', or null for a non-geographical number $phone->getNumberType(); // libphonenumber\PhoneNumberType $phone->isPossibleNumber(); $phone->isValidNumber();
parse() answers one narrow question: can libphonenumber parse this input? It deliberately does not imply that the number is valid. Use parseValid() when validity is part of your boundary:
$phone = ImmutablePhoneNumber::parseValid('09121234567', 'IR'); $phone = ImmutablePhoneNumber::tryParse($input, 'IR'); $validPhone = ImmutablePhoneNumber::tryParseValid($input, 'IR');
parse() throws PhoneNumberParseException. It exposes a stable PhoneNumberParseErrorType. parseValid() additionally throws InvalidPhoneNumberException for parseable but invalid input. The try* variants return null for their corresponding failures.
Extensions are rejected by every parsing API. The value object's canonical identity and persistence representation is E.164, which cannot represent an extension; accepting one would silently discard part of the input.
Two instances compare by normalized phone-number value:
$phone->isEqualTo(ImmutablePhoneNumber::parse('+989121234567'));
Eloquent
The cast persists E.164 strings and returns ImmutablePhoneNumber instances:
use AllBox\PhoneNumber\Eloquent\ImmutablePhoneNumberCast; protected function casts(): array { return [ 'mobile_number' => ImmutablePhoneNumberCast::class, ]; }
null remains null. Invalid non-null stored data throws instead of being silently converted to null; database corruption should not masquerade as an absent value.
Persisted values and string assignments must be strings, because numeric storage loses the leading + and can lose leading zeroes. Assign an ImmutablePhoneNumber, an E.164 string, or null.
Laravel validation
use AllBox\PhoneNumber\Laravel\Validation\PhoneNumberRule; use libphonenumber\PhoneNumberType; 'mobile_number' => [ 'required', new PhoneNumberRule( defaultRegion: 'IR', allowedRegions: ['IR'], allowedTypes: [PhoneNumberType::MOBILE], ), ],
An empty region or type list allows every value. Each validation message can be replaced through the constructor's formatMessage, regionMessage, and typeMessage arguments. This keeps localization in the owning application and avoids a service provider or global configuration.
Spatie Laravel Data
use AllBox\PhoneNumber\ImmutablePhoneNumber; use AllBox\PhoneNumber\LaravelData\Casts\ImmutablePhoneNumberCast; use AllBox\PhoneNumber\LaravelData\Rules\PhoneNumberRule; use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Data; final class ContactData extends Data { public function __construct( #[PhoneNumberRule(defaultRegion: 'IR', allowedRegions: 'IR')] #[WithCast(ImmutablePhoneNumberCast::class, 'IR')] public ImmutablePhoneNumber $phone, ) { } } $contact = ContactData::validateAndCreate([ 'phone' => '0912 123 4567', ]);
The Data cast maps only null and the empty string to null. Other malformed values fail explicitly.
Design and compatibility
- There is no mutable global default region. Pass a region at each input boundary.
- Formatting and number-type enums intentionally come from libphonenumber, avoiding a second abstraction that merely mirrors the upstream library.
- Validity follows the metadata version installed with libphonenumber and cannot prove that a number is assigned or reachable.
- Updating libphonenumber metadata can change validity or type results without changing this package.
Development
composer install composer quality composer hooks:install
composer quality runs the same style, strict static-analysis, and test gates used by CI. composer hooks:install installs the CaptainHook pre-commit hook for contributors after the repository has been cloned. The hook runs those same three checks before each commit.
See CONTRIBUTING.md for contribution and release expectations.
License
MIT. See LICENSE.