Completed
Pull Request — master (#94)
by Joshua
82:48 queued 60:07
created

getDescriptionForValidNumber()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0671

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 36
ccs 16
cts 18
cp 0.8889
rs 6.7272
cc 7
eloc 20
nc 7
nop 3
crap 7.0671
1
<?php
2
3
namespace libphonenumber\geocoding;
4
5
6
use libphonenumber\NumberParseException;
7
use libphonenumber\PhoneNumber;
8
use libphonenumber\PhoneNumberType;
9
use libphonenumber\PhoneNumberUtil;
10
use libphonenumber\prefixmapper\PrefixFileReader;
11
12
class PhoneNumberOfflineGeocoder
13
{
14
    const MAPPING_DATA_DIRECTORY = '/data';
15
    /**
16
     * @var PhoneNumberOfflineGeocoder
17
     */
18
    private static $instance;
19
    /**
20
     * @var PhoneNumberUtil
21
     */
22
    private $phoneUtil;
23
    /**
24
     * @var PrefixFileReader
25
     */
26
    private $prefixFileReader = null;
27
28 244
    private function __construct($phonePrefixDataDirectory)
29
    {
30 244
        if(!extension_loaded('intl')) {
31
            throw new \RuntimeException('The intl extension must be installed');
32
        }
33
34 244
        $this->phoneUtil = PhoneNumberUtil::getInstance();
35
36 244
        $this->prefixFileReader = new PrefixFileReader(dirname(__FILE__) . $phonePrefixDataDirectory);
37 244
    }
38
39
    /**
40
     * Gets a PhoneNumberOfflineGeocoder instance to carry out international phone number geocoding.
41
     *
42
     * <p>The PhoneNumberOfflineGeocoder is implemented as a singleton. Therefore, calling this method
43
     * multiple times will only result in one instance being created.
44
     *
45
     * @param string $mappingDir (Optional) Mapping Data Directory
46
     * @return PhoneNumberOfflineGeocoder
47
     */
48 249
    public static function getInstance($mappingDir = self::MAPPING_DATA_DIRECTORY)
49
    {
50 249
        if (self::$instance === null) {
51 244
            self::$instance = new self($mappingDir);
52
        }
53
54 249
        return self::$instance;
55
    }
56
57 244
    public static function resetInstance()
58
    {
59 244
        self::$instance = null;
60 244
    }
61
62
    /**
63
     * As per getDescriptionForValidNumber, but explicitly checks the validity of the number
64
     * passed in.
65
     *
66
     *
67
     * @see getDescriptionForValidNumber
68
     * @param PhoneNumber $number a valid phone number for which we want to get a text description
69
     * @param string $locale the language code for which the description should be written
70
     * @param string $userRegion the region code for a given user. This region will be omitted from the
71
     *     description if the phone number comes from this region. It is a two-letter uppercase ISO
72
     *     country code as defined by ISO 3166-1.
73
     * @return string a text description for the given language code for the given phone number, or empty
74
     *     string if the number passed in is invalid
75
     */
76 15
    public function getDescriptionForNumber(PhoneNumber $number, $locale, $userRegion = null)
77
    {
78 15
        $numberType = $this->phoneUtil->getNumberType($number);
79
80 15
        if ($numberType === PhoneNumberType::UNKNOWN) {
81 3
            return "";
82 14
        } elseif (!$this->canBeGeocoded($numberType)) {
83 2
            return $this->getCountryNameForNumber($number, $locale);
84
        }
85
86 13
        return $this->getDescriptionForValidNumber($number, $locale, $userRegion);
87
    }
88
89
    /**
90
     * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
91
     * stricter check, as it determines if a number has a geographical association. Also, if new
92
     * phone number types were added, we should check if this other method should be updated too.
93
     *
94
     * @param int $numberType
95
     * @return boolean
96
     */
97 14
    private function canBeGeocoded($numberType)
98
    {
99 14
        return ($numberType === PhoneNumberType::FIXED_LINE || $numberType === PhoneNumberType::MOBILE || $numberType === PhoneNumberType::FIXED_LINE_OR_MOBILE);
100
    }
101
102
    /**
103
     * Returns the customary display name in the given language for the given territory the phone
104
     * number is from. If it could be from many territories, nothing is returned.
105
     *
106
     * @param PhoneNumber $number
107
     * @param $locale
108
     * @return string
109
     */
110 8
    private function getCountryNameForNumber(PhoneNumber $number, $locale)
111
    {
112 8
        $regionCodes = $this->phoneUtil->getRegionCodesForCountryCode($number->getCountryCode());
113
114 8
        if (count($regionCodes) === 1) {
115 4
            return $this->getRegionDisplayName($regionCodes[0], $locale);
116
        } else {
117 5
            $regionWhereNumberIsValid = 'ZZ';
118 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...
119 5
                if ($this->phoneUtil->isValidNumberForRegion($number, $regionCode)) {
120 5
                    if ($regionWhereNumberIsValid !== 'ZZ') {
121
                        // If we can't assign the phone number as definitely belonging to only one territory,
122
                        // then we return nothing.
123 1
                        return "";
124
                    }
125 5
                    $regionWhereNumberIsValid = $regionCode;
126
                }
127
            }
128
129 4
            return $this->getRegionDisplayName($regionWhereNumberIsValid, $locale);
130
        }
131
    }
132
133
    /**
134
     * Returns the customary display name in the given language for the given region.
135
     *
136
     * @param $regionCode
137
     * @param $locale
138
     * @return string
139
     */
140 241
    private function getRegionDisplayName($regionCode, $locale)
141
    {
142 241
        if ($regionCode === null || $regionCode == 'ZZ' || $regionCode === PhoneNumberUtil::REGION_CODE_FOR_NON_GEO_ENTITY) {
143 1
            return "";
144
        }
145
146 241
        return Locale::getDisplayRegion(
147 241
            Locale::countryCodeToLocale($regionCode),
148
            $locale
149
        );
150
    }
151
152
    /**
153
     * Returns a text description for the given phone number, in the language provided. The
154
     * description might consist of the name of the country where the phone number is from, or the
155
     * name of the geographical area the phone number is from if more detailed information is
156
     * available.
157
     *
158
     * <p>This method assumes the validity of the number passed in has already been checked, and that
159
     * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
160
     * candidates for geocoding.
161
     *
162
     * <p>If $userRegion is set, we also consider the region of the user. If the phone number is from
163
     * the same region as the user, only a lower-level description will be returned, if one exists.
164
     * Otherwise, the phone number's region will be returned, with optionally some more detailed
165
     * information.
166
     *
167
     * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
168
     * CA" for a particular number, omitting the United States from the description. For a user from
169
     * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
170
     * States" or even just "United States".
171
     *
172
     * @param PhoneNumber $number a valid phone number for which we want to get a text description
173
     * @param string $locale the language code for which the description should be written
174
     * @param string $userRegion the region code for a given user. This region will be omitted from the
175
     *     description if the phone number comes from this region. It is a two-letter uppercase ISO
176
     *     country code as defined by ISO 3166-1.
177
     * @return string a text description for the given language code for the given phone number
178
     */
179 246
    public function getDescriptionForValidNumber(PhoneNumber $number, $locale, $userRegion = null)
180
    {
181
        // If the user region matches the number's region, then we just show the lower-level
182
        // description, if one exists - if no description exists, we will show the region(country) name
183
        // for the number.
184 246
        $regionCode = $this->phoneUtil->getRegionCodeForNumber($number);
185 246
        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...
186 13
            $languageStr = Locale::getPrimaryLanguage($locale);
187 13
            $scriptStr = "";
188 13
            $regionStr = Locale::getRegion($locale);
189
190 13
            $mobileToken = $this->phoneUtil->getCountryMobileToken($number->getCountryCode());
191 13
            $nationalNumber = $this->phoneUtil->getNationalSignificantNumber($number);
192 13
            if ($mobileToken !== "" && (!strncmp($nationalNumber, $mobileToken, strlen($mobileToken)))) {
193
                // In some countries, eg. Argentina, mobile numbers have a mobile token before the national
194
                // destination code, this should be removed before geocoding.
195 1
                $nationalNumber = substr($nationalNumber, strlen($mobileToken));
196 1
                $region = $this->phoneUtil->getRegionCodeForCountryCode($number->getCountryCode());
197
                try {
198 1
                    $copiedNumber = $this->phoneUtil->parse($nationalNumber, $region);
199
                } catch (NumberParseException $e) {
200
                    // If this happens, just reuse what we had.
201
                    $copiedNumber = $number;
202
                }
203 1
                $areaDescription = $this->prefixFileReader->getDescriptionForNumber($copiedNumber, $languageStr, $scriptStr, $regionStr);
204
            } else {
205 12
                $areaDescription = $this->prefixFileReader->getDescriptionForNumber($number, $languageStr, $scriptStr, $regionStr);
206
            }
207
208 13
            return (strlen($areaDescription) > 0) ? $areaDescription : $this->getCountryNameForNumber($number, $locale);
209
        }
210
        // Otherwise, we just show the region(country) name for now.
211 234
        return $this->getRegionDisplayName($regionCode, $locale);
212
        // TODO: Concatenate the lower-level and country-name information in an appropriate
213
        // way for each language.
214
    }
215
}
216