Phone   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 482
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 4

Test Coverage

Coverage 80%

Importance

Changes 0
Metric Value
wmc 59
lcom 3
cbo 4
dl 0
loc 482
ccs 120
cts 150
cp 0.8
rs 4.08
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A setAllowedCountries() 0 19 3
A addAllowedCountry() 0 17 4
A getAllowedCountries() 0 8 3
A setDefaultCountry() 0 13 2
A setAllowedPhoneTypes() 0 14 2
A addAllowedPhoneType() 0 10 1
A getAllowedPhoneTypes() 0 4 1
B setValue() 0 32 6
A getValuePart() 0 11 3
A getValue() 0 17 5
A loadHttpData() 0 8 5
A getControl() 0 7 1
C getControlPart() 0 94 9
A getLabel() 0 7 1
A getLabelPart() 0 4 1
A validateCountry() 0 12 6
A validateType() 0 12 2
A register() 0 19 3

How to fix   Complexity   

Complex Class

Complex classes like Phone 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 Phone, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Phone.php
4
 *
5
 * @copyright      More in license.md
6
 * @license        http://www.ipublikuj.eu
7
 * @author         Adam Kadlec <[email protected]>
8
 * @package        iPublikuj:FormPhone!
9
 * @subpackage     Controls
10
 * @since          1.0.0
11
 *
12
 * @date           15.12.15
13
 */
14
15
declare(strict_types = 1);
16
17
namespace IPub\FormPhone\Controls;
18
19
use Nette;
20
use Nette\Forms;
21
use Nette\Localization;
22
use Nette\Utils;
23
24
use IPub;
25
use IPub\FormPhone;
26
use IPub\FormPhone\Exceptions;
27
28
use IPub\Phone\Phone as PhoneUtils;
29
30
use libphonenumber;
31
32
/**
33
 * Form phone control element
34
 *
35
 * @package        iPublikuj:FormPhone!
36
 * @subpackage     Controls
37
 *
38
 * @author         Adam Kadlec <[email protected]>
39
 *
40
 * @property-read string $emptyValue
41
 * @property-read Nette\Forms\Rules $rules
42
 */
