ShortNumberInfo   F
last analyzed

Complexity

Total Complexity 85

Size/Duplication

Total Lines 638
Duplicated Lines 0 %

Test Coverage

Coverage 92.23%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 196
dl 0
loc 638
ccs 190
cts 206
cp 0.9223
rs 2
c 2
b 0
f 0
wmc 85

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getSupportedRegions() 0 3 1
A resetInstance() 0 3 1
A connectsToEmergencyNumber() 0 3 1
A getMetadataForRegion() 0 13 4
A getExampleShortNumber() 0 13 4
A getRegionCodesForCountryCode() 0 9 3
B getExampleShortNumberForCost() 0 29 7
A getInstance() 0 7 2
A regionDialingFromMatchesNumber() 0 5 1
A __construct() 0 11 1
A loadMetadataFromFile() 0 17 4
A isPossibleShortNumberForRegion() 0 14 3
B getExpectedCost() 0 31 9
A isValidShortNumber() 0 11 3
A isEmergencyNumber() 0 3 1
A isSmsServiceForRegion() 0 12 3
A isPossibleShortNumber() 0 18 4
A isValidShortNumberForRegion() 0 22 4
A matchesEmergencyNumberHelper() 0 25 5
A isCarrierSpecificForRegion() 0 11 3
B getExpectedCostForRegion() 0 40 8
A isCarrierSpecific() 0 10 2
A matchesPossibleNumberAndNationalNumber() 0 7 3
A getNationalSignificantNumber() 0 12 2
A getRegionCodeForShortNumberFromRegionList() 0 22 6

How to fix   Complexity   

Complex Class

