PhoneValidator   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 7
dl 0
loc 210
ccs 51
cts 51
cp 1
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
C validatePhone() 0 74 12
A isPhoneCountry() 0 4 4
A isPhoneType() 0 7 2
A determineCountries() 0 19 3
A determineTypes() 0 21 2
A constructPhoneTypeConstant() 0 4 1
A checkLeftoverParameters() 0 9 2
1
<?php
2
/**
3
 * PhoneValidator.php
4
 *
5
 * @copyright      More in license.md
6
 * @license        http://www.ipublikuj.eu
7
 * @author         Adam Kadlec <[email protected]>
8
 * @package        iPublikuj:PhoneUI!
9
 * @subpackage     Forms
10
 * @since          1.0.0
11
 *
12
 * @date           12.12.15
13
 */
14
15
declare(strict_types = 1);
16
17
namespace IPub\PhoneUI\Forms;
18
19
use Nette;
20
use Nette\Forms;
21
22
use libphonenumber;
23
use libphonenumber\PhoneNumberUtil;
24
25
use IPub\PhoneUI\Exceptions;
26
27
/**
28
 * Phone number form field validator
29
 *
30
 * @package        iPublikuj:PhoneUI!
31
 * @subpackage     Forms
32
 *
33
 * @author         Adam Kadlec <[email protected]>
34
 */
