allbox/phone-number

An immutable, framework-independent phone number value object with optional Laravel integrations.

Maintainers

Package info

github.com/allbox-tech/phone-number

pkg:composer/allbox/phone-number

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.1 2026-08-13 08:07 UTC

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-php 9.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.