216 lines
9.1 KiB
PHP
216 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace libphonenumber\geocoding;
|
|
|
|
|
|
use libphonenumber\NumberParseException;
|
|
use libphonenumber\PhoneNumber;
|
|
use libphonenumber\PhoneNumberType;
|
|
use libphonenumber\PhoneNumberUtil;
|
|
use libphonenumber\prefixmapper\PrefixFileReader;
|
|
|
|
class PhoneNumberOfflineGeocoder
|
|
{
|
|
const MAPPING_DATA_DIRECTORY = '/data';
|
|
/**
|
|
* @var PhoneNumberOfflineGeocoder
|
|
*/
|
|
private static $instance;
|
|
/**
|
|
* @var PhoneNumberUtil
|
|
*/
|
|
private $phoneUtil;
|
|
/**
|
|
* @var PrefixFileReader
|
|
*/
|
|
private $prefixFileReader = null;
|
|
|
|
private function __construct($phonePrefixDataDirectory)
|
|
{
|
|
if(!extension_loaded('intl')) {
|
|
throw new \RuntimeException('The intl extension must be installed');
|
|
}
|
|
|
|
$this->phoneUtil = PhoneNumberUtil::getInstance();
|
|
|
|
$this->prefixFileReader = new PrefixFileReader(dirname(__FILE__) . $phonePrefixDataDirectory);
|
|
}
|
|
|
|
/**
|
|
* Gets a PhoneNumberOfflineGeocoder instance to carry out international phone number geocoding.
|
|
*
|
|
* <p>The PhoneNumberOfflineGeocoder is implemented as a singleton. Therefore, calling this method
|
|
* multiple times will only result in one instance being created.
|
|
*
|
|
* @param string $mappingDir (Optional) Mapping Data Directory
|
|
* @return PhoneNumberOfflineGeocoder
|
|
*/
|
|
public static function getInstance($mappingDir = self::MAPPING_DATA_DIRECTORY)
|
|
{
|
|
if (self::$instance === null) {
|
|
self::$instance = new self($mappingDir);
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
public static function resetInstance()
|
|
{
|
|
self::$instance = null;
|
|
}
|
|
|
|
/**
|
|
* As per getDescriptionForValidNumber, but explicitly checks the validity of the number
|
|
* passed in.
|
|
*
|
|
*
|
|
* @see getDescriptionForValidNumber
|
|
* @param PhoneNumber $number a valid phone number for which we want to get a text description
|
|
* @param string $locale the language code for which the description should be written
|
|
* @param string $userRegion the region code for a given user. This region will be omitted from the
|
|
* description if the phone number comes from this region. It is a two-letter uppercase ISO
|
|
* country code as defined by ISO 3166-1.
|
|
* @return string a text description for the given language code for the given phone number, or empty
|
|
* string if the number passed in is invalid
|
|
*/
|
|
public function getDescriptionForNumber(PhoneNumber $number, $locale, $userRegion = null)
|
|
{
|
|
$numberType = $this->phoneUtil->getNumberType($number);
|
|
|
|
if ($numberType === PhoneNumberType::UNKNOWN) {
|
|
return "";
|
|
} elseif (!$this->canBeGeocoded($numberType)) {
|
|
return $this->getCountryNameForNumber($number, $locale);
|
|
}
|
|
|
|
return $this->getDescriptionForValidNumber($number, $locale, $userRegion);
|
|
}
|
|
|
|
/**
|
|
* A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
|
|
* stricter check, as it determines if a number has a geographical association. Also, if new
|
|
* phone number types were added, we should check if this other method should be updated too.
|
|
*
|
|
* @param int $numberType
|
|
* @return boolean
|
|
*/
|
|
private function canBeGeocoded($numberType)
|
|
{
|
|
return ($numberType === PhoneNumberType::FIXED_LINE || $numberType === PhoneNumberType::MOBILE || $numberType === PhoneNumberType::FIXED_LINE_OR_MOBILE);
|
|
}
|
|
|
|
/**
|
|
* Returns the customary display name in the given language for the given territory the phone
|
|
* number is from. If it could be from many territories, nothing is returned.
|
|
*
|
|
* @param PhoneNumber $number
|
|
* @param $locale
|
|
* @return string
|
|
*/
|
|
private function getCountryNameForNumber(PhoneNumber $number, $locale)
|
|
{
|
|
$regionCodes = $this->phoneUtil->getRegionCodesForCountryCode($number->getCountryCode());
|
|
|
|
if (count($regionCodes) === 1) {
|
|
return $this->getRegionDisplayName($regionCodes[0], $locale);
|
|
} else {
|
|
$regionWhereNumberIsValid = 'ZZ';
|
|
foreach ($regionCodes as $regionCode) {
|
|
if ($this->phoneUtil->isValidNumberForRegion($number, $regionCode)) {
|
|
if ($regionWhereNumberIsValid !== 'ZZ') {
|
|
// If we can't assign the phone number as definitely belonging to only one territory,
|
|
// then we return nothing.
|
|
return "";
|
|
}
|
|
$regionWhereNumberIsValid = $regionCode;
|
|
}
|
|
}
|
|
|
|
return $this->getRegionDisplayName($regionWhereNumberIsValid, $locale);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the customary display name in the given language for the given region.
|
|
*
|
|
* @param $regionCode
|
|
* @param $locale
|
|
* @return string
|
|
*/
|
|
private function getRegionDisplayName($regionCode, $locale)
|
|
{
|
|
if ($regionCode === null || $regionCode == 'ZZ' || $regionCode === PhoneNumberUtil::REGION_CODE_FOR_NON_GEO_ENTITY) {
|
|
return "";
|
|
}
|
|
|
|
return Locale::getDisplayRegion(
|
|
Locale::countryCodeToLocale($regionCode),
|
|
$locale
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a text description for the given phone number, in the language provided. The
|
|
* description might consist of the name of the country where the phone number is from, or the
|
|
* name of the geographical area the phone number is from if more detailed information is
|
|
* available.
|
|
*
|
|
* <p>This method assumes the validity of the number passed in has already been checked, and that
|
|
* the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
|
|
* candidates for geocoding.
|
|
*
|
|
* <p>If $userRegion is set, we also consider the region of the user. If the phone number is from
|
|
* the same region as the user, only a lower-level description will be returned, if one exists.
|
|
* Otherwise, the phone number's region will be returned, with optionally some more detailed
|
|
* information.
|
|
*
|
|
* <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
|
|
* CA" for a particular number, omitting the United States from the description. For a user from
|
|
* the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
|
|
* States" or even just "United States".
|
|
*
|
|
* @param PhoneNumber $number a valid phone number for which we want to get a text description
|
|
* @param string $locale the language code for which the description should be written
|
|
* @param string $userRegion the region code for a given user. This region will be omitted from the
|
|
* description if the phone number comes from this region. It is a two-letter uppercase ISO
|
|
* country code as defined by ISO 3166-1.
|
|
* @return string a text description for the given language code for the given phone number
|
|
*/
|
|
public function getDescriptionForValidNumber(PhoneNumber $number, $locale, $userRegion = null)
|
|
{
|
|
// If the user region matches the number's region, then we just show the lower-level
|
|
// description, if one exists - if no description exists, we will show the region(country) name
|
|
// for the number.
|
|
$regionCode = $this->phoneUtil->getRegionCodeForNumber($number);
|
|
if ($userRegion == null || $userRegion == $regionCode) {
|
|
$languageStr = Locale::getPrimaryLanguage($locale);
|
|
$scriptStr = "";
|
|
$regionStr = Locale::getRegion($locale);
|
|
|
|
$mobileToken = $this->phoneUtil->getCountryMobileToken($number->getCountryCode());
|
|
$nationalNumber = $this->phoneUtil->getNationalSignificantNumber($number);
|
|
if ($mobileToken !== "" && (!strncmp($nationalNumber, $mobileToken, strlen($mobileToken)))) {
|
|
// In some countries, eg. Argentina, mobile numbers have a mobile token before the national
|
|
// destination code, this should be removed before geocoding.
|
|
$nationalNumber = substr($nationalNumber, strlen($mobileToken));
|
|
$region = $this->phoneUtil->getRegionCodeForCountryCode($number->getCountryCode());
|
|
try {
|
|
$copiedNumber = $this->phoneUtil->parse($nationalNumber, $region);
|
|
} catch (NumberParseException $e) {
|
|
// If this happens, just reuse what we had.
|
|
$copiedNumber = $number;
|
|
}
|
|
$areaDescription = $this->prefixFileReader->getDescriptionForNumber($copiedNumber, $languageStr, $scriptStr, $regionStr);
|
|
} else {
|
|
$areaDescription = $this->prefixFileReader->getDescriptionForNumber($number, $languageStr, $scriptStr, $regionStr);
|
|
}
|
|
|
|
return (strlen($areaDescription) > 0) ? $areaDescription : $this->getCountryNameForNumber($number, $locale);
|
|
}
|
|
// Otherwise, we just show the region(country) name for now.
|
|
return $this->getRegionDisplayName($regionCode, $locale);
|
|
// TODO: Concatenate the lower-level and country-name information in an appropriate
|
|
// way for each language.
|
|
}
|
|
}
|