Complex classes like ShortNumberInfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ShortNumberInfo, and based on these observations, apply Extract Interface, too.

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
12
namespace libphonenumber;
13
14
class ShortNumberInfo
15
{
16
    const META_DATA_FILE_PREFIX = 'ShortNumberMetadata';
17
    /**
18
     * @var ShortNumberInfo
19
     */
20
    protected static $instance;
21
    /**
22
     * @var MatcherAPIInterface
23
     */
24
    protected $matcherAPI;
25
    protected $currentFilePrefix;
26
    protected $regionToMetadataMap = array();
27
    protected $countryCallingCodeToRegionCodeMap = array();
28
    protected $countryCodeToNonGeographicalMetadataMap = array();
29
    protected static $regionsWhereEmergencyNumbersMustBeExact = array(
30
        'BR',
31
        'CL',
32
        'NI',
33
    );
34
35 29
    protected function __construct(MatcherAPIInterface $matcherAPI)
36
    {
37 29
        $this->matcherAPI = $matcherAPI;
38
39
        // TODO: Create ShortNumberInfo for a given map
40 29
        $this->countryCallingCodeToRegionCodeMap = CountryCodeToRegionCodeMap::$countryCodeToRegionCodeMap;
41
42 29
        $this->currentFilePrefix = dirname(__FILE__) . '/data/' . static::META_DATA_FILE_PREFIX;
43
44
        // Initialise PhoneNumberUtil to make sure regex's are setup correctly
45 29
        PhoneNumberUtil::getInstance();
46 29
    }
47
48
    /**
49
     * Returns the singleton instance of ShortNumberInfo
50
     *
51
     * @return \libphonenumber\ShortNumberInfo
52
     */
53 5652
    public static function getInstance()
54
    {
55 5652
        if (null === static::$instance) {
56 29
            static::$instance = new self(RegexBasedMatcher::create());
57
        }
58
59 5652
        return static::$instance;
60
    }
61
62 28
    public static function resetInstance()
63
    {
64 28
        static::$instance = null;
65 28
    }
66
67
    /**
68
     * Returns a list with teh region codes that match the specific country calling code. For
69
     * non-geographical country calling codes, the region code 001 is returned. Also, in the case
70
     * of no region code being found, an empty list is returned.
71
     *
72
     * @param int $countryCallingCode
73
     * @return array
74
     */
75 949
    protected function getRegionCodesForCountryCode($countryCallingCode)
76
    {
77 949
        if (!array_key_exists($countryCallingCode, $this->countryCallingCodeToRegionCodeMap)) {
78 1
            $regionCodes = null;
79
        } else {
80 949
            $regionCodes = $this->countryCallingCodeToRegionCodeMap[$countryCallingCode];
81
        }
82
83 949
        return ($regionCodes === null) ? array() : $regionCodes;
84
    }
85
86
    /**
87
     * Helper method to check that the country calling code of the number matches the region it's
88
     * being dialed from.
89
     * @param PhoneNumber $number
90
     * @param string $regionDialingFrom
91
     * @return bool
92
     */
93 948
    protected function regionDialingFromMatchesNumber(PhoneNumber $number, $regionDialingFrom)
94
    {
95 948
        $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
96
97 948
        return in_array($regionDialingFrom, $regionCodes);
98
    }
99
100
    public function getSupportedRegions()
101
    {
102
        return ShortNumbersRegionCodeSet::$shortNumbersRegionCodeSet;
103
    }
104
105
    /**
106
     * Gets a valid short number for the specified region.
107
     *
108
     * @param $regionCode String the region for which an example short number is needed
109
     * @return string a valid short number for the specified region. Returns an empty string when the
110
     *      metadata does not contain such information.
111
     */
112 242
    public function getExampleShortNumber($regionCode)
113
    {
114 242
        $phoneMetadata = $this->getMetadataForRegion($regionCode);
115 242
        if ($phoneMetadata === null) {
116 1
            return '';
117
        }
118
119
        /** @var PhoneNumberDesc $desc */
120 242
        $desc = $phoneMetadata->getShortCode();
121 242
        if ($desc !== null && $desc->hasExampleNumber()) {
122 242
            return $desc->getExampleNumber();
123
        }
124
        return '';
125
    }
126
127
    /**
128
     * @param $regionCode
129
     * @return PhoneMetadata|null
130
     */
131 2199
    public function getMetadataForRegion($regionCode)
132
    {
133 2199
        if (!in_array($regionCode, ShortNumbersRegionCodeSet::$shortNumbersRegionCodeSet)) {
134 1
            return null;
135
        }
136
137 2199
        if (!isset($this->regionToMetadataMap[$regionCode])) {
138
            // The regionCode here will be valid and won't be '001', so we don't need to worry about
139
            // what to pass in for the country calling code.
140 262
            $this->loadMetadataFromFile($this->currentFilePrefix, $regionCode, 0);
141
        }
142
143 2199
        return isset($this->regionToMetadataMap[$regionCode]) ? $this->regionToMetadataMap[$regionCode] : null;
144
    }
145
146 262
    protected function loadMetadataFromFile($filePrefix, $regionCode, $countryCallingCode)
147
    {
148 262
        $isNonGeoRegion = PhoneNumberUtil::REGION_CODE_FOR_NON_GEO_ENTITY === $regionCode;
149 262
        $fileName = $filePrefix . '_' . ($isNonGeoRegion ? $countryCallingCode : $regionCode) . '.php';
150 262
        if (!is_readable($fileName)) {
151
            throw new \Exception('missing metadata: ' . $fileName);
152
        }
153
154 262
        $metadataLoader = new DefaultMetadataLoader();
155 262
        $data = $metadataLoader->loadMetadata($fileName);
156
157 262
        $metadata = new PhoneMetadata();
158 262
        $metadata->fromArray($data);
159 262
        if ($isNonGeoRegion) {
160
            $this->countryCodeToNonGeographicalMetadataMap[$countryCallingCode] = $metadata;
161
        } else {
162 262
            $this->regionToMetadataMap[$regionCode] = $metadata;
163
        }
164 262
    }
165
166
    /**
167
     *  Gets a valid short number for the specified cost category.
168
     *
169
     * @param string $regionCode the region for which an example short number is needed
170
     * @param int $cost the cost category of number that is needed
171
     * @return string a valid short number for the specified region and cost category. Returns an empty string
172
     * when the metadata does not contain such information, or the cost is UNKNOWN_COST.
173
     */
174 965
    public function getExampleShortNumberForCost($regionCode, $cost)
175
    {
176 965
        $phoneMetadata = $this->getMetadataForRegion($regionCode);
177 965
        if ($phoneMetadata === null) {
178
            return '';
179
        }
180
181
        /** @var PhoneNumberDesc $desc */
182 965
        $desc = null;
183
        switch ($cost) {
184 965
            case ShortNumberCost::TOLL_FREE:
185 242
                $desc = $phoneMetadata->getTollFree();
186 242
                break;
187 724
            case ShortNumberCost::STANDARD_RATE:
188 242
                $desc = $phoneMetadata->getStandardRate();
189 242
                break;
190 483
            case ShortNumberCost::PREMIUM_RATE:
191 242
                $desc = $phoneMetadata->getPremiumRate();
192 242
                break;
193
            default:
194
                // UNKNOWN_COST numbers are computed by the process of elimination from the other cost categories
195 241
                break;
196
        }
197
198 965
        if ($desc !== null && $desc->hasExampleNumber()) {
199 293
            return $desc->getExampleNumber();
200
        }
201
202 672
        return '';
203
    }
204
205
    /**
206
     * Returns true if the given number, exactly as dialed, might be used to connect to an emergency
207
     * service in the given region.
208
     * <p>
209
     * This method accepts a string, rather than a PhoneNumber, because it needs to distinguish
210
     * cases such as "+1 911" and "911", where the former may not connect to an emergency service in
211
     * all cases but the latter would. This method takes into account cases where the number might
212
     * contain formatting, or might have additional digits appended (when it is okay to do that in
213
     * the specified region).
214
     *
215
     * @param string $number the phone number to test
216
     * @param string $regionCode the region where the phone number if being dialled
217
     * @return boolean whether the number might be used to connect to an emergency service in the given region
218
     */
219 10
    public function connectsToEmergencyNumber($number, $regionCode)
220
    {
221 10
        return $this->matchesEmergencyNumberHelper($number, $regionCode, true /* allows prefix match */);
222
    }
223
224
    /**
225
     * @param string $number
226
     * @param string $regionCode
227
     * @param bool $allowPrefixMatch
228
     * @return bool
229
     */
230 265
    protected function matchesEmergencyNumberHelper($number, $regionCode, $allowPrefixMatch)
231
    {
232 265
        $number = PhoneNumberUtil::extractPossibleNumber($number);
0 ignored issues
show
Bug introduced by
$number of type string is incompatible with the type integer expected by parameter $number of libphonenumber\PhoneNumb...extractPossibleNumber(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

232
        $number = PhoneNumberUtil::extractPossibleNumber(/** @scrutinizer ignore-type */ $number);
Loading history...
233 265
        $matcher = new Matcher(PhoneNumberUtil::$PLUS_CHARS_PATTERN, $number);
234 265
        if ($matcher->lookingAt()) {
235
            // Returns false if the number starts with a plus sign. We don't believe dialing the country
236
            // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can
237
            // add additional logic here to handle it.
238 2
            return false;
239
        }
240
241 263
        $metadata = $this->getMetadataForRegion($regionCode);
242 263
        if ($metadata === null || !$metadata->hasEmergency()) {
243
            return false;
244
        }
245
246 263
        $normalizedNumber = PhoneNumberUtil::normalizeDigitsOnly($number);
247 263
        $emergencyDesc = $metadata->getEmergency();
248
249
        $allowPrefixMatchForRegion = (
250 263
            $allowPrefixMatch
251 263
            && !in_array($regionCode, static::$regionsWhereEmergencyNumbersMustBeExact)
252
        );
253
254 263
        return $this->matcherAPI->matchNationalNumber($normalizedNumber, $emergencyDesc, $allowPrefixMatchForRegion);
255
    }
256
257
    /**
258
     * Given a valid short number, determines whether it is carrier-specific (however, nothing is
259
     * implied about its validity). Carrier-specific numbers may connect to a different end-point, or
260
     * not connect at all, depending on the user's carrier. If it is important that the number is
261
     * valid, then its validity must first be checked using {@link #isValidShortNumber} or
262
     * {@link #isValidShortNumberForRegion}.
263
     *
264
     * @param PhoneNumber $number the valid short number to check
265
     * @return boolean whether the short number is carrier-specific, assuming the input was a valid short
266
     *     number
267
     */
268 1
    public function isCarrierSpecific(PhoneNumber $number)
269
    {
270 1
        $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
271 1
        $regionCode = $this->getRegionCodeForShortNumberFromRegionList($number, $regionCodes);
272 1
        $nationalNumber = $this->getNationalSignificantNumber($number);
273 1
        $phoneMetadata = $this->getMetadataForRegion($regionCode);
274
275 1
        return ($phoneMetadata !== null) && $this->matchesPossibleNumberAndNationalNumber(
276 1
            $nationalNumber,
277 1
            $phoneMetadata->getCarrierSpecific()
278
        );
279
    }
280
281
    /**
282
     * Given a valid short number, determines whether it is carrier-specific when dialed from the
283
     * given region (however, nothing is implied about its validity). Carrier-specific numbers may
284
     * connect to a different end-point, or not connect at all, depending on the user's carrier. If
285
     * it is important that the number is valid, then its validity must first be checked using
286
     * {@link #isValidShortNumber} or {@link #isValidShortNumberForRegion}. Returns false if the
287
     * number doesn't match the region provided.
288
     * @param PhoneNumber $number The valid short number to check
289
     * @param string $regionDialingFrom The region from which the number is dialed
290
     * @return bool Whether the short number is carrier-specific in the provided region, assuming the
291
     *      input was a valid short number
292
     */
293 84
    public function isCarrierSpecificForRegion(PhoneNumber $number, $regionDialingFrom)
294
    {
295 84
        if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
296
            return false;
297
        }
298
299 84
        $nationalNumber = $this->getNationalSignificantNumber($number);
300 84
        $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
301
302 84
        return ($phoneMetadata !== null)
303 84
            && $this->matchesPossibleNumberAndNationalNumber($nationalNumber, $phoneMetadata->getCarrierSpecific());
304
    }
305
306
    /**
307
     * Given a valid short number, determines whether it is an SMS service (however, nothing is
308
     * implied about its validity). An SMS service is where the primary or only intended usage is to
309
     * receive and/or send text messages (SMSs). This includes MMS as MMS numbers downgrade to SMS if
310
     * the other party isn't MMS-capable. If it is important that the number is valid, then its
311
     * validity must first be checked using {@link #isValidShortNumber} or {@link
312
     * #isValidShortNumberForRegion}. Returns false if the number doesn't match the region provided.
313
     *
314
     * @param PhoneNumber $number The valid short number to check
315
     * @param string $regionDialingFrom The region from which the number is dialed
316
     * @return bool Whether the short number is an SMS service in the provided region, assuming the input
317
     *  was a valid short number.
318
     */
319 83
    public function isSmsServiceForRegion(PhoneNumber $number, $regionDialingFrom)
320
    {
321 83
        if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
322
            return false;
323
        }
324
325 83
        $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
326
327 83
        return ($phoneMetadata !== null)
328 83
            && $this->matchesPossibleNumberAndNationalNumber(
329 83
                $this->getNationalSignificantNumber($number),
330 83
                $phoneMetadata->getSmsServices()
331
            );
332
    }
