Completed
Push — master ( 0993ee...e11175 )
by Adam
02:47
created

Phone   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 10

Test Coverage

Coverage 85.11%

Importance

Changes 11
Bugs 3 Features 0
Metric Value
wmc 51
c 11
b 3
f 0
lcom 3
cbo 10
dl 0
loc 421
ccs 120
cts 141
cp 0.8511
rs 8.3206

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A setDefaultCountry() 0 13 2
B setValue() 0 22 4
B getValue() 0 19 6
A setAllowedCountries() 0 20 3
A addAllowedCountry() 0 17 4
A getAllowedCountries() 0 9 3
A setAllowedPhoneTypes() 0 15 2
A addAllowedPhoneType() 0 10 1
A getAllowedPhoneTypes() 0 4 1
A loadHttpData() 0 8 3
A getControl() 0 5 1
C getControlPart() 0 70 8
A getLabelPart() 0 4 1
B 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
namespace IPub\FormPhone\Controls;
16
17
use Nette;
18
use Nette\Application\UI;
19
use Nette\Bridges;
20
use Nette\Forms;
21
use Nette\Localization;
22
use Nette\Utils;
23
24
use Latte;
25
26
use IPub;
27 1
use IPub\FormPhone;
28 1
use IPub\FormPhone\Exceptions;
29
30
use IPub\Phone\Phone as PhoneUtils;
31
32
use libphonenumber;
33
use libphonenumber\geocoding;
34
35
/**
36
 * Form phone control element
37
 *
38
 * @package        iPublikuj:FormPhone!
39
 * @subpackage     Controls
40
 *
41
 * @author         Adam Kadlec <[email protected]>
42
 */
