Completed
Push — master ( fbb0e0...5fe3af )
by Propa
03:28
created

PhoneValidator   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 97.14%

Importance

Changes 27
Bugs 8 Features 6
Metric Value
c 27
b 8
f 6
dl 0
loc 247
ccs 68
cts 70
cp 0.9714
rs 9.3999
wmc 33
lcom 1
cbo 5

9 Methods

Rating   Name   Duplication   Size   Complexity  
C isValidNumber() 0 35 7
A __construct() 0 4 1
B validatePhone() 0 23 6
C assignParameters() 0 27 7
B checkCountries() 0 10 6
A parseTypes() 0 14 2
A isInputField() 0 4 1
A isPhoneCountry() 0 4 1
A isPhoneType() 0 7 2
1
<?php namespace Propaganistas\LaravelPhone;
2
3
use Iso3166\Codes as ISO3166;
4
use libphonenumber\NumberParseException;
5
use libphonenumber\PhoneNumberType;
6
use libphonenumber\PhoneNumberUtil;
7
use Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException;
8
use Propaganistas\LaravelPhone\Exceptions\InvalidParameterException;
9
10
class PhoneValidator
11
{
12
13
	/**
14
	 * @var \libphonenumber\PhoneNumberUtil
15
	 */
16
	protected $lib;
17
18
	/**
19
	 * Dotted validator data.
20
	 *
21
	 * @var array
22
	 */
23
	protected $data;
24
25
	/**
26
	 * Whether the country should be auto-detected.
27
	 *
28
	 * @var bool
29
	 */
30
	protected $autodetect = false;
31
32
	/**
33
	 * Whether to allow lenient checking of numbers (i.e. landline without area codes).
34
	 *
35
	 * @var bool
36
	 */
37
	protected $lenient = false;
38
39
	/**
40
	 * The field to use for country if not passed as a parameter.
41
	 *
42
	 * @var string|null
43
	 */
44
	protected $countryField = null;
45
46
	/**
47
	 * Countries to validate against.
48
	 *
49
	 * @var array
50
	 */
51
	protected $countries = [];
52
53
	/**
54
	 * Transformed phone number types to validate against.
55
	 *
56
	 * @var array
57
	 */
58
	protected $types = [];
59
60
	/**
61
	 * PhoneValidator constructor.
62
	 */
63 20
	public function __construct(PhoneNumberUtil $lib)
64
	{
65 20
		$this->lib = $lib;
66 18
	}
67
68
	/**
69
	 * Validates a phone number.
70
	 *
71
	 * @param  string $attribute
72
	 * @param  mixed  $value
73
	 * @param  array  $parameters
74
	 * @param  object $validator
75
	 * @return bool
76
	 * @throws \Propaganistas\LaravelPhone\Exceptions\InvalidParameterException
77
	 * @throws \Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException
78
	 */
79 20
	public function validatePhone($attribute, $value, array $parameters, $validator)
80
	{
81 18
		$this->data = array_dot($validator->getData());
82
83 19
		$this->assignParameters($parameters);
84
85 17
		$this->checkCountries($attribute);
86
87
		// If autodetecting, let's first try without a country.
88
		// Otherwise use provided countries as default.
89 14
		if ($this->autodetect || ($this->lenient && empty($this->countries))) {
90 4
			array_unshift($this->countries, null);
91 4
		}
92
93 14
		foreach ($this->countries as $country) {
94 14
			if ($this->isValidNumber($value, $country)) {
95 14
				return true;
96
			}
97 14
		}
98
99
		// All specified country validations have failed.
100 14
		return false;
101 2
	}
102
103
	/**
104
	 * Parses the supplied validator parameters.
105
	 *
106
	 * @param array $parameters
107
	 * @throws \Propaganistas\LaravelPhone\Exceptions\InvalidParameterException
108
	 */
109 20
	protected function assignParameters(array $parameters)
110
	{
111 20
		$types = array();
112
113 18
		foreach ($parameters as $parameter) {
114
			// First check if the parameter is some phone type configuration.
115 14
			if ($this->isPhoneType($parameter)) {
116 8
				$types[] = strtoupper($parameter);
117 14
			} elseif ($this->isPhoneCountry($parameter)) {
118 10
				$this->countries[] = strtoupper($parameter);
119 13
			} elseif ($parameter == 'AUTO') {
120 2
				$this->autodetect = true;
121 9
			} elseif ($parameter == 'LENIENT') {
122 2
				$this->lenient = true;
123 2
			}
124
			// Lastly check if it is an input field containing the country.
125 4
			elseif ($this->isInputField($parameter)) {
126 2
				$this->countryField = $parameter;
127 2
			} else {
128
				// Force developers to write proper code.
129 2
				throw new InvalidParameterException($parameter);
130
			}
131 19
		}
132
133 16
		$this->types = $this->parseTypes($types);
134
135 16
	}
136
137
	/**
138
	 * Checks the detected countries. Overrides countries if a country field is present.
139
	 * When using a country field, we should validate to false if country is empty so no exception
140
	 * will be thrown.
141
	 *
142
	 * @param string $attribute
143
	 * @throws \Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException
144
	 */
145 17
	protected function checkCountries($attribute)
146
	{
147 16
		$countryField = (is_null($this->countryField) ? $attribute . '_country' : $this->countryField);
148
149 16
		if ($this->isInputField($countryField)) {
150 8
			$this->countries = array(array_get($this->data, $countryField));
151 17
		} elseif (!$this->autodetect && !$this->lenient && empty($this->countries)) {
152 2
			throw new NoValidCountryFoundException;
153
		}
154 14
	}
155
156
	/**
157
	 * Performs the actual validation of the phone number.
158
	 *
159
	 * @param mixed $number
160
	 * @param null  $country
161
	 * @return bool
162
	 */
163 14
	protected function isValidNumber($number, $country = null)
164
	{
165
		try {
166
			// Throws NumberParseException if not parsed correctly against the supplied country.
167
			// If no country was given, tries to discover the country code from the number itself.
168 14
			$phoneNumber = $this->lib->parse($number, $country);
169
170
			// Check if type is allowed.
171 14
			if (empty($this->types) || in_array($this->lib->getNumberType($phoneNumber), $this->types)) {
172
173
				// Lenient validation.
174 14
				if ($this->lenient) {
175 2
					return $this->lib->isPossibleNumber($phoneNumber, $country);
176
				}
177
178
				// For automatic detection, the number should have a country code.
179 14
				if ($phoneNumber->hasCountryCode()) {
180
181
					// Automatic detection:
182 14
					if ($this->autodetect) {
183
						// Validate if the international phone number is valid for its contained country.
184 2
						return $this->lib->isValidNumber($phoneNumber);
185
					}
186
187
					// Validate number against the specified country.
188 12
					return $this->lib->isValidNumberForRegion($phoneNumber, $country);
189
				}
190
			}
191
192 10
		} catch (NumberParseException $e) {
193
			// Proceed to default validation error.
194
		}
195
196 10
		return false;
197
	}
198
199
	/**
200
	 * Parses the supplied phone number types.
201
	 *
202
	 * @param array $types
203
	 * @return array
204
	 */
205
	protected function parseTypes(array $types)
206
	{
207
		// Transform types to their namespaced class constant.
208 16
		array_walk($types, function(&$type) {
209 6
			$type = constant('\libphonenumber\PhoneNumberType::' . $type);
210 16
		});
211
212
		// Add in the unsure number type if applicable.
213 16
		if (array_intersect([PhoneNumberType::FIXED_LINE, PhoneNumberType::MOBILE], $types)) {
214 6
			$types[] = PhoneNumberType::FIXED_LINE_OR_MOBILE;
215 6
		}
216
217 17
		return $types;
218 1
	}
219
220
	/**
221
	 * Checks if the given field is an actual input field.
222
	 *
223
	 * @param string $field
224
	 * @return bool
225
	 */
226 18
	public function isInputField($field)
227
	{
228 18
		return !is_null(array_get($this->data, $field));
229
	}
230
231
	/**
232
	 * Checks if the supplied string is a valid country code.
233
	 *
234
	 * @param  string $country
235
	 * @return bool
236
	 */
237 12
	public function isPhoneCountry($country)
238
	{
239 12
		return ISO3166::isValid(strtoupper($country));
240
	}
241
242
	/**
243
	 * Checks if the supplied string is a valid phone number type.
244
	 *
245
	 * @param  string $type
246
	 * @return bool
247
	 */
248 14
	public function isPhoneType($type)
249
	{
250
		// Legacy support.
251 14
		$type = ($type == 'LANDLINE' ? 'FIXED_LINE' : $type);
252
253 14
		return defined('\libphonenumber\PhoneNumberType::' . strtoupper($type));
254
	}
255
256
}
257