333
334
    /**
335
     * Helper method to get the region code for a given phone number, from a list of possible region
336
     * codes. If the list contains more than one region, the first region for which the number is
337
     * valid is returned.
338
     *
339
     * @param PhoneNumber $number
340
     * @param $regionCodes
341
     * @return String|null Region Code (or null if none are found)
342
     */
343 246
    protected function getRegionCodeForShortNumberFromRegionList(PhoneNumber $number, $regionCodes)
344
    {
345 246
        if (count($regionCodes) == 0) {
346
            return null;
347
        }
348
349 246
        if (count($regionCodes) == 1) {
350 193
            return $regionCodes[0];
351
        }
352
353 54
        $nationalNumber = $this->getNationalSignificantNumber($number);
354
355 54
        foreach ($regionCodes as $regionCode) {
356 54
            $phoneMetadata = $this->getMetadataForRegion($regionCode);
357 54
            if ($phoneMetadata !== null
358 54
                && $this->matchesPossibleNumberAndNationalNumber($nationalNumber, $phoneMetadata->getShortCode())
359
            ) {
360
                // The number is valid for this region.
361 54
                return $regionCode;
362
            }
363
        }
364
        return null;
365
    }
366
367
    /**
368
     * Check whether a short number is a possible number. If a country calling code is shared by
369
     * multiple regions, this returns true if it's possible in any of them. This provides a more
370
     * lenient check than {@link #isValidShortNumber}. See {@link
371
     * #IsPossibleShortNumberForRegion(PhoneNumber, String)} for details.
372
     *
373
     * @param $number PhoneNumber the short number to check
374
     * @return boolean whether the number is a possible short number
375
     */
