Skip to content

使用 libphonenumber 在 PHP 中构建电话号码工具类:国家、语言和时区检测

简介

在构建全球化应用时,电话号码通常是最可靠的用户数据。与电子邮件地址或用户名不同,电话号码包含固有的地理信息,可以帮助个性化用户体验。从自动选择正确的通知语言到在适当的本地时间安排消息发送,从电话号码中提取元数据是非常有价值的。

本指南演示如何使用 Google 的 libphonenumber 库(通过 PHP 版本)构建一个 PhoneUtil 类,从国际电话号码中提取国家代码、推断首选语言和检测时区。

前置条件

  • PHP 8.1 或更高版本
  • Composer 包管理器
  • PHP 类和异常处理的基础知识

安装 libphonenumber-for-php

通过 Composer 安装库:

bash
composer require giggsey/libphonenumber-for-php

精简版可用

如果你只需要基本的解析和格式化功能,不需要地理编码或运营商信息,可以使用 giggsey/libphonenumber-for-php-lite 获得更小的安装体积。

构建 PhoneUtil 类

让我们创建一个工具类,提供三个核心方法:国家代码检测、语言推断和时区解析。

基本结构

php
<?php

namespace App\Utils;

use libphonenumber\PhoneNumberToTimeZonesMapper;
use libphonenumber\PhoneNumberUtil;
use Throwable;

class PhoneUtil
{
    // 方法将在这里添加
}

获取国家代码

第一个方法从电话号码中提取 ISO 3166-1 alpha-2 国家代码(如 "US"、"BR"、"MX"):

php
public static function getCountryCode(string $phone_number): ?string
{
    try {
        $phoneUtil = PhoneNumberUtil::getInstance();
        $phoneObj = $phoneUtil->parse("+{$phone_number}");

        return $phoneUtil->getRegionCodeForNumber($phoneObj);
    } catch (Throwable $e) {
        // 记录或报告错误
        return null;
    }
}

电话号码格式

此实现期望电话号码不包含前导 + 符号。方法在解析时会自动添加。请根据你的应用存储电话号码的方式进行调整。

推断用户语言

基于国家代码,我们可以对用户的首选语言做出合理推断。这对于发送本地化通知特别有用:

php
public static function getCountryLanguage(string $phone_number): ?string
{
    try {
        $country_code = self::getCountryCode($phone_number);

        if (! $country_code) {
            return null;
        }

        return match ($country_code) {
            // 西班牙语国家
            'ES', 'MX', 'AR', 'CO', 'PE', 'VE', 'CL', 'EC',
            'GT', 'CU', 'BO', 'DO', 'HN', 'PY', 'SV', 'NI',
            'CR', 'PA', 'UY' => 'es',

            // 葡萄牙语
            'BR' => 'pt_BR',

            // 中文
            'CN', 'TW', 'HK', 'SG' => 'zh_CN',

            // 印地语
            'IN' => 'hi',

            // 默认英语
            default => 'en',
        };
    } catch (Throwable $e) {
        return null;
    }
}

扩展语言支持

上述映射涵盖了主要语言。根据你的应用支持的语言环境进行扩展。可以考虑添加法国(FR => fr)、德国(DE => de)或日本(JP => ja)等国家。

检测时区

时区检测对于在适当时间安排用户通信至关重要。libphonenumber 库提供了 PhoneNumberToTimeZonesMapper 类来实现这一功能:

php
public static function getTimezone(string $phone_number): ?string
{
    try {
        $country_code = self::getCountryCode($phone_number);
        $phoneUtil = PhoneNumberUtil::getInstance();
        $phoneObj = $phoneUtil->parse("+{$phone_number}", $country_code);
        $timezoneMapper = PhoneNumberToTimeZonesMapper::getInstance();
        $timezones = $timezoneMapper->getTimeZonesForNumber($phoneObj);
        $timezone = $timezones[0] ?? null;

        if (! $timezone) {
            return null;
        }

        // 处理未知时区的回退
        if ($timezone === 'Etc/Unknown') {
            return match ($country_code) {
                'MX' => 'America/Mexico_City',
                'BR' => 'America/Sao_Paulo',
                'IN' => 'Asia/Kolkata',
                default => null,
            };
        }

        return $timezone;
    } catch (Throwable $e) {
        return null;
    }
}

处理已弃用的时区

某些电话号码可能返回已弃用的时区标识符。将它们映射到现代等效标识符是一个好习惯:

php
public static function getTimezone(string $phone_number): ?string
{
    try {
        // ... 前面的代码 ...

        // 将已弃用的时区映射到现代等效时区
        $deprecatedTimezones = [
            'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
            'America/Catamarca' => 'America/Argentina/Catamarca',
            'America/Cordoba' => 'America/Argentina/Cordoba',
            'America/Jujuy' => 'America/Argentina/Jujuy',
            'America/Mendoza' => 'America/Argentina/Mendoza',
        ];

        if (isset($deprecatedTimezones[$timezone])) {
            return $deprecatedTimezones[$timezone];
        }

        return $timezone;
    } catch (Throwable $e) {
        return null;
    }
}