43
class Phone extends Forms\Controls\TextInput
44 1
{
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 1
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 1
90
	/**
91
	 * @param PhoneUtils $phoneUtils
92
	 * @param string|NULL $label
93
	 * @param int|NULL $maxLength
94
	 */
95
	public function __construct(PhoneUtils $phoneUtils, $label = NULL, $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
		{
115 1
			$country = $this->validateCountry($country);
116 1
			$this->allowedCountries[] = strtoupper($country);
117 1
		}
118
119
		// Check for auto country detection
120 1
		if (in_array('AUTO', $this->allowedCountries)) {
121
			$this->allowedCountries = ['AUTO'];
122
		}
123
124
		// Remove duplicities
125 1
		array_unique($this->allowedCountries);
126
127 1
		return $this;
128
	}
129
130
	/**
131
	 * @param string $country
132
	 *
133 1
	 * @return $this
134
	 *
135
	 * @throws Exceptions\NoValidCountryException
136
	 */
137
	public function addAllowedCountry($country)
138
	{
139 1
		$country = $this->validateCountry($country);
140 1
		$this->allowedCountries[] = strtoupper($country);
141
142
		// Remove duplicities
143 1
		array_unique($this->allowedCountries);
144
145 1
		if (strtoupper($country) === 'AUTO') {
146
			$this->allowedCountries = ['AUTO'];
147
148 1
		} else if (($key = array_search('AUTO', $this->allowedCountries)) && $key !== FALSE) {
149
			unset($this->allowedCountries[$key]);
150
		}
151
152 1
		return $this;
153
	}
154
155
	/**
156
	 * @return array
157
	 */
158
	public function getAllowedCountries()
159
	{
160 1
		if (in_array('AUTO', $this->allowedCountries, TRUE) || $this->allowedCountries === []) {
161 1
			return $this->phoneUtils->getSupportedCountries();
162
163
		} else {
164 1
			return $this->allowedCountries;
165
		}
166
	}
167
168
	/**
169
	 * @param string|NULL $country
170
	 *
171
	 * @return $this
172
	 *
173
	 * @throws Exceptions\NoValidCountryException
174
	 */
175
	public function setDefaultCountry($country = NULL)
176
	{
177 1
		if ($country === NULL) {
178 1
			$this->defaultCountry = NULL;
179
180 1
		} else {
181 1
			$country = $this->validateCountry($country);
182
183 1
			$this->defaultCountry = strtoupper($country);
184 1
		}
185
186 1
		return $this;
187
	}
188
189 1
	/**
190 1
	 * @param array $types
191
	 *
192
	 * @return $this
193
	 *
194
	 * @throws Exceptions\NoValidTypeException
195
	 */
196
	public function setAllowedPhoneTypes(array $types = [])
197
	{
198
		$this->allowedTypes = [];
199
200
		foreach($types as $type)
201
		{
202
			$type = $this->validateType($type);
203
			$this->allowedTypes[] = strtoupper($type);
204
		}
205
206
		// Remove duplicities
207
		array_unique($this->allowedTypes);
208
209
		return $this;
210
	}
211
212
	/**
213
	 * @param string $type
214
	 *
215
	 * @return $this
216
	 *
217
	 * @throws Exceptions\NoValidTypeException
218
	 */
219
	public function addAllowedPhoneType($type)
220
	{
221 1
		$type = $this->validateType($type);
222 1
		$this->allowedTypes[] = strtoupper($type);
223
224
		// Remove duplicities
225 1
		array_unique($this->allowedTypes);
226
227 1
		return $this;
228
	}
229
230
	/**
231
	 * @return array
232
	 */
233
	public function getAllowedPhoneTypes()
234
	{
235 1
		return $this->allowedTypes;
236
	}
237
238
	/**
239
	 * @param string
240
	 *
241
	 * @return $this
242
	 *
243
	 * @throws Exceptions\InvalidArgumentException
244
	 */
245
	public function setValue($value)
246
	{
247 1
		if ($value === NULL) {
248 1
			$this->country = NULL;
249 1
			$this->number = NULL;
250
251 1
			return $this;
252
		}
253
254 1
		foreach($this->getAllowedCountries() as $country) {
255 1
			if ($this->phoneUtils->isValid($value, $country)) {
256 1
				$phone = $this->phoneUtils->parse($value, $country);
257
258 1
				$this->country = $phone->getCountry();
259 1
				$this->number = str_replace(' ', '', $phone->getNationalNumber());
260
261 1
				return $this;
262
			}
263 1
		}
264
265 1
		throw new Exceptions\InvalidArgumentException('Provided value is not valid phone number, or is out of list of allowed countries, "' . $value . '" given.');
266
	}
267
268
	/**
269
	 * @return IPub\Phone\Entities\Phone|NULL
270
	 */
271
	public function getValue()
272
	{
273 1
		if ($this->country === NULL || $this->number === NULL) {
274 1
			return NULL;
275
		}
276
277
		try {
278 1
			// Try to parse number & country
279 1
			$number = $this->phoneUtils->parse($this->number, $this->country);
280
281
			return $number === NULL ? NULL : $number;
282 1
283 1
		} catch (IPub\Phone\Exceptions\NoValidCountryException $ex) {
284
			return NULL;
285
286 1
		} catch (IPub\Phone\Exceptions\NoValidPhoneException $ex) {
287
			return NULL;
288
		}
289
	}
290
291
	/**
292
	 * Loads HTTP data
293
	 *
294
	 * @return void
295
	 */
296 1
	public function loadHttpData()
297 1
	{
298 1
		$country = $this->getHttpData(Forms\Form::DATA_LINE, '[' . static::FIELD_COUNTRY . ']');
299
		$this->country = (string) $country === '' | $country === NULL ? NULL : $country;
0 ignored issues
show
Documentation Bug introduced by
It seems like (string) $country === ''... NULL ? NULL : $country can also be of type array or object<Nette\Http\FileUpload>. However, the property $country is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: ((string) $country === '') | $country === NULL, Probably Intended Meaning: (string) $country === ('' | $country === NULL)

When comparing the result of a bit operation, we suggest to add explicit parenthesis and not to rely on PHP’s built-in operator precedence to ensure the code behaves as intended and to make it more readable.

Let’s take a look at these examples:

// Returns always int(0).
return 0 === $foo & 4;
return (0 === $foo) & 4;

// More likely intended return: true/false
return 0 === ($foo & 4);
Loading history...
300
301
		$number = $this->getHttpData(Forms\Form::DATA_LINE, '[' . static::FIELD_NUMBER . ']');
302
		$this->number = (string) $number === '' | $number === NULL ? NULL : $number;
0 ignored issues
show
Documentation Bug introduced by
It seems like (string) $number === '' ...= NULL ? NULL : $number can also be of type array or object<Nette\Http\FileUpload>. However, the property $number is declared as type string|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: ((string) $number === '') | $number === NULL, Probably Intended Meaning: (string) $number === ('' | $number === NULL)

When comparing the result of a bit operation, we suggest to add explicit parenthesis and not to rely on PHP’s built-in operator precedence to ensure the code behaves as intended and to make it more readable.

Let’s take a look at these examples:

// Returns always int(0).
return 0 === $foo & 4;
return (0 === $foo) & 4;

// More likely intended return: true/false
return 0 === ($foo & 4);
Loading history...
303
	}
304
305 1
	/**
306 1
	 * @return Utils\Html
307 1
	 */
308
	public function getControl()
309 1
	{
310
		return Utils\Html::el()
311
			->add($this->getControlPart(static::FIELD_COUNTRY) . $this->getControlPart(static::FIELD_NUMBER));
312
	}
313
314
	/**
315
	 * @param string
316
	 *
317 1
	 * @return Utils\Html
318
	 *
319 1
	 * @throws Exceptions\InvalidArgumentException
320
	 */
321
	public function getControlPart($key)
322 1
	{
323
		$name = $this->getHtmlName();
324
325 1
		// Try to get translator
326 1
		$translator = $this->getTranslator();
327 1
328 1
		if ($translator instanceof Localization\ITranslator && method_exists($translator, 'getLocale')) {
329 1
			$locale = $translator->getLocale();
0 ignored issues
show
Bug introduced by
The method getLocale() does not seem to exist on object<Nette\Localization\ITranslator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
330
		} else {
331 1
			$locale = 'en_US';
332
		}
333 1
334 1
		if ($key === static::FIELD_COUNTRY) {
335 1
			$control = Forms\Helpers::createSelectBox(
336 1
				array_reduce($this->getAllowedCountries(), function (array $result, $row) use ($locale) {
337 1
					$countryName = geocoding\Locale::getDisplayRegion(
338 1
						geocoding\Locale::countryCodeToLocale($row),
339
						$locale
340 1
					);
341 1
342
					$result[$row] = Utils\Html::el('option')
343 1
						->setText('+' . $this->phoneUtils->getCountryCodeForCountry($row) . ' ('. $countryName . ')')
344
						->addAttributes([
345 1
							'data-mask' => preg_replace('/[0-9]/', '9', $this->phoneUtils->getExampleNationalNumber($row)),
346
						])
347
						->value($row);
348 1
349 1
					return $result;
350 1
				}, []),
351 1
				[
352 1
					'selected?' => $this->country === NULL ? $this->defaultCountry : $this->country,
353 1
				]
354
			);
355 1
356
			$control
357
				->name($name . '[' . static::FIELD_COUNTRY . ']')
358
				->id($this->getHtmlId() . '-' . static::FIELD_COUNTRY)
359 1
				->{'data-ipub-forms-phone'}('')
360
				->{'data-settings'}(json_encode([
361 1
					'field' => $name . '[' . static::FIELD_NUMBER . ']'
362 1
				]));
363
364 1
			if ($this->isDisabled()) {
365
				$control->disabled(TRUE);
366
			}
367 1
368 1
			return $control;
369 1
370 1
		} else if ($key === static::FIELD_NUMBER) {
371 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...
372
373 1
			$control = Utils\Html::el('input');
374
375
			$control
376
				->name($name . '[' . static::FIELD_NUMBER . ']')
377 1
				->id($this->getHtmlId() . '-' . static::FIELD_NUMBER)
378
				->value($this->number)
379
				->type('text')
380
				->{'data-nette-rules'}($input->attrs['data-nette-rules']);
381
382
			if ($this->isDisabled()) {
383
				$control->disabled(TRUE);
384
			}
385
386
			return $control;
387
		}
388
389
		throw new Exceptions\InvalidArgumentException('Part ' . $key . ' does not exist.');
390
	}
391
392
	/**
393
	 * @return NULL
394
	 */
395
	public function getLabelPart()
396
	{
397
		return NULL;
398
	}
399
400
	/**
401 1
	 * @param string $country
402
	 *
403 1
	 * @return string
404 1
	 *
405
	 * @throws Exceptions\NoValidCountryException
406
	 */
407 1
	protected function validateCountry($country)
408
	{
409
		// Country code have to be upper-cased
410
		$country = strtoupper($country);
411
412
		if ((strlen($country) === 2 && ctype_alpha($country) && ctype_upper($country) && in_array($country, $this->phoneUtils->getSupportedCountries())) || $country === 'AUTO') {
413
			return $country;
414
415
		} else {
416
			throw new Exceptions\NoValidCountryException('Provided country code "' . $country . '" is not valid. Provide valid country code or AUTO for automatic detection.');
417
		}
418
	}
419
420
	/**
421 1
	 * @param string $type
422
	 *
423 1
	 * @return string
424 1
	 *
425
	 * @throws Exceptions\NoValidTypeException
426
	 */
427
	protected function validateType($type)
428
	{
429
		// Phone type have to be upper-cased
430
		$type = strtoupper($type);
431
432
		if (defined('\IPub\Phone\Phone::TYPE_' . $type)) {
433
			return $type;
434
435
		} else {
436
			throw new Exceptions\NoValidTypeException('Provided phone type "' . $type . '" is not valid. Provide valid phone type.');
437
		}
438 1
	}
439 1
440
	/**
441
	 * @param PhoneUtils $phoneUtils
442 1
	 * @param string $method
443
	 */
444 1
	public static function register(PhoneUtils $phoneUtils, $method = 'addPhone')
445 1
	{
446 1
		// Check for multiple registration
447 1
		if (self::$registered) {
448 1
			throw new Nette\InvalidStateException('Phone control already registered.');
449
		}
450 1
451
		self::$registered = TRUE;
452 1
453 1
		$class = function_exists('get_called_class') ? get_called_class() : __CLASS__;
454
		Forms\Container::extensionMethod(
455 1
			$method, function (Forms\Container $form, $name, $label = NULL, $maxLength = NULL) use ($class, $phoneUtils) {
456
			$component = new $class($phoneUtils, $label, $maxLength);
457
			$form->addComponent($component, $name);
458
459
			return $component;
460
		}
461
		);
462
	}
463
}
464