43 1
class Phone extends Forms\Controls\TextInput
44
{
45
	/**
46
	 * Define filed attributes
47
	 */
48
	const FIELD_COUNTRY = 'country';
49
	const FIELD_NUMBER = 'number';
50
51
	/**
52
	 * @var IPub\Phone\Phone
53
	 */
54
	private $phoneUtils;
55
56
	/**
57
	 * List of allowed countries
58
	 *
59
	 * @var array
60
	 */
61
	private $allowedCountries = [];
62
63
	/**
64
	 * List of allowed phone types
65
	 *
66
	 * @var array
67
	 */
68
	private $allowedTypes = [];
69
70
	/**
71
	 * @var string|NULL
72
	 */
73
	private $number = NULL;
74
75
	/**
76
	 * @var string|NULL
77
	 */
78
	private $country = NULL;
79
80
	/**
81
	 * @var string
82
	 */
83
	private $defaultCountry;
84
85
	/**
86
	 * @var bool
87
	 */
88
	private static $registered = FALSE;
89
90
	/**
91
	 * @param PhoneUtils $phoneUtils
92
	 * @param string|NULL $label
93
	 * @param int|NULL $maxLength
94
	 */
95
	public function __construct(PhoneUtils $phoneUtils, string $label = NULL, int $maxLength = NULL)
96
	{
97 1
		parent::__construct($label, $maxLength);
98
99 1
		$this->phoneUtils = $phoneUtils;
100 1
	}
101
102
	/**
103
	 * @param array $countries
104
	 *
105
	 * @return $this
106
	 *
107
	 * @throws Exceptions\NoValidCountryException
108
	 */
109
	public function setAllowedCountries(array $countries = [])
110
	{
111 1
		$this->allowedCountries = [];
112
113 1
		foreach ($countries as $country) {
114 1
			$country = $this->validateCountry($country);
115 1
			$this->allowedCountries[] = strtoupper($country);
116
		}
117
118
		// Check for auto country detection
119 1
		if (in_array('AUTO', $this->allowedCountries)) {
120
			$this->allowedCountries = ['AUTO'];
121
		}
122
123
		// Remove duplicities
124 1
		array_unique($this->allowedCountries);
125
126 1
		return $this;
127
	}
128
129
	/**
130
	 * @param string $country
131
	 *
132
	 * @return $this
133
	 *
134
	 * @throws Exceptions\NoValidCountryException
135
	 */
136
	public function addAllowedCountry(string $country)
137
	{
138 1
		$country = $this->validateCountry($country);
139 1
		$this->allowedCountries[] = strtoupper($country);
140
141
		// Remove duplicities
142 1
		array_unique($this->allowedCountries);
143
144 1
		if (strtoupper($country) === 'AUTO') {
145
			$this->allowedCountries = ['AUTO'];
146
147 1
		} elseif (($key = array_search('AUTO', $this->allowedCountries)) && $key !== FALSE) {
148
			unset($this->allowedCountries[$key]);
149
		}
150
151 1
		return $this;
152
	}
153
154
	/**
155
	 * @return array
156
	 */
157
	public function getAllowedCountries() : array
158
	{
159 1
		if (in_array('AUTO', $this->allowedCountries, TRUE) || $this->allowedCountries === []) {
160 1
			return $this->phoneUtils->getSupportedCountries();
161
		}
162
163 1
		return $this->allowedCountries;
164
	}
165
166
	/**
167
	 * @param string|NULL $country
168
	 *
169
	 * @return $this
170
	 *
171
	 * @throws Exceptions\NoValidCountryException
172
	 */
173
	public function setDefaultCountry(string $country = NULL)
174
	{
175 1
		if ($country === NULL) {
176 1
			$this->defaultCountry = NULL;
177
178
		} else {
179 1
			$country = $this->validateCountry($country);
180
181 1
			$this->defaultCountry = strtoupper($country);
182
		}
183
184 1
		return $this;
185
	}
186
187
	/**
188
	 * @param array $types
189
	 *
190
	 * @return $this
191
	 *
192
	 * @throws Exceptions\NoValidTypeException
193
	 */
194
	public function setAllowedPhoneTypes(array $types = [])
195
	{
196
		$this->allowedTypes = [];
197
198
		foreach ($types as $type) {
199
			$type = $this->validateType($type);
200
			$this->allowedTypes[] = strtoupper($type);
201
		}
202
203
		// Remove duplicities
204
		array_unique($this->allowedTypes);
205
206
		return $this;
207
	}
208
209
	/**
210
	 * @param string $type
211
	 *
212
	 * @return $this
213
	 *
214
	 * @throws Exceptions\NoValidTypeException
215
	 */
216
	public function addAllowedPhoneType(string $type)
217
	{
218 1
		$type = $this->validateType($type);
219 1
		$this->allowedTypes[] = strtoupper($type);
220
221
		// Remove duplicities
222 1
		array_unique($this->allowedTypes);
223
224 1
		return $this;
225
	}
226
227
	/**
228
	 * @return array
229
	 */
230
	public function getAllowedPhoneTypes() : array
231
	{
232 1
		return $this->allowedTypes;
233
	}
234
235
	/**
236
	 * @param string
237
	 *
238
	 * @return $this
239
	 *
240
	 * @throws Exceptions\InvalidArgumentException
241
	 * @throws IPub\Phone\Exceptions\NoValidCountryException
242
	 * @throws IPub\Phone\Exceptions\NoValidPhoneException
243
	 */
244
	public function setValue($value)
245
	{
246 1
		if ($value === NULL) {
247 1
			$this->country = NULL;
248 1
			$this->number = NULL;
249
250 1
			return $this;
251
		}
252
253 1
		if ($value instanceof IPub\Phone\Entities\Phone) {
0 ignored issues
show
Bug introduced by
The class IPub\Phone\Entities\Phone does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
254
			if (in_array($value->getCountry(), $this->getAllowedCountries(), TRUE)) {
255
				$this->country = $value->getCountry();
256
				$this->number = str_replace(' ', '', $value->getNationalNumber());
257
258
				return $this;
259
			}
260
261
		} else {
262 1
			foreach ($this->getAllowedCountries() as $country) {
263 1
				if ($this->phoneUtils->isValid((string) $value, $country)) {
264 1
					$phone = IPub\Phone\Entities\Phone::fromNumber((string) $value, $country);
265
266 1
					$this->country = $phone->getCountry();
267 1
					$this->number = str_replace(' ', '', $phone->getNationalNumber());
268
269 1
					return $this;
270
				}
271
			}
272
		}
273
274 1
		throw new Exceptions\InvalidArgumentException(sprintf('Provided value is not valid phone number, or is out of list of allowed countries, "%s" given.', $value));
275
	}
276
277
	/**
278
	 * @param string $key
279
	 *
280
	 * @return string
281
	 *
282
	 * @throws Exceptions\InvalidArgumentException
283
	 */
284
	public function getValuePart(string $key) : string
285
	{
286 1
		if ($key === self::FIELD_COUNTRY) {
287 1
			return $this->country;
288
289 1
		} elseif ($key === self::FIELD_NUMBER) {
290 1
			return $this->number;
291
		}
292
293
		throw new Exceptions\InvalidArgumentException('Invalid field key provided.');
294
	}
295
296
	/**
297
	 * @return IPub\Phone\Entities\Phone|NULL|FALSE
298
	 */
299
	public function getValue()
300
	{
301 1
		if ($this->country === NULL || $this->number === NULL) {
302 1
			return NULL;
303
		}
304
305
		try {
306
			// Try to parse number & country
307 1
			return IPub\Phone\Entities\Phone::fromNumber($this->number, $this->country);
308
309
		} catch (IPub\Phone\Exceptions\NoValidCountryException $ex) {
0 ignored issues
show
Bug introduced by
The class IPub\Phone\Exceptions\NoValidCountryException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
310
			return FALSE;
311
312
		} catch (IPub\Phone\Exceptions\NoValidPhoneException $ex) {
0 ignored issues
show
Bug introduced by
The class IPub\Phone\Exceptions\NoValidPhoneException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
313
			return FALSE;
314
		}
315
	}
316
317
	/**
318
	 * Loads HTTP data
319
	 *
320
	 * @return void
321
	 */
322
	public function loadHttpData()
323
	{
324 1
		$country = $this->getHttpData(Forms\Form::DATA_LINE, '[' . self::FIELD_COUNTRY . ']');
325 1
		$this->country = ($country === '' || $country === NULL) ? NULL : (string) $country;
326
327 1
		$number = $this->getHttpData(Forms\Form::DATA_LINE, '[' . self::FIELD_NUMBER . ']');
328 1
		$this->number = ($number === '' || $number === NULL) ? NULL : (string) $number;
329 1
	}
330
331
	/**
332
	 * @return Utils\Html
333
	 */
334
	public function getControl()
335
	{
336 1
		$el = Utils\Html::el();
337 1
		$el->addHtml($this->getControlPart(self::FIELD_COUNTRY) . $this->getControlPart(self::FIELD_NUMBER));
338
339 1
		return $el;
340
	}
341
342
	/**
343
	 * @return Utils\Html
344
	 *
345
	 * @throws Exceptions\InvalidArgumentException
346
	 */
347
	public function getControlPart()
348
	{
349 1
		$args = func_get_args();
350 1
		$key = reset($args);
351
352 1
		$name = $this->getHtmlName();
353
354 1
		if ($key === self::FIELD_COUNTRY) {
355
			// Try to get translator
356 1
			$translator = $this->getTranslator();
357
358 1
			$locale = 'en_US';
359
360 1
			if ($translator instanceof Localization\ITranslator && method_exists($translator, 'getLocale') === TRUE) {
0 ignored issues
show
Bug introduced by
The class Nette\Localization\ITranslator does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
361
				try {
362
					$locale = $translator->getLocale();
363
364
				} catch (\Exception $ex) {
365
					$locale = 'en_US';
366
				}
367
			}
368
369 1
			$items = array_reduce($this->getAllowedCountries(), function (array $result, $row) use ($locale) {
370 1
				$countryName = FormPhone\Locale\Locale::getDisplayRegion(
371 1
					FormPhone\Locale\Locale::countryCodeToLocale($row),
372
					$locale
373
				);
374
375 1
				$result[$row] = Utils\Html::el('option');
376 1
				$result[$row]->setText('+' . $this->phoneUtils->getCountryCodeForCountry($row) . ' (' . $countryName . ')');
377 1
				$result[$row]->data('mask', preg_replace('/[0-9]/', '9', $this->phoneUtils->getExampleNationalNumber($row)));
378 1
				$result[$row]->addAttributes([
379 1
					'value' => $row,
380
				]);
381
382 1
				return $result;
383 1
			}, []);
384
385 1
			$control = Forms\Helpers::createSelectBox(
386
				$items,
387
				[
388 1
					'selected?' => $this->country === NULL ? $this->defaultCountry : $this->country,
389
				]
390
			);
391
392 1
			$control->addAttributes([
393 1
				'name' => $name . '[' . self::FIELD_COUNTRY . ']',
394 1
				'id'   => $this->getHtmlId() . '-' . self::FIELD_COUNTRY,
395
			]);
396 1
			$control->data('ipub-forms-phone', '');
397 1
			$control->data('settings', json_encode([
398 1
					'field' => $name . '[' . self::FIELD_NUMBER . ']'
399
				])
400
			);
401
402 1
			if ($this->isDisabled()) {
403
				$control->addAttributes([
404
					'disabled' => TRUE,
405
				]);
406
			}
407
408 1
			return $control;
409
410 1
		} elseif ($key === self::FIELD_NUMBER) {
411 1
			$prototype = $this->getControlPrototype();
412
413 1
			$input = parent::getControl();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (getControl() instead of getControlPart()). Are you sure this is correct? If so, you might want to change this to $this->getControl().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
414
415 1
			$control = Utils\Html::el('input');
416
417 1
			$control->addAttributes([
418 1
				'name'        => $name . '[' . self::FIELD_NUMBER . ']',
419 1
				'id'          => $this->getHtmlId() . '-' . self::FIELD_NUMBER,
420 1
				'value'       => $this->number,
421 1
				'type'        => 'text',
422 1
				'class'       => $prototype->getAttribute('class'),
423 1
				'placeholder' => $prototype->getAttribute('placeholder'),
424 1
				'title'       => $prototype->getAttribute('title')
425
			]);
426
427 1
			$control->data('nette-empty-value', Utils\Strings::trim($this->translate($this->emptyValue)));
428 1
			$control->data('nette-rules', $input->{'data-nette-rules'});
429
430 1
			if ($this->isDisabled()) {
431
				$control->addAttributes([
432
					'disabled' => TRUE,
433
				]);
434
			}
435
436 1
			return $control;
437
		}
438
439
		throw new Exceptions\InvalidArgumentException(sprintf('Part "%s" does not exist.', $key));
440
	}
441
442
	/**
443
	 * {@inheritdoc}
444
	 */
445
	public function getLabel($caption = NULL)
446
	{
447
		$label = parent::getLabel($caption);
448
		$label->for = $this->getHtmlId() . '-' . self::FIELD_NUMBER;
449
450
		return $label;
451
	}
452
453
	/**
454
	 * {@inheritdoc}
455
	 */
456
	public function getLabelPart()
457
	{
458 1
		return NULL;
459
	}
460
461
	/**
462
	 * @param string $country
463
	 *
464
	 * @return string
465
	 *
466
	 * @throws Exceptions\NoValidCountryException
467
	 */
468
	private function validateCountry(string $country) : string
469
	{
470
		// Country code have to be upper-cased
471 1
		$country = strtoupper($country);
472
473 1
		if ((strlen($country) === 2 && ctype_alpha($country) && ctype_upper($country) && in_array($country, $this->phoneUtils->getSupportedCountries())) || $country === 'AUTO') {
474 1
			return $country;
475
476
		} else {
477 1
			throw new Exceptions\NoValidCountryException(sprintf('Provided country code "%s" is not valid. Provide valid country code or AUTO for automatic detection.', $country));
478
		}
479
	}
480
481
	/**
482
	 * @param string $type
483
	 *
484
	 * @return string
485
	 *
486
	 * @throws Exceptions\NoValidTypeException
487
	 */
488
	private function validateType(string $type) : string
489
	{
490
		// Phone type have to be upper-cased
491 1
		$type = strtoupper($type);
492
493 1
		if (defined('\IPub\Phone\Phone::TYPE_' . $type)) {
494 1
			return $type;
495
496
		} else {
497
			throw new Exceptions\NoValidTypeException(sprintf('Provided phone type "%s" is not valid. Provide valid phone type.', $type));
498
		}
499
	}
500
501
	/**
502
	 * @param PhoneUtils $phoneUtils
503
	 * @param string $method
504
	 */
505
	public static function register(PhoneUtils $phoneUtils, string $method = 'addPhone')
506
	{
507
		// Check for multiple registration
508 1
		if (self::$registered) {
509 1
			throw new Nette\InvalidStateException('Phone control already registered.');
510
		}
511
512 1
		self::$registered = TRUE;
513
514 1
		$class = function_exists('get_called_class') ? get_called_class() : __CLASS__;
515 1
		Forms\Container::extensionMethod(
516 1
			$method, function (Forms\Container $form, $name, $label = NULL, $maxLength = NULL) use ($class, $phoneUtils) {
517 1
			$component = new $class($phoneUtils, $label, $maxLength);
518 1
			$form->addComponent($component, $name);
519
520 1
			return $component;
521 1
		}
522
		);
523 1
	}
524
}
525