376 2
    public function isPossibleShortNumber(PhoneNumber $number)
377
    {
378 2
        $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
379 2
        $shortNumberLength = strlen($this->getNationalSignificantNumber($number));
380
381 2
        foreach ($regionCodes as $region) {
382 2
            $phoneMetadata = $this->getMetadataForRegion($region);
383
384 2
            if ($phoneMetadata === null) {
385
                continue;
386
            }
387
388 2
            if (in_array($shortNumberLength, $phoneMetadata->getGeneralDesc()->getPossibleLength())) {
389 1
                return true;
390
            }
391
        }
392
393 2
        return false;
394
    }
395
396
    /**
397
     * Check whether a short number is a possible number when dialled from a region, given the number
398
     * in the form of a string, and the region where the number is dialled from. This provides a more
399
     * lenient check than {@link #isValidShortNumber}.
400
     *
401
     * @param PhoneNumber $shortNumber The short number to check
402
     * @param string $regionDialingFrom Region dialing From
403
     * @return boolean whether the number is a possible short number
404
     */
405 408
    public function isPossibleShortNumberForRegion(PhoneNumber $shortNumber, $regionDialingFrom)
406
    {
407 408
        if (!$this->regionDialingFromMatchesNumber($shortNumber, $regionDialingFrom)) {
408 1
            return false;
409
        }
410
411 407
        $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
412
413 407
        if ($phoneMetadata === null) {
414
            return false;
415
        }
416
417 407
        $numberLength = strlen($this->getNationalSignificantNumber($shortNumber));
418 407
        return in_array($numberLength, $phoneMetadata->getGeneralDesc()->getPossibleLength());
419
    }
