ShortNumberInfo.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. <?php
  2. /**
  3. * Methods for getting information about short phone numbers, such as short codes and emergency
  4. * numbers. Note that most commercial short numbers are not handled here, but by the
  5. * {@link PhoneNumberUtil}.
  6. *
  7. * @author Shaopeng Jia
  8. * @author David Yonge-Mallo
  9. * @since 5.8
  10. */
  11. namespace libphonenumber;
  12. class ShortNumberInfo
  13. {
  14. const META_DATA_FILE_PREFIX = 'ShortNumberMetadata';
  15. /**
  16. * @var ShortNumberInfo
  17. */
  18. protected static $instance;
  19. /**
  20. * @var MatcherAPIInterface
  21. */
  22. protected $matcherAPI;
  23. protected $currentFilePrefix;
  24. protected $regionToMetadataMap = array();
  25. protected $countryCallingCodeToRegionCodeMap = array();
  26. protected $countryCodeToNonGeographicalMetadataMap = array();
  27. protected static $regionsWhereEmergencyNumbersMustBeExact = array(
  28. 'BR',
  29. 'CL',
  30. 'NI',
  31. );
  32. protected function __construct(MatcherAPIInterface $matcherAPI)
  33. {
  34. $this->matcherAPI = $matcherAPI;
  35. // TODO: Create ShortNumberInfo for a given map
  36. $this->countryCallingCodeToRegionCodeMap = CountryCodeToRegionCodeMap::$countryCodeToRegionCodeMap;
  37. $this->currentFilePrefix = dirname(__FILE__) . '/data/' . static::META_DATA_FILE_PREFIX;
  38. // Initialise PhoneNumberUtil to make sure regex's are setup correctly
  39. PhoneNumberUtil::getInstance();
  40. }
  41. /**
  42. * Returns the singleton instance of ShortNumberInfo
  43. *
  44. * @return \libphonenumber\ShortNumberInfo
  45. */
  46. public static function getInstance()
  47. {
  48. if (null === static::$instance) {
  49. static::$instance = new self(RegexBasedMatcher::create());
  50. }
  51. return static::$instance;
  52. }
  53. public static function resetInstance()
  54. {
  55. static::$instance = null;
  56. }
  57. /**
  58. * Returns a list with teh region codes that match the specific country calling code. For
  59. * non-geographical country calling codes, the region code 001 is returned. Also, in the case
  60. * of no region code being found, an empty list is returned.
  61. *
  62. * @param int $countryCallingCode
  63. * @return array
  64. */
  65. protected function getRegionCodesForCountryCode($countryCallingCode)
  66. {
  67. if (!array_key_exists($countryCallingCode, $this->countryCallingCodeToRegionCodeMap)) {
  68. $regionCodes = null;
  69. } else {
  70. $regionCodes = $this->countryCallingCodeToRegionCodeMap[$countryCallingCode];
  71. }
  72. return ($regionCodes === null) ? array() : $regionCodes;
  73. }
  74. /**
  75. * Helper method to check that the country calling code of the number matches the region it's
  76. * being dialed from.
  77. * @param PhoneNumber $number
  78. * @param string $regionDialingFrom
  79. * @return bool
  80. */
  81. protected function regionDialingFromMatchesNumber(PhoneNumber $number, $regionDialingFrom)
  82. {
  83. $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
  84. return in_array($regionDialingFrom, $regionCodes);
  85. }
  86. public function getSupportedRegions()
  87. {
  88. return ShortNumbersRegionCodeSet::$shortNumbersRegionCodeSet;
  89. }
  90. /**
  91. * Gets a valid short number for the specified region.
  92. *
  93. * @param $regionCode String the region for which an example short number is needed
  94. * @return string a valid short number for the specified region. Returns an empty string when the
  95. * metadata does not contain such information.
  96. */
  97. public function getExampleShortNumber($regionCode)
  98. {
  99. $phoneMetadata = $this->getMetadataForRegion($regionCode);
  100. if ($phoneMetadata === null) {
  101. return '';
  102. }
  103. /** @var PhoneNumberDesc $desc */
  104. $desc = $phoneMetadata->getShortCode();
  105. if ($desc !== null && $desc->hasExampleNumber()) {
  106. return $desc->getExampleNumber();
  107. }
  108. return '';
  109. }
  110. /**
  111. * @param $regionCode
  112. * @return PhoneMetadata|null
  113. */
  114. public function getMetadataForRegion($regionCode)
  115. {
  116. if (!in_array($regionCode, ShortNumbersRegionCodeSet::$shortNumbersRegionCodeSet)) {
  117. return null;
  118. }
  119. if (!isset($this->regionToMetadataMap[$regionCode])) {
  120. // The regionCode here will be valid and won't be '001', so we don't need to worry about
  121. // what to pass in for the country calling code.
  122. $this->loadMetadataFromFile($this->currentFilePrefix, $regionCode, 0);
  123. }
  124. return isset($this->regionToMetadataMap[$regionCode]) ? $this->regionToMetadataMap[$regionCode] : null;
  125. }
  126. protected function loadMetadataFromFile($filePrefix, $regionCode, $countryCallingCode)
  127. {
  128. $isNonGeoRegion = PhoneNumberUtil::REGION_CODE_FOR_NON_GEO_ENTITY === $regionCode;
  129. $fileName = $filePrefix . '_' . ($isNonGeoRegion ? $countryCallingCode : $regionCode) . '.php';
  130. if (!is_readable($fileName)) {
  131. throw new \Exception('missing metadata: ' . $fileName);
  132. }
  133. $metadataLoader = new DefaultMetadataLoader();
  134. $data = $metadataLoader->loadMetadata($fileName);
  135. $metadata = new PhoneMetadata();
  136. $metadata->fromArray($data);
  137. if ($isNonGeoRegion) {
  138. $this->countryCodeToNonGeographicalMetadataMap[$countryCallingCode] = $metadata;
  139. } else {
  140. $this->regionToMetadataMap[$regionCode] = $metadata;
  141. }
  142. }
  143. /**
  144. * Gets a valid short number for the specified cost category.
  145. *
  146. * @param string $regionCode the region for which an example short number is needed
  147. * @param int $cost the cost category of number that is needed
  148. * @return string a valid short number for the specified region and cost category. Returns an empty string
  149. * when the metadata does not contain such information, or the cost is UNKNOWN_COST.
  150. */
  151. public function getExampleShortNumberForCost($regionCode, $cost)
  152. {
  153. $phoneMetadata = $this->getMetadataForRegion($regionCode);
  154. if ($phoneMetadata === null) {
  155. return '';
  156. }
  157. /** @var PhoneNumberDesc $desc */
  158. $desc = null;
  159. switch ($cost) {
  160. case ShortNumberCost::TOLL_FREE:
  161. $desc = $phoneMetadata->getTollFree();
  162. break;
  163. case ShortNumberCost::STANDARD_RATE:
  164. $desc = $phoneMetadata->getStandardRate();
  165. break;
  166. case ShortNumberCost::PREMIUM_RATE:
  167. $desc = $phoneMetadata->getPremiumRate();
  168. break;
  169. default:
  170. // UNKNOWN_COST numbers are computed by the process of elimination from the other cost categories
  171. break;
  172. }
  173. if ($desc !== null && $desc->hasExampleNumber()) {
  174. return $desc->getExampleNumber();
  175. }
  176. return '';
  177. }
  178. /**
  179. * Returns true if the given number, exactly as dialed, might be used to connect to an emergency
  180. * service in the given region.
  181. * <p>
  182. * This method accepts a string, rather than a PhoneNumber, because it needs to distinguish
  183. * cases such as "+1 911" and "911", where the former may not connect to an emergency service in
  184. * all cases but the latter would. This method takes into account cases where the number might
  185. * contain formatting, or might have additional digits appended (when it is okay to do that in
  186. * the specified region).
  187. *
  188. * @param string $number the phone number to test
  189. * @param string $regionCode the region where the phone number if being dialled
  190. * @return boolean whether the number might be used to connect to an emergency service in the given region
  191. */
  192. public function connectsToEmergencyNumber($number, $regionCode)
  193. {
  194. return $this->matchesEmergencyNumberHelper($number, $regionCode, true /* allows prefix match */);
  195. }
  196. /**
  197. * @param string $number
  198. * @param string $regionCode
  199. * @param bool $allowPrefixMatch
  200. * @return bool
  201. */
  202. protected function matchesEmergencyNumberHelper($number, $regionCode, $allowPrefixMatch)
  203. {
  204. $number = PhoneNumberUtil::extractPossibleNumber($number);
  205. $matcher = new Matcher(PhoneNumberUtil::$PLUS_CHARS_PATTERN, $number);
  206. if ($matcher->lookingAt()) {
  207. // Returns false if the number starts with a plus sign. We don't believe dialing the country
  208. // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can
  209. // add additional logic here to handle it.
  210. return false;
  211. }
  212. $metadata = $this->getMetadataForRegion($regionCode);
  213. if ($metadata === null || !$metadata->hasEmergency()) {
  214. return false;
  215. }
  216. $normalizedNumber = PhoneNumberUtil::normalizeDigitsOnly($number);
  217. $emergencyDesc = $metadata->getEmergency();
  218. $allowPrefixMatchForRegion = (
  219. $allowPrefixMatch
  220. && !in_array($regionCode, static::$regionsWhereEmergencyNumbersMustBeExact)
  221. );
  222. return $this->matcherAPI->matchNationalNumber($normalizedNumber, $emergencyDesc, $allowPrefixMatchForRegion);
  223. }
  224. /**
  225. * Given a valid short number, determines whether it is carrier-specific (however, nothing is
  226. * implied about its validity). Carrier-specific numbers may connect to a different end-point, or
  227. * not connect at all, depending on the user's carrier. If it is important that the number is
  228. * valid, then its validity must first be checked using {@link #isValidShortNumber} or
  229. * {@link #isValidShortNumberForRegion}.
  230. *
  231. * @param PhoneNumber $number the valid short number to check
  232. * @return boolean whether the short number is carrier-specific, assuming the input was a valid short
  233. * number
  234. */
  235. public function isCarrierSpecific(PhoneNumber $number)
  236. {
  237. $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
  238. $regionCode = $this->getRegionCodeForShortNumberFromRegionList($number, $regionCodes);
  239. $nationalNumber = $this->getNationalSignificantNumber($number);
  240. $phoneMetadata = $this->getMetadataForRegion($regionCode);
  241. return ($phoneMetadata !== null) && $this->matchesPossibleNumberAndNationalNumber(
  242. $nationalNumber,
  243. $phoneMetadata->getCarrierSpecific()
  244. );
  245. }
  246. /**
  247. * Given a valid short number, determines whether it is carrier-specific when dialed from the
  248. * given region (however, nothing is implied about its validity). Carrier-specific numbers may
  249. * connect to a different end-point, or not connect at all, depending on the user's carrier. If
  250. * it is important that the number is valid, then its validity must first be checked using
  251. * {@link #isValidShortNumber} or {@link #isValidShortNumberForRegion}. Returns false if the
  252. * number doesn't match the region provided.
  253. * @param PhoneNumber $number The valid short number to check
  254. * @param string $regionDialingFrom The region from which the number is dialed
  255. * @return bool Whether the short number is carrier-specific in the provided region, assuming the
  256. * input was a valid short number
  257. */
  258. public function isCarrierSpecificForRegion(PhoneNumber $number, $regionDialingFrom)
  259. {
  260. if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
  261. return false;
  262. }
  263. $nationalNumber = $this->getNationalSignificantNumber($number);
  264. $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
  265. return ($phoneMetadata !== null)
  266. && $this->matchesPossibleNumberAndNationalNumber($nationalNumber, $phoneMetadata->getCarrierSpecific());
  267. }
  268. /**
  269. * Given a valid short number, determines whether it is an SMS service (however, nothing is
  270. * implied about its validity). An SMS service is where the primary or only intended usage is to
  271. * receive and/or send text messages (SMSs). This includes MMS as MMS numbers downgrade to SMS if
  272. * the other party isn't MMS-capable. If it is important that the number is valid, then its
  273. * validity must first be checked using {@link #isValidShortNumber} or {@link
  274. * #isValidShortNumberForRegion}. Returns false if the number doesn't match the region provided.
  275. *
  276. * @param PhoneNumber $number The valid short number to check
  277. * @param string $regionDialingFrom The region from which the number is dialed
  278. * @return bool Whether the short number is an SMS service in the provided region, assuming the input
  279. * was a valid short number.
  280. */
  281. public function isSmsServiceForRegion(PhoneNumber $number, $regionDialingFrom)
  282. {
  283. if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
  284. return false;
  285. }
  286. $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
  287. return ($phoneMetadata !== null)
  288. && $this->matchesPossibleNumberAndNationalNumber(
  289. $this->getNationalSignificantNumber($number),
  290. $phoneMetadata->getSmsServices()
  291. );
  292. }
  293. /**
  294. * Helper method to get the region code for a given phone number, from a list of possible region
  295. * codes. If the list contains more than one region, the first region for which the number is
  296. * valid is returned.
  297. *
  298. * @param PhoneNumber $number
  299. * @param $regionCodes
  300. * @return String|null Region Code (or null if none are found)
  301. */
  302. protected function getRegionCodeForShortNumberFromRegionList(PhoneNumber $number, $regionCodes)
  303. {
  304. if (count($regionCodes) == 0) {
  305. return null;
  306. }
  307. if (count($regionCodes) == 1) {
  308. return $regionCodes[0];
  309. }
  310. $nationalNumber = $this->getNationalSignificantNumber($number);
  311. foreach ($regionCodes as $regionCode) {
  312. $phoneMetadata = $this->getMetadataForRegion($regionCode);
  313. if ($phoneMetadata !== null
  314. && $this->matchesPossibleNumberAndNationalNumber($nationalNumber, $phoneMetadata->getShortCode())
  315. ) {
  316. // The number is valid for this region.
  317. return $regionCode;
  318. }
  319. }
  320. return null;
  321. }
  322. /**
  323. * Check whether a short number is a possible number. If a country calling code is shared by
  324. * multiple regions, this returns true if it's possible in any of them. This provides a more
  325. * lenient check than {@link #isValidShortNumber}. See {@link
  326. * #IsPossibleShortNumberForRegion(PhoneNumber, String)} for details.
  327. *
  328. * @param $number PhoneNumber the short number to check
  329. * @return boolean whether the number is a possible short number
  330. */
  331. public function isPossibleShortNumber(PhoneNumber $number)
  332. {
  333. $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
  334. $shortNumberLength = strlen($this->getNationalSignificantNumber($number));
  335. foreach ($regionCodes as $region) {
  336. $phoneMetadata = $this->getMetadataForRegion($region);
  337. if ($phoneMetadata === null) {
  338. continue;
  339. }
  340. if (in_array($shortNumberLength, $phoneMetadata->getGeneralDesc()->getPossibleLength())) {
  341. return true;
  342. }
  343. }
  344. return false;
  345. }
  346. /**
  347. * Check whether a short number is a possible number when dialled from a region, given the number
  348. * in the form of a string, and the region where the number is dialled from. This provides a more
  349. * lenient check than {@link #isValidShortNumber}.
  350. *
  351. * @param PhoneNumber $shortNumber The short number to check
  352. * @param string $regionDialingFrom Region dialing From
  353. * @return boolean whether the number is a possible short number
  354. */
  355. public function isPossibleShortNumberForRegion(PhoneNumber $shortNumber, $regionDialingFrom)
  356. {
  357. if (!$this->regionDialingFromMatchesNumber($shortNumber, $regionDialingFrom)) {
  358. return false;
  359. }
  360. $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
  361. if ($phoneMetadata === null) {
  362. return false;
  363. }
  364. $numberLength = strlen($this->getNationalSignificantNumber($shortNumber));
  365. return in_array($numberLength, $phoneMetadata->getGeneralDesc()->getPossibleLength());
  366. }
  367. /**
  368. * Tests whether a short number matches a valid pattern. If a country calling code is shared by
  369. * multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify
  370. * the number is actually in use, which is impossible to tell by just looking at the number
  371. * itself. See {@link #isValidShortNumberForRegion(PhoneNumber, String)} for details.
  372. *
  373. * @param $number PhoneNumber the short number for which we want to test the validity
  374. * @return boolean whether the short number matches a valid pattern
  375. */
  376. public function isValidShortNumber(PhoneNumber $number)
  377. {
  378. $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
  379. $regionCode = $this->getRegionCodeForShortNumberFromRegionList($number, $regionCodes);
  380. if (count($regionCodes) > 1 && $regionCode !== null) {
  381. // If a matching region had been found for the phone number from among two or more regions,
  382. // then we have already implicitly verified its validity for that region.
  383. return true;
  384. }
  385. return $this->isValidShortNumberForRegion($number, $regionCode);
  386. }
  387. /**
  388. * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify
  389. * the number is actually in use, which is impossible to tell by just looking at the number
  390. * itself.
  391. *
  392. * @param PhoneNumber $number The Short number for which we want to test the validity
  393. * @param string $regionDialingFrom the region from which the number is dialed
  394. * @return boolean whether the short number matches a valid pattern
  395. */
  396. public function isValidShortNumberForRegion(PhoneNumber $number, $regionDialingFrom)
  397. {
  398. if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
  399. return false;
  400. }
  401. $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
  402. if ($phoneMetadata === null) {
  403. return false;
  404. }
  405. $shortNumber = $this->getNationalSignificantNumber($number);
  406. $generalDesc = $phoneMetadata->getGeneralDesc();
  407. if (!$this->matchesPossibleNumberAndNationalNumber($shortNumber, $generalDesc)) {
  408. return false;
  409. }
  410. $shortNumberDesc = $phoneMetadata->getShortCode();
  411. return $this->matchesPossibleNumberAndNationalNumber($shortNumber, $shortNumberDesc);
  412. }
  413. /**
  414. * Gets the expected cost category of a short number when dialled from a region (however, nothing is
  415. * implied about its validity). If it is important that the number is valid, then its validity
  416. * must first be checked using {@link isValidShortNumberForRegion}. Note that emergency numbers
  417. * are always considered toll-free.
  418. * Example usage:
  419. * <pre>{@code
  420. * $shortInfo = ShortNumberInfo::getInstance();
  421. * $shortNumber = PhoneNumberUtil::parse("110", "US);
  422. * $regionCode = "FR";
  423. * if ($shortInfo->isValidShortNumberForRegion($shortNumber, $regionCode)) {
  424. * $cost = $shortInfo->getExpectedCostForRegion($shortNumber, $regionCode);
  425. * // Do something with the cost information here.
  426. * }}</pre>
  427. *
  428. * @param PhoneNumber $number the short number for which we want to know the expected cost category,
  429. * as a string
  430. * @param string $regionDialingFrom the region from which the number is dialed
  431. * @return int the expected cost category for that region of the short number. Returns UNKNOWN_COST if
  432. * the number does not match a cost category. Note that an invalid number may match any cost
  433. * category.
  434. */
  435. public function getExpectedCostForRegion(PhoneNumber $number, $regionDialingFrom)
  436. {
  437. if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
  438. return ShortNumberCost::UNKNOWN_COST;
  439. }
  440. // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null.
  441. $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
  442. if ($phoneMetadata === null) {
  443. return ShortNumberCost::UNKNOWN_COST;
  444. }
  445. $shortNumber = $this->getNationalSignificantNumber($number);
  446. // The possible lengths are not present for a particular sub-type if they match the general
  447. // description; for this reason, we check the possible lengths against the general description
  448. // first to allow an early exit if possible.
  449. if (!in_array(strlen($shortNumber), $phoneMetadata->getGeneralDesc()->getPossibleLength())) {
  450. return ShortNumberCost::UNKNOWN_COST;
  451. }
  452. // The cost categories are tested in order of decreasing expense, since if for some reason the
  453. // patterns overlap the most expensive matching cost category should be returned.
  454. if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getPremiumRate())) {
  455. return ShortNumberCost::PREMIUM_RATE;
  456. }
  457. if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getStandardRate())) {
  458. return ShortNumberCost::STANDARD_RATE;
  459. }
  460. if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getTollFree())) {
  461. return ShortNumberCost::TOLL_FREE;
  462. }
  463. if ($this->isEmergencyNumber($shortNumber, $regionDialingFrom)) {
  464. // Emergency numbers are implicitly toll-free.
  465. return ShortNumberCost::TOLL_FREE;
  466. }
  467. return ShortNumberCost::UNKNOWN_COST;
  468. }
  469. /**
  470. * Gets the expected cost category of a short number (however, nothing is implied about its
  471. * validity). If the country calling code is unique to a region, this method behaves exactly the
  472. * same as {@link #getExpectedCostForRegion(PhoneNumber, String)}. However, if the country calling
  473. * code is shared by multiple regions, then it returns the highest cost in the sequence
  474. * PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, TOLL_FREE. The reason for the position of
  475. * UNKNOWN_COST in this order is that if a number is UNKNOWN_COST in one region but STANDARD_RATE
  476. * or TOLL_FREE in another, its expected cost cannot be estimated as one of the latter since it
  477. * might be a PREMIUM_RATE number.
  478. *
  479. * <p>
  480. * For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, the expected
  481. * cost returned by this method will be STANDARD_RATE, since the NANPA countries share the same
  482. * country calling code.
  483. * </p>
  484. *
  485. * Note: If the region from which the number is dialed is known, it is highly preferable to call
  486. * {@link #getExpectedCostForRegion(PhoneNumber, String)} instead.
  487. *
  488. * @param PhoneNumber $number the short number for which we want to know the expected cost category
  489. * @return int the highest expected cost category of the short number in the region(s) with the given
  490. * country calling code
  491. */
  492. public function getExpectedCost(PhoneNumber $number)
  493. {
  494. $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
  495. if (count($regionCodes) == 0) {
  496. return ShortNumberCost::UNKNOWN_COST;
  497. }
  498. if (count($regionCodes) == 1) {
  499. return $this->getExpectedCostForRegion($number, $regionCodes[0]);
  500. }
  501. $cost = ShortNumberCost::TOLL_FREE;
  502. foreach ($regionCodes as $regionCode) {
  503. $costForRegion = $this->getExpectedCostForRegion($number, $regionCode);
  504. switch ($costForRegion) {
  505. case ShortNumberCost::PREMIUM_RATE:
  506. return ShortNumberCost::PREMIUM_RATE;
  507. case ShortNumberCost::UNKNOWN_COST:
  508. $cost = ShortNumberCost::UNKNOWN_COST;
  509. break;
  510. case ShortNumberCost::STANDARD_RATE:
  511. if ($cost != ShortNumberCost::UNKNOWN_COST) {
  512. $cost = ShortNumberCost::STANDARD_RATE;
  513. }
  514. break;
  515. case ShortNumberCost::TOLL_FREE:
  516. // Do nothing
  517. break;
  518. }
  519. }
  520. return $cost;
  521. }
  522. /**
  523. * Returns true if the given number exactly matches an emergency service number in the given
  524. * region.
  525. * <p>
  526. * This method takes into account cases where the number might contain formatting, but doesn't
  527. * allow additional digits to be appended. Note that {@code isEmergencyNumber(number, region)}
  528. * implies {@code connectsToEmergencyNumber(number, region)}.
  529. *
  530. * @param string $number the phone number to test
  531. * @param string $regionCode the region where the phone number is being dialled
  532. * @return boolean whether the number exactly matches an emergency services number in the given region
  533. */
  534. public function isEmergencyNumber($number, $regionCode)
  535. {
  536. return $this->matchesEmergencyNumberHelper($number, $regionCode, false /* doesn't allow prefix match */);
  537. }
  538. /**
  539. * Gets the national significant number of the a phone number. Note a national significant number
  540. * doesn't contain a national prefix or any formatting.
  541. * <p>
  542. * This is a temporary duplicate of the {@code getNationalSignificantNumber} method from
  543. * {@code PhoneNumberUtil}. Ultimately a canonical static version should exist in a separate
  544. * utility class (to prevent {@code ShortNumberInfo} needing to depend on PhoneNumberUtil).
  545. *
  546. * @param PhoneNumber $number the phone number for which the national significant number is needed
  547. * @return string the national significant number of the PhoneNumber object passed in
  548. */
  549. protected function getNationalSignificantNumber(PhoneNumber $number)
  550. {
  551. // If leading zero(s) have been set, we prefix this now. Note this is not a national prefix.
  552. $nationalNumber = '';
  553. if ($number->isItalianLeadingZero()) {
  554. $zeros = str_repeat('0', $number->getNumberOfLeadingZeros());
  555. $nationalNumber .= $zeros;
  556. }
  557. $nationalNumber .= $number->getNationalNumber();
  558. return $nationalNumber;
  559. }
  560. /**
  561. * TODO: Once we have benchmarked ShortnumberInfo, consider if it is worth keeping
  562. * this performance optimization.
  563. * @param string $number
  564. * @param PhoneNumberDesc $numberDesc
  565. * @return bool
  566. */
  567. protected function matchesPossibleNumberAndNationalNumber($number, PhoneNumberDesc $numberDesc)
  568. {
  569. if (count($numberDesc->getPossibleLength()) > 0 && !in_array(strlen($number), $numberDesc->getPossibleLength())) {
  570. return false;
  571. }
  572. return $this->matcherAPI->matchNationalNumber($number, $numberDesc, false);
  573. }
  574. }