Understanding how libphonenumber-for-php manages resources is crucial for building high-performance applications. This guide covers optimization strategies and best practices.
Singleton Pattern
PhoneNumberUtil is implemented as a singleton to optimize memory usage and initialization overhead.
Always Use getInstance()
use libphonenumber\PhoneNumberUtil;
// ✅ CORRECT - Uses singleton instance
$phoneUtil = PhoneNumberUtil::getInstance();
// ❌ INCORRECT - Creates new instance each time (not recommended)
$phoneUtil = new PhoneNumberUtil(...);
The singleton pattern ensures that:
- Only one instance exists per process
- Metadata is loaded once and reused
- Memory footprint is minimized
How getInstance() Works
From src/PhoneNumberUtil.php:426-442:
/**
* Gets a PhoneNumberUtil instance to carry out international phone number
* formatting, parsing or validation. The instance is loaded with phone number
* metadata for a number of most commonly used regions.
*
* The PhoneNumberUtil is implemented as a singleton. Therefore calling
* getInstance multiple times will only result in one instance being created.
*/
public static function getInstance(
string $metadataLocation = __NAMESPACE__ . '\\data\\PhoneNumberMetadata_',
array $countryCallingCodeToRegionCodeMap = CountryCodeToRegionCodeMap::COUNTRY_CODE_TO_REGION_CODE_MAP,
): PhoneNumberUtil {
if (!isset(static::$instance)) {
$metadataSource = new MultiFileMetadataSourceImpl($metadataLocation);
static::$instance = new static($metadataSource, $countryCallingCodeToRegionCodeMap);
}
return static::$instance;
}
Calling getInstance() multiple times is safe and efficient - it returns the same instance without additional overhead.
One of the library’s key performance features is lazy loading of metadata. Country-specific metadata is only loaded when needed.
The MultiFileMetadataSourceImpl class loads metadata on-demand:
public function getMetadataForRegion(string $regionCode): PhoneMetadata
{
$regionCode = strtoupper($regionCode);
if (!isset($this->regionToMetadataMap[$regionCode])) {
// Load metadata only when first accessed
$this->loadMetadataFromFile($this->currentFilePrefix, $regionCode, 0);
}
return $this->regionToMetadataMap[$regionCode];
}
// First call for US numbers - loads US metadata
$usNumber = $phoneUtil->parse("+1 650 253 0000", "US");
// Subsequent US numbers - uses cached metadata (faster)
$usNumber2 = $phoneUtil->parse("+1 415 555 1234", "US");
// First call for UK numbers - loads UK metadata
$ukNumber = $phoneUtil->parse("+44 20 7031 3000", "GB");
The first parse operation for a region is slower due to metadata loading. Subsequent operations for the same region are significantly faster.
Caching PhoneNumber Objects
For frequently accessed phone numbers, consider caching the parsed PhoneNumber objects:
use libphonenumber\PhoneNumberUtil;
use libphonenumber\PhoneNumber;
class PhoneNumberCache
{
private array $cache = [];
private PhoneNumberUtil $phoneUtil;
public function __construct()
{
$this->phoneUtil = PhoneNumberUtil::getInstance();
}
public function parse(string $number, string $region): PhoneNumber
{
$key = $number . '_' . $region;
if (!isset($this->cache[$key])) {
$this->cache[$key] = $this->phoneUtil->parse($number, $region);
}
return $this->cache[$key];
}
}
When to Cache
Caching is beneficial when:
- Processing the same phone numbers multiple times
- Displaying formatted numbers in lists or tables
- Performing multiple validation operations on the same number
Cache Considerations
Be mindful of memory usage when caching PhoneNumber objects. Each object consumes memory, so implement cache size limits or use a proper caching solution like Redis or Memcached for large-scale applications.
Batch Processing
When processing multiple phone numbers, reuse the PhoneNumberUtil instance:
// ✅ EFFICIENT - Single getInstance call
$phoneUtil = PhoneNumberUtil::getInstance();
foreach ($phoneNumbers as $number) {
try {
$parsed = $phoneUtil->parse($number, $defaultRegion);
if ($phoneUtil->isValidNumber($parsed)) {
$formatted = $phoneUtil->format($parsed, PhoneNumberFormat::E164);
// Process formatted number
}
} catch (\Exception $e) {
// Handle error
}
}
// ❌ INEFFICIENT - Multiple getInstance calls (unnecessary overhead)
foreach ($phoneNumbers as $number) {
$phoneUtil = PhoneNumberUtil::getInstance(); // Don't do this!
// ...
}
Database Storage
For optimal database performance, store phone numbers in E164 format:
use libphonenumber\PhoneNumberFormat;
// Store in E164 format: +14155551234
$e164 = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
// Store in database
$db->query("INSERT INTO contacts (phone) VALUES (?)", [$e164]);
Benefits of E164 Storage
- Consistent format across all phone numbers
- Easy comparison and deduplication
- Efficient indexing and searching
- No loss of country code information
Retrieving from Database
// Retrieve E164 format from database
$e164Number = $db->query("SELECT phone FROM contacts WHERE id = ?", [$id]);
// Parse for display
$phoneNumber = $phoneUtil->parse($e164Number, null);
$formatted = $phoneUtil->format($phoneNumber, PhoneNumberFormat::INTERNATIONAL);
echo $formatted; // "+1 415 555 1234"
Production Optimizations
1. Enable OPcache
Ensure OPcache is enabled to cache compiled PHP code:
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
2. Use Composer Autoloader Optimization
Generate optimized autoloader for production:
composer install --optimize-autoloader --no-dev
3. Consider the Lite Version
For applications that only need core functionality, use the lite version:
composer require giggsey/libphonenumber-for-php-lite
See the Lite Version page for details on what’s included and performance benefits.
Memory Considerations
The full library with all metadata loaded uses approximately:
- PhoneNumberUtil instance: ~2-5 MB
- Per region metadata: ~50-200 KB
- PhoneNumber objects: ~1-2 KB each
Memory Profiling
$memBefore = memory_get_usage();
$phoneUtil = PhoneNumberUtil::getInstance();
$number = $phoneUtil->parse("+1 650 253 0000", "US");
$memAfter = memory_get_usage();
$memUsed = $memAfter - $memBefore;
echo "Memory used: " . ($memUsed / 1024) . " KB\n";
Typical operation performance (approximate):
| Operation | First Call | Subsequent Calls |
|---|
| getInstance() | ~1-2ms | ~0.01ms |
| parse() (new region) | ~5-10ms | ~1-2ms |
| parse() (cached region) | ~1-2ms | ~1-2ms |
| isValidNumber() | ~0.5-1ms | ~0.5-1ms |
| format() | ~0.5-1ms | ~0.5-1ms |
These benchmarks vary based on hardware, PHP version, and OPcache configuration. Always benchmark in your specific environment.
Dependency Injection
In frameworks with dependency injection, register PhoneNumberUtil as a singleton:
// Symfony example
services:
libphonenumber.phone_number_util:
class: libphonenumber\PhoneNumberUtil
factory: ['libphonenumber\PhoneNumberUtil', 'getInstance']
shared: true
// Laravel example
use Illuminate\Support\ServiceProvider;
use libphonenumber\PhoneNumberUtil;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(PhoneNumberUtil::class, function () {
return PhoneNumberUtil::getInstance();
});
}
}
Monitoring and Profiling
Monitor performance in production:
use libphonenumber\PhoneNumberUtil;
class MonitoredPhoneNumberUtil
{
private PhoneNumberUtil $phoneUtil;
private LoggerInterface $logger;
public function parse(string $number, string $region): PhoneNumber
{
$start = microtime(true);
try {
$result = $this->phoneUtil->parse($number, $region);
$duration = microtime(true) - $start;
if ($duration > 0.1) {
$this->logger->warning('Slow phone number parse', [
'duration' => $duration,
'region' => $region
]);
}
return $result;
} catch (\Exception $e) {
$this->logger->error('Phone number parse error', [
'error' => $e->getMessage(),
'number' => $number,
'region' => $region
]);
throw $e;
}
}
}