420
421
    /**
422
     * Tests whether a short number matches a valid pattern. If a country calling code is shared by
423
     * multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify
424
     * the number is actually in use, which is impossible to tell by just looking at the number
425
     * itself. See {@link #isValidShortNumberForRegion(PhoneNumber, String)} for details.
426
     *
427
     * @param $number PhoneNumber the short number for which we want to test the validity
428
     * @return boolean whether the short number matches a valid pattern
429
     */
430 245
    public function isValidShortNumber(PhoneNumber $number)
431
    {
432 245
        $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
433 245
        $regionCode = $this->getRegionCodeForShortNumberFromRegionList($number, $regionCodes);
434 245
        if (count($regionCodes) > 1 && $regionCode !== null) {
435
            // If a matching region had been found for the phone number from among two or more regions,
436
            // then we have already implicitly verified its validity for that region.
437 53
            return true;
438
        }
439
440 193
        return $this->isValidShortNumberForRegion($number, $regionCode);
441
    }
442
443
    /**
444
     * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify
445
     * the number is actually in use, which is impossible to tell by just looking at the number
446
     * itself.
447
     *
448
     * @param PhoneNumber $number The Short number for which we want to test the validity
449
     * @param string $regionDialingFrom the region from which the number is dialed
450
     * @return boolean whether the short number matches a valid pattern
451
     */
452 246
    public function isValidShortNumberForRegion(PhoneNumber $number, $regionDialingFrom)
453
    {
454 246
        if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
455 1
            return false;
456
        }
457 245
        $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
458
459 245
        if ($phoneMetadata === null) {
460
            return false;
461
        }
462
463 245
        $shortNumber = $this->getNationalSignificantNumber($number);
464
465 245
        $generalDesc = $phoneMetadata->getGeneralDesc();
466
467 245
        if (!$this->matchesPossibleNumberAndNationalNumber($shortNumber, $generalDesc)) {
468 1
            return false;
469
        }
470
471 245
        $shortNumberDesc = $phoneMetadata->getShortCode();
