Completed
Pull Request — master (#115)
by
unknown
22:02
created

PhoneNumber   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 402
Duplicated Lines 5.72 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 23
loc 402
ccs 102
cts 102
cp 1
rs 9.1199
c 0
b 0
f 0
wmc 41
lcom 1
cbo 7

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A make() 0 6 1
A ofCountry() 0 11 2
A formatInternational() 0 4 1
A formatNational() 0 4 1
A formatE164() 0 4 1
A formatRFC3966() 0 4 1
A format() 0 13 2
A formatForCountry() 11 11 2
A formatForMobileDialingInCountry() 12 12 2
A getCountry() 0 8 2
A isOfCountry() 0 6 1
B filterValidCountry() 0 39 8
A getType() 0 12 3
A isOfType() 0 11 2
A getPhoneNumberInstance() 0 4 1
A numberLooksInternational() 0 4 1
A lenient() 0 6 1
A toJson() 0 4 1
A jsonSerialize() 0 4 1
A serialize() 0 4 1
A unserialize() 0 6 1
A __toString() 0 10 2
A isValidNumber() 0 11 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PhoneNumber 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 PhoneNumber, and based on these observations, apply Extract Interface, too.

1
<?php namespace Propaganistas\LaravelPhone;
2
3
use Exception;
4
use Illuminate\Contracts\Support\Jsonable;
5
use Illuminate\Support\Arr;
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\Str;
8
use JsonSerializable;
9
use libphonenumber\NumberParseException as libNumberParseException;
10
use libphonenumber\PhoneNumberFormat;
11
use libphonenumber\PhoneNumberType;
12
use libphonenumber\PhoneNumberUtil;
13
use Propaganistas\LaravelPhone\Exceptions\NumberFormatException;
14
use Propaganistas\LaravelPhone\Exceptions\CountryCodeException;
15
use Propaganistas\LaravelPhone\Exceptions\NumberParseException;
16
use Propaganistas\LaravelPhone\Traits\ParsesCountries;
17
use Propaganistas\LaravelPhone\Traits\ParsesFormats;
18
use Propaganistas\LaravelPhone\Traits\ParsesTypes;
19
use Serializable;
20
21
class PhoneNumber implements Jsonable, JsonSerializable, Serializable
22
{
23
    use ParsesCountries,
24
        ParsesFormats,
25
        ParsesTypes;
26
27
    /**
28
     * The provided phone number.
29
     *
30
     * @var string
31
     */
32
    protected $number;
33
34
    /**
35
     * The provided phone country.
36
     *
37
     * @var array
38
     */
39
    protected $countries = [];
40
41
    /**
42
     * The detected phone country.
43
     *
44
     * @var string
45
     */
46
    protected $country;
47
48
    /**
49
     * Whether to allow lenient checks (i.e. landline numbers without area codes).
50
     *
51
     * @var bool
52
     */
53
    protected $lenient = false;
54
55
    /**
56
     * @var \libphonenumber\PhoneNumberUtil
57
     */
58
    protected $lib;
59
60
    /**
61
     * Phone constructor.
62
     *
63
     * @param string $number
64
     */
65 131
    public function __construct($number)
66
    {
67 131
        $this->number = $number;
68 131
        $this->lib = PhoneNumberUtil::getInstance();
69 131
    }
70
71
    /**
72
     * Create a phone instance.
73
     *
74
     * @param string       $number
75
     * @param string|array $country
76
     * @return static
77
     */
78 41
    public static function make($number, $country = [])
79
    {
80 41
        $instance = new static($number);
81
82 41
        return $instance->ofCountry($country);
83
    }
84
85
    /**
86
     * Set the country to which the phone number belongs to.
87
     *
88
     * @param string|array $country
89
     * @return static
90
     */
91 110
    public function ofCountry($country)
92
    {
93 110
        $countries = is_array($country) ? $country : func_get_args();
94
95 110
        $instance = clone $this;
96 110
        $instance->countries = array_unique(
97 110
            array_merge($instance->countries, static::parseCountries($countries))
98
        );
99
100 110
        return $instance;
101
    }
102
103
    /**
104
     * Format the phone number in international format.
105
     *
106
     * @return string
107
     */
108 3
    public function formatInternational()
109
    {
110 3
        return $this->format(PhoneNumberFormat::INTERNATIONAL);
111
    }
112
113
    /**
114
     * Format the phone number in national format.
115
     *
116
     * @return string
117
     */
118 3
    public function formatNational()
119
    {
120 3
        return $this->format(PhoneNumberFormat::NATIONAL);
121
    }
122
123
    /**
124
     * Format the phone number in E164 format.
125
     *
126
     * @return string
127
     */
128 21
    public function formatE164()
129
    {
130 21
        return $this->format(PhoneNumberFormat::E164);
131
    }
132
133
    /**
134
     * Format the phone number in RFC3966 format.
135
     *
136
     * @return string
137
     */
138 12
    public function formatRFC3966()
139
    {
140 12
        return $this->format(PhoneNumberFormat::RFC3966);
141
    }
142
143
    /**
144
     * Format the phone number in a given format.
145
     *
146
     * @param string $format
147
     * @return string
148
     * @throws \Propaganistas\LaravelPhone\Exceptions\NumberFormatException
149
     */
150 54
    public function format($format)
151
    {
152 54
        $parsedFormat = static::parseFormat($format);
153
154 54
        if (is_null($parsedFormat)) {
155 3
            throw NumberFormatException::invalid($format);
156
        }
157
158 51
        return $this->lib->format(
159 51
            $this->getPhoneNumberInstance(),
160 33
            $parsedFormat
161
        );
162
    }
163
164
    /**
165
     * Format the phone number in a way that it can be dialled from the provided country.
166
     *
167
     * @param string $country
168
     * @return string
169
     * @throws \Propaganistas\LaravelPhone\Exceptions\CountryCodeException
170
     */
171 6 View Code Duplication
    public function formatForCountry($country)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
172
    {
173 6
        if (! static::isValidCountryCode($country)) {
174 3
            throw CountryCodeException::invalid($country);
175
        }
176
177 3
        return $this->lib->formatOutOfCountryCallingNumber(
178 3
            $this->getPhoneNumberInstance(),
179 3
            $country
180
        );
181
    }
182
183
    /**
184
     * Format the phone number in a way that it can be dialled from the provided country using a cellphone.
185
     *
186
     * @param string $country
187
     * @param bool   $removeFormatting
188
     * @return string
189
     * @throws \Propaganistas\LaravelPhone\Exceptions\CountryCodeException
190
     */
191 6 View Code Duplication
    public function formatForMobileDialingInCountry($country, $removeFormatting = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
192
    {
193 6
        if (! static::isValidCountryCode($country)) {
194 3
            throw CountryCodeException::invalid($country);
195
        }
196
197 3
        return $this->lib->formatNumberForMobileDialing(
198 3
            $this->getPhoneNumberInstance(),
199 3
            $country,
200 3
            $removeFormatting
201
        );
202
    }
203
204
    /**
205
     * Get the phone number's country.
206
     *
207
     * @return string
208
     */
209 110
    public function getCountry()
210
    {
211 110
        if (! $this->country) {
212 110
            $this->country = $this->filterValidCountry($this->countries);
213
        }
214
215 92
        return $this->country;
216
    }
217
218
    /**
219
     * Check if the phone number is of (a) given country(ies).
220
     *
221
     * @param string|array $country
222
     * @return bool
223
     */
224 3
    public function isOfCountry($country)
225
    {
226 3
        $countries = static::parseCountries($country);
227
228 3
        return in_array($this->getCountry(), $countries);
229
    }
230
231
    /**
232
     * Filter the provided countries to the one that is valid for the number.
233
     *
234
     * @param string|array $countries
235
     * @return string
236
     * @throws \Propaganistas\LaravelPhone\Exceptions\NumberParseException
237
     */
238 110
    protected function filterValidCountry($countries)
239
    {
240 110
        $result = Collection::make($countries)
241 36
                            ->filter(function ($country) {
242
                                try {
243 98
                                    $instance = $this->lib->parse($this->number, $country);
244
245 98
                                    return $this->lenient
246 17
                                        ? $this->lib->isPossibleNumber($instance, $country)
247 98
                                        : $this->lib->isValidNumberForRegion($instance, $country);
248 3
                                } catch (libNumberParseException $e) {
249 3
                                    return false;
250
                                }
251 110
                            })->first();
252
253
        // If we got a new result, return it.
254 110
        if ($result) {
255 89
            return $result;
256
        }
257
258
        // Last resort: try to detect it from an international number.
259 56
        if ($this->numberLooksInternational()) {
260 33
            $countries[] = null;
261
        }
262
263 56
        foreach ($countries as $country) {
0 ignored issues
show
Bug introduced by
The expression $countries of type string|array 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...
264 47
            $instance = $this->lib->parse($this->number, $country);
265
266 47
            if ($this->lib->isValidNumber($instance)) {
267 41
                return $this->lib->getRegionCodeForNumber($instance);
268
            }
269
        }
270
271 38
        if ($countries = array_filter($countries)) {
272 20
            throw NumberParseException::countryMismatch($this->number, $countries);
273
        }
274
275 24
        throw NumberParseException::countryRequired($this->number);
276
    }
277
278
    /**
279
     * Get the phone number's type.
280
     *
281
     * @param bool $asConstant
282
     * @return string|int|null
283
     */
284 26
    public function getType($asConstant = false)
285
    {
286 26
        $type = $this->lib->getNumberType($this->getPhoneNumberInstance());
287
288 26
        if ($asConstant) {
289 26
            return $type;
290
        }
291
292 3
        $stringType = Arr::get(static::parseTypesAsStrings($type), 0);
293
294 3
        return $stringType ? strtolower($stringType) : null;
295
    }
296
297
    /**
298
     * Check if the phone number is of (a) given type(s).
299
     *
300
     * @param string $type
301
     * @return bool
302
     */
303 23
    public function isOfType($type)
304
    {
305 23
        $types = static::parseTypes($type);
306
307
        // Add the unsure type when applicable.
308 23
        if (array_intersect([PhoneNumberType::FIXED_LINE, PhoneNumberType::MOBILE], $types)) {
309 23
            $types[] = PhoneNumberType::FIXED_LINE_OR_MOBILE;
310
        }
311
312 23
        return in_array($this->getType(true), $types, true);
313
    }
314
315
    /**
316
     * Get the PhoneNumber instance of the current number.
317
     *
318
     * @return \libphonenumber\PhoneNumber
319
     */
320 98
    public function getPhoneNumberInstance()
321
    {
322 98
        return $this->lib->parse($this->number, $this->getCountry());
323
    }
324
325
    /**
326
     * Determine whether the phone number seems to be in international format.
327
     *
328
     * @return bool
329
     */
330 56
    protected function numberLooksInternational()
331
    {
332 56
        return Str::startsWith($this->number, '+');
333
    }
334
335
    /**
336
     * Enable lenient number parsing.
337
     *
338
     * @return $this
339
     */
340 32
    public function lenient()
341
    {
342 32
        $this->lenient = true;
343
344 32
        return $this;
345
    }
346
347
    /**
348
     * Convert the phone instance to JSON.
349
     *
350
     * @param  int $options
351
     * @return string
352
     */
353 3
    public function toJson($options = 0)
354
    {
355 3
        return json_encode($this->jsonSerialize(), $options);
356
    }
357
358
    /**
359
     * Convert the phone instance into something JSON serializable.
360
     *
361
     * @return string
362
     */
363 3
    public function jsonSerialize()
364
    {
365 3
        return $this->formatE164();
366
    }
367
368
    /**
369
     * Convert the phone instance into a string representation.
370
     *
371
     * @return string
372
     */
373 3
    public function serialize()
374
    {
375 3
        return $this->formatE164();
376
    }
377
378
    /**
379
     * Reconstructs the phone instance from a string representation.
380
     *
381
     * @param string $serialized
382
     */
383 3
    public function unserialize($serialized)
384
    {
385 3
        $this->lib = PhoneNumberUtil::getInstance();
386 3
        $this->number = $serialized;
387 3
        $this->country = $this->lib->getRegionCodeForNumber($this->getPhoneNumberInstance());
388 3
    }
389
390
    /**
391
     * Convert the phone instance to a formatted number.
392
     *
393
     * @return string
394
     */
395 12
    public function __toString()
396
    {
397
        // Formatting the phone number could throw an exception, but __toString() doesn't cope well with that.
398
        // Let's just return the original number in that case.
399
        try {
400 12
            return $this->formatE164();
401 6
        } catch (Exception $exception) {
402 6
            return (string) $this->number;
403
        }
404
    }
405
406
    /**
407
     * Check if the phone number is valid.
408
     *
409
     * @return boolean
410
     */
411
    public function isValidNumber()
412
    {
413
        try {
414
            return $this->lib->isValidNumber(
415
                $this->getPhoneNumberInstance(),
416
                $this->getCountry()
0 ignored issues
show
Unused Code introduced by
The call to PhoneNumberUtil::isValidNumber() has too many arguments starting with $this->getCountry().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
417
            );
418
        } catch(Exception $exception) {
419
            return false;
420
        }
421
    }
422
}
423