35 1
class PhoneValidator
36
{
37
	/**
38
	 * Define validator calling constant
39
	 */
40
	public const PHONE = 'IPub\PhoneUI\Forms\PhoneValidator::validatePhone';
41
42
	/**
43
	 * @param Forms\IControl $control
44
	 * @param array|NULL $params
45
	 *
46
	 * @return bool
47
	 *
48
	 * @throws Exceptions\InvalidParameterException
49
	 * @throws Exceptions\NoValidCountryException
50
	 */
51
	public static function validatePhone(Forms\IControl $control, $params = []) : bool
52
	{
53 1
		if (!$control instanceof Forms\Controls\TextInput) {
54 1
			throw new Exceptions\InvalidArgumentException(sprintf('This validator could be used only on text field. You used it on: "%s"', get_class($control)));
55
		}
56
57 1
		$params = $params === NULL ? [] : $params;
58
59
		// Get form element container
60
		/** @var Forms\Container $container */
61 1
		$container = $control->getParent();
62
63
		// Get form element value
64 1
		$value = $control->getValue();
65
		// Sanitize params
66 1
		$params = array_map('strtoupper', $params);
67
68
		// Check if phone country field exists...
69 1
		if ($countryField = $container->getComponent($control->getName() . '_country', FALSE)) {
70
			// ...use selected value as a list of allowed countries
71 1
			$selectedCountry = $countryField->getValue();
72
73 1
			$allowedCountries = self::isPhoneCountry($selectedCountry) ? [$selectedCountry] : [];
74
75
		} else {
76
			// Get list of allowed countries from params
77 1
			$allowedCountries = self::determineCountries($params);
78
		}
79
80
		// Get list of allowed phone types
81 1
		$allowedTypes = self::determineTypes($params);
82
83
		// Check for leftover parameters
84 1
		self::checkLeftoverParameters($params, $allowedCountries, array_keys($allowedTypes));
85
86
		// Get instance of phone number util
87 1
		$phoneNumberUtil = PhoneNumberUtil::getInstance();
88
89
		// Perform validation
90 1
		foreach ($allowedCountries as $country) {
91
			try {
92
				// For default countries or country field, the following throws NumberParseException if
93
				// not parsed correctly against the supplied country
94
				// For automatic detection: tries to discover the country code using from the number itself
95 1
				$phoneProto = $phoneNumberUtil->parse($value, $country);
96
97
				// For automatic detection, the number should have a country code
98
				// Check if type is allowed
99
				if (
100 1
					$phoneProto->hasCountryCode() &&
101 1
					$allowedTypes === [] ||
102 1
					in_array($phoneNumberUtil->getNumberType($phoneProto), $allowedTypes)
103
				) {
104
					// Automatic detection:
105 1
					if ($country == 'ZZ') {
106
						// Validate if the international phone number is valid for its contained country
107 1
						return $phoneNumberUtil->isValidNumber($phoneProto);
108
					}
109
110
					// Validate number against the specified country. Return only if success
111
					// If failure, continue loop to next specified country
112 1
					if ($phoneNumberUtil->isValidNumberForRegion($phoneProto, $country)) {
113 1
						return TRUE;
114
					}
115
				}
116
117 1
			} catch (libphonenumber\NumberParseException $ex) {
118
				// Proceed to default validation error
119
			}
120
		}
121
122
		// All specified country validations have failed
123 1
		return FALSE;
124
	}
125
126
	/**
127
	 * Checks if the supplied string is a valid country code using some arbitrary country validation
128
	 * If using a package based on umpirsky/country-list, invalidate the option 'ZZ => Unknown or invalid region'
129
	 *
130
	 * @param string $country
131
	 *
132
	 * @return bool
133
	 */
134
	protected static function isPhoneCountry(string $country) : bool
135
	{
136 1
		return (strlen($country) === 2 && ctype_alpha($country) && ctype_upper($country) && $country !== 'ZZ');
137
	}
138
139
	/**
140
	 * Checks if the supplied string is a valid phone number type
141
	 *
142
	 * @param string $type
143
	 *
144
	 * @return bool
145
	 */
146
	protected static function isPhoneType(string $type) : bool
147
	{
148
		// Legacy support.
149 1
		$type = ($type == 'LANDLINE' ? 'FIXED_LINE' : $type);
150
151 1
		return defined(self::constructPhoneTypeConstant($type));
152
	}
153
154
	/**
155
	 * Sets the countries to validate against
156
	 *
157
	 * @param array $params
158
	 *
159
	 * @return array
160
	 *
161
	 * @throws Exceptions\NoValidCountryException
162
	 */
163
	protected static function determineCountries(array $params = []) : array
164
	{
165
		// Check if we need to parse for automatic detection
166 1
		if (in_array('AUTO', $params)) {
167 1
			return ['ZZ'];
168
169
		// Else use the supplied parameters
170
		} else {
171 1
			$allowedCountries = array_filter($params, function ($item) {
172 1
				return self::isPhoneCountry($item);
173 1
			});
174
175 1
			if ($allowedCountries === []) {
176 1
				throw new Exceptions\NoValidCountryException;
177
			}
178
179 1
			return $allowedCountries;
180
		}
181
	}
182
183
	/**
184
	 * Sets the phone number types to validate against
185
	 *
186
	 * @param array $params
187
	 *
188
	 * @return array
189
	 */
190
	protected static function determineTypes(array $params = []) : array
191
	{
192
		// Get phone types
193 1
		$untransformedTypes = array_filter($params, function ($item) {
194 1
			return self::isPhoneType($item);
195 1
		});
196
197
		// Transform valid types to their namespaced class constant
198 1
		$allowedTypes = array_reduce($untransformedTypes, function (array $result, $item) {
199 1
			$result[$item] = constant('\libphonenumber\PhoneNumberType::' . constant(self::constructPhoneTypeConstant($item)));
200
201 1
			return $result;
202 1
		}, []);
203
204
		// Add in the unsure number type if applicable.
205 1
		if (array_intersect(['FIXED_LINE', 'MOBILE'], $params)) {
206 1
			$allowedTypes['FIXED_LINE_OR_MOBILE'] = libphonenumber\PhoneNumberType::FIXED_LINE_OR_MOBILE;
207
		}
208
209 1
		return $allowedTypes;
210
	}
211
212
	/**
213
	 * Constructs the corresponding namespaced class constant for a phone number type
214
	 *
215
	 * @param string $type
216
	 *
217
	 * @return string
218
	 */
219
	protected static function constructPhoneTypeConstant(string $type) : string
220
	{
221 1
		return '\IPub\Phone\Phone::TYPE_' . $type;
222
	}
223
224
	/**
225
	 * Checks for parameter leftovers to force developers to write proper code
226
	 *
227
	 * @param array $params
228
	 * @param array $allowedCountries
229
	 * @param array $allowedTypes
230
	 *
231
	 * @return void
232
	 *
233
	 * @throws Exceptions\InvalidParameterException
234
	 */
235
	protected static function checkLeftoverParameters(array $params = [], array $allowedCountries = [], array $allowedTypes = [])
236
	{
237
		// Remove the automatic detection option if applicable
238 1
		$leftovers = array_diff($params, $allowedCountries, $allowedTypes, ['AUTO']);
239
240 1
		if (!empty($leftovers)) {
241 1
			throw new Exceptions\InvalidParameterException(sprintf('Invalid parameters were sent to the validator: "%s"', implode(', ', $leftovers)));
242
		}
243 1
	}
244
}
245