Completed
Pull Request — master (#119)
by Joshua
45:09 queued 30:37
created

PhoneNumberOfflineGeocoder   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Test Coverage

Coverage 98.31%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 26
c 8
b 0
f 0
lcom 2
cbo 4
dl 0
loc 200
ccs 58
cts 59
cp 0.9831
rs 10

8 Methods

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