propaganistas/laravel-phone
Add robust phone number validation, casting, and formatting to Laravel using Google’s libphonenumber (PHP port). Validate by country or dynamic country fields, cast model attributes to phone objects, format numbers consistently, and compare/evaluate phone metadata.
Installation:
composer require propaganistas/laravel-phone
Add translation key to resources/lang/{locale}/validation.php:
'phone' => 'The :attribute field must be a valid number.',
First Use Case: Validate a phone number in a form request:
use Propaganistas\LaravelPhone\Rules\Phone;
public function rules()
{
return [
'phone' => ['required', new Phone],
];
}
Basic Validation:
// Array syntax
'phone' => 'phone:US,BE'
// Rule object syntax
new Phone()->country(['US', 'BE'])
Dynamic Country Validation:
// Auto-detect country from `phone_country` field
'phone' => 'phone',
'phone_country' => 'required_with:phone'
// Custom country field
'phone' => 'phone:custom_country_field',
'custom_country_field' => 'required_with:phone'
Type Constraints:
// Whitelist mobile numbers
'phone' => 'phone:mobile'
// Blacklist fixed-line numbers
'phone' => 'phone:!fixed_line'
Lenient Validation:
'phone' => 'phone:LENIENT' // Checks length only
Casting Phone Numbers:
use Propaganistas\LaravelPhone\Casts\E164PhoneNumberCast;
class User extends Model
{
protected $casts = [
'phone' => E164PhoneNumberCast::class.':BE',
];
}
Handling Raw Input:
use Propaganistas\LaravelPhone\Casts\RawPhoneNumberCast;
protected $casts = [
'phone' => RawPhoneNumberCast::class.':country_field',
];
Order of Attribute Assignment:
// Correct: Set country before phone
$user->phone_country = 'BE';
$user->phone = '012 34 56 78';
Formatting:
$phone = phone('012 34 56 78', 'BE');
$formatted = $phone->formatNational(); // "012 34 56 78"
Country-Specific Formatting:
$phone->formatForCountry('US'); // "011 32 12 34 56 78"
Comparison:
$phone1 = phone('+3212345678');
$phone2 = phone('012 34 56 78', 'BE');
$phone1->equals($phone2); // true
Type Checking:
$phone->isOfType('mobile'); // true/false
E.164 Storage:
// Model observer or accessor
$user->phone_e164 = phone($user->phone, $user->phone_country)->formatE164();
Search Optimization:
// Observer for precomputing search fields
public function saving(User $user)
{
if ($user->isDirty('phone')) {
$phone = phone($user->phone, $user->phone_country);
$user->phone_normalized = preg_replace('/[^0-9]/', '', $user->phone);
$user->phone_national = preg_replace('/[^0-9]/', '', $phone->formatNational());
$user->phone_e164 = $phone->formatE164();
}
}
Querying:
User::where(function($query) use ($search) {
$query->where('phone_normalized', 'LIKE', $normalizedSearch.'%')
->orWhere('phone_e164', 'LIKE', $e164Search.'%');
});
Country Dependency:
// Validation
'phone' => 'phone:BE'
// Casting
'phone' => E164PhoneNumberCast::class.':BE'
Attribute Assignment Order:
phone before phone_country with E164PhoneNumberCast.$user->phone_country = 'BE';
$user->phone = '012 34 56 78'; // Now works
Validation Strictness:
LENIENT mode without realizing it bypasses carrier pattern checks.// Avoid unless necessary
'phone' => 'phone:LENIENT'
Database Collation:
utf8mb4_bin).ALTER TABLE users MODIFY phone_e164 VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
Validation Errors:
PhoneNumberUtil::isValidNumber() to debug:
use libphonenumber\PhoneNumberUtil;
$util = PhoneNumberUtil::getInstance();
$util->isValidNumber($phoneNumber);
Country Code Issues:
US, not USA).// Wrong: 'BE' vs 'be'
'phone' => 'phone:be' // Fails silently
Casting Edge Cases:
// Correct
'phone' => E164PhoneNumberCast::class.':BE'
// Incorrect (missing colon)
'phone' => E164PhoneCast::class.BE // Throws error
Avoid Over-Fetching:
PhoneNumberUtil instance:
$util = PhoneNumberUtil::getInstance(); // Expensive; reuse
Batch Processing:
$users = User::whereNull('phone_e164')->get();
foreach ($users as $user) {
$user->phone_e164 = phone($user->phone, $user->phone_country)->formatE164();
}
User::upsert($users->toArray());
Custom Formatting:
PhoneNumber class:
class CustomPhoneNumber extends PhoneNumber
{
public function formatCustom()
{
return $this->formatNational().' ('.$this->getCountry().')';
}
}
Validation Rules:
use Propaganistas\LaravelPhone\Rules\Phone;
class CustomPhoneRule extends Phone
{
public function passes($attribute, $value)
{
if (!parent::passes($attribute, $value)) {
return false;
}
// Add custom logic
return true;
}
}
Database Observers:
class PhoneObserver
{
public function saving(User $user)
{
if ($user->isDirty('phone')) {
$phone = phone($user->phone, $user->phone_country);
$user->phone_normalized = $this->normalize($phone->getNationalNumber());
$user->phone_e164 = $phone->formatE164();
}
}
protected function normalize($number)
{
return preg_replace('/[^0-9]/', '', $number);
}
}
How can I help you explore Laravel packages today?