Guide
Models and queries
The cast
use Devix\Phone\Laravel\Casts\AsPhoneNumber;
protected function casts(): array
{
return [
'phone' => AsPhoneNumber::class, // country from phone_country, then config
'mobile' => AsPhoneNumber::class.':AE', // national input is always AE
'office' => AsPhoneNumber::using('office_country') // country from another attribute
];
}
- Writing accepts a string or a
PhoneNumber. Complete numbers are stored as E.164 (+971501234567), with;ext=12when there is an extension. - Reading returns a
PhoneNumber(ornull). - Input that isn't a number is stored exactly as typed, so nothing is lost. Validate first to keep the column clean.
A varchar(32) column is enough for E.164 plus an extension.
The trait
use Devix\Phone\Laravel\Concerns\HasPhoneNumbers;
class Customer extends Model
{
use HasPhoneNumbers;
}
Attribute order stops mattering. Set the number before its country and the model still stores E.164, because numbers are normalised again when it saves:
$customer->phone = '0300 1234567';
$customer->phone_country = 'PK';
$customer->save(); // phone = +923001234567
Search however it was typed:
Customer::wherePhone('phone', '0300-1234567', 'PK')->get();
Customer::wherePhone('phone', '+92 300 1234567')->get();
Customer::wherePhone('phone', $request->phone('phone'))->get();
Input that isn't a number matches nothing, rather than everything.
Unique numbers
Because every number is stored the same way, Laravel's own unique rule works — validate against the
E.164 form:
$e164 = $request->phone('phone')?->e164();
Validator::make(['phone' => $e164], ['phone' => Rule::unique('users', 'phone')->ignore($user)]);
API resources and Inertia
PhoneNumber is JsonSerializable:
{ "e164": "+971501234567", "international": "+971 50 123 4567", "national": "050 123 4567",
"country": "AE", "type": "mobile", "valid": true, "reason": "valid" }
Updated 12 Sep 2026