472
473 245
        return $this->matchesPossibleNumberAndNationalNumber($shortNumber, $shortNumberDesc);
474
    }
475
476
    /**
477
     * Gets the expected cost category of a short number  when dialled from a region (however, nothing is
478
     * implied about its validity). If it is important that the number is valid, then its validity
479
     * must first be checked using {@link isValidShortNumberForRegion}. Note that emergency numbers
480
     * are always considered toll-free.
481
     * Example usage:
482
     * <pre>{@code
483
     * $shortInfo = ShortNumberInfo::getInstance();
484
     * $shortNumber = PhoneNumberUtil::parse("110", "US);
485
     * $regionCode = "FR";
486
     * if ($shortInfo->isValidShortNumberForRegion($shortNumber, $regionCode)) {
487
     *     $cost = $shortInfo->getExpectedCostForRegion($shortNumber, $regionCode);
488
     *    // Do something with the cost information here.
489
     * }}</pre>
490
     *
491
     * @param PhoneNumber $number the short number for which we want to know the expected cost category,
492
     *     as a string
493
     * @param string $regionDialingFrom the region from which the number is dialed
494
     * @return int the expected cost category for that region of the short number. Returns UNKNOWN_COST if
495
     *     the number does not match a cost category. Note that an invalid number may match any cost
496
     *     category.
497
     */
498 537
    public function getExpectedCostForRegion(PhoneNumber $number, $regionDialingFrom)
499
    {
500 537
        if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) {
501 2
            return ShortNumberCost::UNKNOWN_COST;
502
        }
503
        // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null.
504 536
        $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom);
505 536
        if ($phoneMetadata === null) {
506
            return ShortNumberCost::UNKNOWN_COST;
507
        }
508
509 536
        $shortNumber = $this->getNationalSignificantNumber($number);
510
511
        // The possible lengths are not present for a particular sub-type if they match the general
512
        // description; for this reason, we check the possible lengths against the general description
513
        // first to allow an early exit if possible.
514 536
        if (!in_array(strlen($shortNumber), $phoneMetadata->getGeneralDesc()->getPossibleLength())) {
515 1
            return ShortNumberCost::UNKNOWN_COST;
516
        }
517
518
        // The cost categories are tested in order of decreasing expense, since if for some reason the
519
        // patterns overlap the most expensive matching cost category should be returned.
520 536
        if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getPremiumRate())) {
521 29
            return ShortNumberCost::PREMIUM_RATE;
522
        }
523
524 509
        if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getStandardRate())) {
525 27
            return ShortNumberCost::STANDARD_RATE;
526
        }
527
528 484
        if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getTollFree())) {
529 484
            return ShortNumberCost::TOLL_FREE;
530
        }
531
532 3
        if ($this->isEmergencyNumber($shortNumber, $regionDialingFrom)) {
533
            // Emergency numbers are implicitly toll-free.
534
            return ShortNumberCost::TOLL_FREE;
535
        }
536
537 3
        return ShortNumberCost::UNKNOWN_COST;
538
    }
539
540
    /**
541
     * Gets the expected cost category of a short number (however, nothing is implied about its
542
     * validity). If the country calling code is unique to a region, this method behaves exactly the
543
     * same as {@link #getExpectedCostForRegion(PhoneNumber, String)}. However, if the country calling
544
     * code is shared by multiple regions, then it returns the highest cost in the sequence
545
     * PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, TOLL_FREE. The reason for the position of
546
     * UNKNOWN_COST in this order is that if a number is UNKNOWN_COST in one region but STANDARD_RATE
547
     * or TOLL_FREE in another, its expected cost cannot be estimated as one of the latter since it
548
     * might be a PREMIUM_RATE number.
549
     *
550
     * <p>
551
     * For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, the expected
552
     * cost returned by this method will be STANDARD_RATE, since the NANPA countries share the same
553
     * country calling code.
554
     * </p>
555
     *
556
     * Note: If the region from which the number is dialed is known, it is highly preferable to call
557
     * {@link #getExpectedCostForRegion(PhoneNumber, String)} instead.
558
     *
559
     * @param PhoneNumber $number the short number for which we want to know the expected cost category
560
     * @return int the highest expected cost category of the short number in the region(s) with the given
561
     *     country calling code
562
     */
