Completed
Push — analysis-z4wVAw ( 7f865f )
by Joshua
21:35 queued 05:10
created

getCountryNameForNumber()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
rs 8.6737
ccs 11
cts 11
cp 1
cc 5
eloc 12
nc 5
nop 2
crap 5
1
<?php
2
3
namespace libphonenumber\geocoding;
4
5
use libphonenumber\NumberParseException;
6
use libphonenumber\PhoneNumber;
7
use libphonenumber\PhoneNumberType;
8
use libphonenumber\PhoneNumberUtil;
9
use libphonenumber\prefixmapper\PrefixFileReader;
10
11
class PhoneNumberOfflineGeocoder
12
{
13
    const MAPPING_DATA_DIRECTORY = '/data';
14
    /**
15
     * @var PhoneNumberOfflineGeocoder
16
     */
17
    protected static $instance;
18
    /**
19
     * @var PhoneNumberUtil
20
     */
21
    protected $phoneUtil;
22
    /**
23
     * @var PrefixFileReader
24
     */
25
    protected $prefixFileReader = null;
26
27 245
    protected function __construct($phonePrefixDataDirectory)
28
    {
29 245
        if (!extension_loaded('intl')) {
30
            throw new \RuntimeException('The intl extension must be installed');
31
        }
32
33 245
        $this->phoneUtil = PhoneNumberUtil::getInstance();
34
35 245
        $this->prefixFileReader = new PrefixFileReader(dirname(__FILE__) . $phonePrefixDataDirectory);
36 245
    }
37
38
    /**
39
     * Gets a PhoneNumberOfflineGeocoder instance to carry out international phone number geocoding.
40
     *
41
     * <p>The PhoneNumberOfflineGeocoder is implemented as a singleton. Therefore, calling this method
42
     * multiple times will only result in one instance being created.
43
     *
44
     * @param string $mappingDir (Optional) Mapping Data Directory
45
     * @return PhoneNumberOfflineGeocoder
46
     */
47 250
    public static function getInstance($mappingDir = self::MAPPING_DATA_DIRECTORY)
48
    {
49 250
        if (static::$instance === null) {
50 245
            static::$instance = new static($mappingDir);
51
        }
52
53 250
        return static::$instance;
54
    }
55
56 245
    public static function resetInstance()
57
    {
58 245
        static::$instance = null;
59 245
    }
60
61
    /**
62
     * As per getDescriptionForValidNumber, but explicitly checks the validity of the number
63
     * passed in.
64
     *
65
     *
66
     * @see getDescriptionForValidNumber
67
     * @param PhoneNumber $number a valid phone number for which we want to get a text description
68
     * @param string $locale the language code for which the description should be written
69
     * @param string $userRegion the region code for a given user. This region will be omitted from the
70
     *     description if the phone number comes from this region. It is a two-letter uppercase ISO
71
     *     country code as defined by ISO 3166-1.
72
     * @return string a text description for the given language code for the given phone number, or empty
73
     *     string if the number passed in is invalid
74
     */
75 16
    public function getDescriptionForNumber(PhoneNumber $number, $locale, $userRegion = null)
76
    {
77 16
        $numberType = $this->phoneUtil->getNumberType($number);
78
79 16
        if ($numberType === PhoneNumberType::UNKNOWN) {
80 3
            return "";
81 15
        } elseif (!$this->canBeGeocoded($numberType)) {
82 2
            return $this->getCountryNameForNumber($number, $locale);
83
        }
84
85 14
        return $this->getDescriptionForValidNumber($number, $locale, $userRegion);
86
    }
87
88
    /**
89
     * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
90
     * stricter check, as it determines if a number has a geographical association. Also, if new
91
     * phone number types were added, we should check if this other method should be updated too.
92
     *
93
     * @param int $numberType
94
     * @return boolean
95
     */
96 15
    protected function canBeGeocoded($numberType)
97
    {
98 15
        return ($numberType === PhoneNumberType::FIXED_LINE || $numberType === PhoneNumberType::MOBILE || $numberType === PhoneNumberType::FIXED_LINE_OR_MOBILE);
99
    }
100
101
    /**
102
     * Returns the customary display name in the given language for the given territory the phone
103
     * number is from. If it could be from many territories, nothing is returned.
104
     *
105
     * @param PhoneNumber $number
106
     * @param $locale
107
     * @return string
108
     */
109 8
    protected function getCountryNameForNumber(PhoneNumber $number, $locale)
110
    {
111 8
        $regionCodes = $this->phoneUtil->getRegionCodesForCountryCode($number->getCountryCode());
112
113 8
        if (count($regionCodes) === 1) {
114 4
            return $this->getRegionDisplayName($regionCodes[0], $locale);
115
        } else {
116 5
            $regionWhereNumberIsValid = 'ZZ';
117 5
            foreach ($regionCodes as $regionCode) {
0 ignored issues
show
Bug introduced by
The expression $regionCodes of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
118 5
                if ($this->phoneUtil->isValidNumberForRegion($number, $regionCode)) {
119 5
                    if ($regionWhereNumberIsValid !== 'ZZ') {
120
                        // If we can't assign the phone number as definitely belonging to only one territory,
121
                        // then we return nothing.
122 1
                        return "";
123
                    }
124 5
                    $regionWhereNumberIsValid = $regionCode;
125
                }
126
            }
127
128 4
            return $this->getRegionDisplayName($regionWhereNumberIsValid, $locale);
129
        }
130
    }
131
132
    /**
133
     * Returns the customary display name in the given language for the given region.
134
     *
135
     * @param $regionCode
136
     * @param $locale
137
     * @return string
138
     */
139 241
    protected function getRegionDisplayName($regionCode, $locale)
140
    {
141 241
        if ($regionCode === null || $regionCode == 'ZZ' || $regionCode === PhoneNumberUtil::REGION_CODE_FOR_NON_GEO_ENTITY) {
142 1
            return "";
143
        }
144
145 241
        return Locale::getDisplayRegion(
146 241
            Locale::countryCodeToLocale($regionCode),
147
            $locale
148
        );
149
    }
150
151
    /**
152
     * Returns a text description for the given phone number, in the language provided. The
153
     * description might consist of the name of the country where the phone number is from, or the
154
     * name of the geographical area the phone number is from if more detailed information is
155
     * available.
156
     *
157
     * <p>This method assumes the validity of the number passed in has already been checked, and that
158
     * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
159
     * candidates for geocoding.
160
     *
161
     * <p>If $userRegion is set, we also consider the region of the user. If the phone number is from
162
     * the same region as the user, only a lower-level description will be returned, if one exists.
163
     * Otherwise, the phone number's region will be returned, with optionally some more detailed
164
     * information.
165
     *
166
     * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
167
     * CA" for a particular number, omitting the United States from the description. For a user from
168
     * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
169
     * States" or even just "United States".
170
     *
171
     * @param PhoneNumber $number a valid phone number for which we want to get a text description
172
     * @param string $locale the language code for which the description should be written
173
     * @param string $userRegion the region code for a given user. This region will be omitted from the
174
     *     description if the phone number comes from this region. It is a two-letter uppercase ISO
175
     *     country code as defined by ISO 3166-1.
176
     * @return string a text description for the given language code for the given phone number
177
     */
178 247
    public function getDescriptionForValidNumber(PhoneNumber $number, $locale, $userRegion = null)
179
    {
180
        // If the user region matches the number's region, then we just show the lower-level
181
        // description, if one exists - if no description exists, we will show the region(country) name
182
        // for the number.
183 247
        $regionCode = $this->phoneUtil->getRegionCodeForNumber($number);
184 247
        if ($userRegion == null || $userRegion == $regionCode) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $userRegion of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
185 14
            $languageStr = Locale::getPrimaryLanguage($locale);
186 14
            $scriptStr = "";
187 14
            $regionStr = Locale::getRegion($locale);
188
189 14
            $mobileToken = $this->phoneUtil->getCountryMobileToken($number->getCountryCode());
190 14
            $nationalNumber = $this->phoneUtil->getNationalSignificantNumber($number);
191 14
            if ($mobileToken !== "" && (!strncmp($nationalNumber, $mobileToken, strlen($mobileToken)))) {
192
                // In some countries, eg. Argentina, mobile numbers have a mobile token before the national
193
                // destination code, this should be removed before geocoding.
194 1
                $nationalNumber = substr($nationalNumber, strlen($mobileToken));
195 1
                $region = $this->phoneUtil->getRegionCodeForCountryCode($number->getCountryCode());
196
                try {
197 1
                    $copiedNumber = $this->phoneUtil->parse($nationalNumber, $region);
198
                } catch (NumberParseException $e) {
199
                    // If this happens, just reuse what we had.
200
                    $copiedNumber = $number;
201
                }
202 1
                $areaDescription = $this->prefixFileReader->getDescriptionForNumber($copiedNumber, $languageStr, $scriptStr, $regionStr);
203
            } else {
204 13
                $areaDescription = $this->prefixFileReader->getDescriptionForNumber($number, $languageStr, $scriptStr, $regionStr);
205
            }
206
207 14
            return (strlen($areaDescription) > 0) ? $areaDescription : $this->getCountryNameForNumber($number, $locale);
208
        }
209
        // Otherwise, we just show the region(country) name for now.
210 234
        return $this->getRegionDisplayName($regionCode, $locale);
211
        // TODO: Concatenate the lower-level and country-name information in an appropriate
212
        // way for each language.
213
    }
214
}
215