完整实现

这是组合所有方法的完整 PhoneUtil 类:

php
<?php

namespace App\Utils;

use libphonenumber\PhoneNumberToTimeZonesMapper;
use libphonenumber\PhoneNumberUtil;
use Throwable;

class PhoneUtil
{
    public static function getCountryCode(string $phone_number): ?string
    {
        try {
            $phoneUtil = PhoneNumberUtil::getInstance();
            $phoneObj = $phoneUtil->parse("+{$phone_number}");

            return $phoneUtil->getRegionCodeForNumber($phoneObj);
        } catch (Throwable $e) {
            // 考虑记录日志: Errors::reportThrowable($e);
            return null;
        }
    }

    public static function getCountryLanguage(string $phone_number): ?string
    {
        try {
            $country_code = self::getCountryCode($phone_number);

            if (! $country_code) {
                return null;
            }

            return match ($country_code) {
                'ES', 'MX', 'AR', 'CO', 'PE', 'VE', 'CL', 'EC',
                'GT', 'CU', 'BO', 'DO', 'HN', 'PY', 'SV', 'NI',
                'CR', 'PA', 'UY' => 'es',
                'BR' => 'pt_BR',
                'CN', 'TW', 'HK', 'SG' => 'zh_CN',
                'IN' => 'hi',
                default => 'en',
            };
        } catch (Throwable $e) {
            return null;
        }
    }

    public static function getTimezone(string $phone_number): ?string
    {
        try {
            $country_code = self::getCountryCode($phone_number);
            $phoneUtil = PhoneNumberUtil::getInstance();
            $phoneObj = $phoneUtil->parse("+{$phone_number}", $country_code);
            $timezoneMapper = PhoneNumberToTimeZonesMapper::getInstance();
            $timezones = $timezoneMapper->getTimeZonesForNumber($phoneObj);
            $timezone = $timezones[0] ?? null;

            if (! $timezone) {
                return null;
            }

            if ($timezone === 'Etc/Unknown') {
                return match ($country_code) {
                    'MX' => 'America/Mexico_City',
                    'BR' => 'America/Sao_Paulo',
                    'IN' => 'Asia/Kolkata',
                    default => null,
                };
            }

            $deprecatedTimezones = [
                'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
                'America/Catamarca' => 'America/Argentina/Catamarca',
                'America/Cordoba' => 'America/Argentina/Cordoba',
                'America/Jujuy' => 'America/Argentina/Jujuy',
                'America/Mendoza' => 'America/Argentina/Mendoza',
            ];

            return $deprecatedTimezones[$timezone] ?? $timezone;
        } catch (Throwable $e) {
            return null;
        }
    }
}

使用示例

基本用法

php
use App\Utils\PhoneUtil;

// 巴西电话号码
$phone = '5511999998888';

$country = PhoneUtil::getCountryCode($phone);      // "BR"
$language = PhoneUtil::getCountryLanguage($phone); // "pt_BR"
$timezone = PhoneUtil::getTimezone($phone);        // "America/Sao_Paulo"

实际应用:本地化通知

php
public function sendNotification(User $user, string $message): void
{
    $language = PhoneUtil::getCountryLanguage($user->phone);
    $timezone = PhoneUtil::getTimezone($user->phone);

    // 设置翻译的语言环境
    app()->setLocale($language ?? 'en');

    // 在适当的本地时间安排发送
    $sendAt = now($timezone)->setTime(10, 0);

    Notification::send($user, new UserNotification(
        message: __($message),
        scheduledAt: $sendAt
    ));
}

调试电话号码

libphonenumber 项目提供了一个在线演示,用于测试电话号码解析:

https://libphonenumber.appspot.com

在报告问题之前,使用此工具验证预期行为。

故障排除

返回未知时区

对于某些移动号码,特别是在巴西、墨西哥或印度等大国,库可能返回 Etc/Unknown。这发生在号码没有提供足够的地理特异性时。如上所示,使用国家级回退来处理这种情况。

解析失败

解析失败的常见原因:

  • 电话号码格式无效
  • 国家代码前缀缺失或不正确
  • 输入中包含非数字字符

在处理之前始终对电话号码进行清理:

php
$phone = preg_replace('/[^0-9]/', '', $phone);

结论

使用 libphonenumber 构建电话号码工具为国际化应用提供了可靠的基础。通过提取国家代码、推断语言和检测时区,你可以提供尊重用户地理环境的个性化体验。

关键要点:

  • 使用 PhoneNumberUtil 进行解析和国家检测
  • 将国家代码映射到你支持的语言环境以进行语言推断
  • 使用合理的国家级默认值处理 Etc/Unknown 时区
  • 将已弃用的时区标识符映射到其现代等效项

对于有严格要求的应用,考虑允许用户在其偏好设置中覆盖推断的值,同时使用基于电话的检测作为智能默认值。