563 3
    public function getExpectedCost(PhoneNumber $number)
564
    {
565 3
        $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode());
566 3
        if (count($regionCodes) == 0) {
567 1
            return ShortNumberCost::UNKNOWN_COST;
568
        }
569 3
        if (count($regionCodes) == 1) {
570 1
            return $this->getExpectedCostForRegion($number, $regionCodes[0]);
571
        }
572 2
        $cost = ShortNumberCost::TOLL_FREE;
573 2
        foreach ($regionCodes as $regionCode) {
574 2
            $costForRegion = $this->getExpectedCostForRegion($number, $regionCode);
575
            switch ($costForRegion) {
576 2
                case ShortNumberCost::PREMIUM_RATE:
577 1
                    return ShortNumberCost::PREMIUM_RATE;
578
579 2
                case ShortNumberCost::UNKNOWN_COST:
580 1
                    $cost = ShortNumberCost::UNKNOWN_COST;
581 1
                    break;
582
583 2
                case ShortNumberCost::STANDARD_RATE:
584 1
                    if ($cost != ShortNumberCost::UNKNOWN_COST) {
585 1
                        $cost = ShortNumberCost::STANDARD_RATE;
586
                    }
587 1
                    break;
588 2
                case ShortNumberCost::TOLL_FREE:
589
                    // Do nothing
590 2
                    break;
591
            }
592
        }
593 2
        return $cost;
594
    }
595
596
    /**
597
     * Returns true if the given number exactly matches an emergency service number in the given
598
     * region.
599
     * <p>
600
     * This method takes into account cases where the number might contain formatting, but doesn't
601
     * allow additional digits to be appended. Note that {@code isEmergencyNumber(number, region)}
602
     * implies {@code connectsToEmergencyNumber(number, region)}.
603
     *
604
     * @param string $number the phone number to test
605
     * @param string $regionCode the region where the phone number is being dialled
606
     * @return boolean whether the number exactly matches an emergency services number in the given region
607
     */
608 255
    public function isEmergencyNumber($number, $regionCode)
609
    {
610 255
        return $this->matchesEmergencyNumberHelper($number, $regionCode, false /* doesn't allow prefix match */);
611
    }
612
613
    /**
614
     * Gets the national significant number of the a phone number. Note a national significant number
615
     * doesn't contain a national prefix or any formatting.
616
     * <p>
617
     * This is a temporary duplicate of the {@code getNationalSignificantNumber} method from
618
     * {@code PhoneNumberUtil}. Ultimately a canonical static version should exist in a separate
619
     * utility class (to prevent {@code ShortNumberInfo} needing to depend on PhoneNumberUtil).
620
     *
621
     * @param PhoneNumber $number the phone number for which the national significant number is needed
622
     * @return string the national significant number of the PhoneNumber object passed in
623
     */
624 948
    protected function getNationalSignificantNumber(PhoneNumber $number)
625
    {
626
        // If leading zero(s) have been set, we prefix this now. Note this is not a national prefix.
627 948
        $nationalNumber = '';
628 948
        if ($number->isItalianLeadingZero()) {
629 48
            $zeros = str_repeat('0', $number->getNumberOfLeadingZeros());
630 48
            $nationalNumber .= $zeros;
631
        }
632
633 948
        $nationalNumber .= $number->getNationalNumber();
634
635 948
        return $nationalNumber;
636
    }
637
638
    /**
639
     * TODO: Once we have benchmarked ShortnumberInfo, consider if it is worth keeping
640
     * this performance optimization.
641
     * @param string $number
642
     * @param PhoneNumberDesc $numberDesc
643
     * @return bool
644
     */
645 945
    protected function matchesPossibleNumberAndNationalNumber($number, PhoneNumberDesc $numberDesc)
646
    {
647 945
        if (count($numberDesc->getPossibleLength()) > 0 && !in_array(strlen($number), $numberDesc->getPossibleLength())) {
648 493
            return false;
649
        }
650
651 945
        return $this->matcherAPI->matchNationalNumber($number, $numberDesc, false);
652
    }
653
}
654