Completed
Push — master ( 89b72c...f0a079 )
by Propa
06:00
created

PhoneValidator   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 97.18%

Importance

Changes 25
Bugs 6 Features 6
Metric Value
wmc 34
c 25
b 6
f 6
lcom 1
cbo 5
dl 0
loc 249
ccs 69
cts 71
cp 0.9718
rs 9.2

9 Methods

Rating   Name   Duplication   Size   Complexity  
A isPhoneType() 0 7 2
A __construct() 0 4 1
B validatePhone() 0 23 6
C assignParameters() 0 28 7
B checkCountries() 0 10 6
C isValidNumber() 0 35 7
A parseTypes() 0 14 2
A isInputField() 0 4 2
A isPhoneCountry() 0 4 1
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 14
			if ($this->isInputField($parameter)) {
115 2
				$this->countryField = $parameter;
116 2
			} else {
117 14
				$parameter = strtoupper($parameter);
118
119 14
				if ($this->isPhoneCountry($parameter)) {
120 11
					$this->countries[] = $parameter;
121 14
				} elseif ($this->isPhoneType($parameter)) {
122 9
					$types[] = $parameter;
123 13
				} elseif ($parameter == 'AUTO') {
124 2
					$this->autodetect = true;
125 7
				} elseif ($parameter == 'LENIENT') {
126 2
					$this->lenient = true;
127 2
				} else {
128
					// Force developers to write proper code.
129 2
					throw new InvalidParameterException($parameter);
130
				}
131
			}
132 19
		}
133
134 16
		$this->types = $this->parseTypes($types);
135
136 16
	}
137
138
	/**
139
	 * Checks the detected countries. Overrides countries if a country field is present.
140
	 * When using a country field, we should validate to false if country is empty so no exception
141
	 * will be thrown.
142
	 *
143
	 * @param string $attribute
144
	 * @throws \Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException
145
	 */
146 17
	protected function checkCountries($attribute)
147
	{
148 16
		$countryField = (is_null($this->countryField) ? $attribute . '_country' : $this->countryField);
149
150 16
		if ($value = $this->isInputField($countryField)) {
151 8
			$this->countries = array($value);
152 17
		} elseif (!$this->autodetect && !$this->lenient && empty($this->countries)) {
153 2
			throw new NoValidCountryFoundException;
154
		}
155 14
	}
156
157
	/**
158
	 * Performs the actual validation of the phone number.
159
	 *
160
	 * @param mixed $number
161
	 * @param null  $country
162
	 * @return bool
163
	 */
164 14
	protected function isValidNumber($number, $country = null)
165
	{
166
		try {
167
			// Throws NumberParseException if not parsed correctly against the supplied country.
168
			// If no country was given, tries to discover the country code from the number itself.
169 14
			$phoneNumber = $this->lib->parse($number, $country);
170
171
			// Check if type is allowed.
172 14
			if (empty($this->types) || in_array($this->lib->getNumberType($phoneNumber), $this->types)) {
173
174
				// Lenient validation.
175 14
				if ($this->lenient) {
176 2
					return $this->lib->isPossibleNumber($phoneNumber, $country);
177
				}
178
179
				// For automatic detection, the number should have a country code.
180 14
				if ($phoneNumber->hasCountryCode()) {
181
182
					// Automatic detection:
183 14
					if ($this->autodetect) {
184
						// Validate if the international phone number is valid for its contained country.
185 2
						return $this->lib->isValidNumber($phoneNumber);
186
					}
187
188
					// Validate number against the specified country.
189 12
					return $this->lib->isValidNumberForRegion($phoneNumber, $country);
190
				}
191
			}
192
193 10
		} catch (NumberParseException $e) {
194
			// Proceed to default validation error.
195
		}
196
197 10
		return false;
198
	}
199
200
	/**
201
	 * Parses the supplied phone number types.
202
	 *
203
	 * @param array $types
204
	 * @return array
205
	 */
206
	protected function parseTypes(array $types)
207
	{
208
		// Transform types to their namespaced class constant.
209 16
		array_walk($types, function(&$type) {
210 6
			$type = constant('\libphonenumber\PhoneNumberType::' . $type);
211 16
		});
212
213
		// Add in the unsure number type if applicable.
214 16
		if (array_intersect([PhoneNumberType::FIXED_LINE, PhoneNumberType::MOBILE], $types)) {
215 6
			$types[] = PhoneNumberType::FIXED_LINE_OR_MOBILE;
216 6
		}
217
218 17
		return $types;
219 1
	}
220
221
	/**
222
	 * Checks if the given field is an actual input field and returns the value if applicable.
223
	 * Null otherwise.
224
	 *
225
	 * @param string $field
226
	 * @return mixed|null
227
	 */
228 19
	public function isInputField($field)
229
	{
230 19
		return isset($this->data[$field]) ? $this->data[$field] : null;
231
	}
232
233
	/**
234
	 * Checks if the supplied string is a valid country code.
235
	 *
236
	 * @param  string $country
237
	 * @return bool
238
	 */
239 14
	public function isPhoneCountry($country)
240
	{
241 14
		return ISO3166::isValid($country);
242
	}
243
244
	/**
245
	 * Checks if the supplied string is a valid phone number type.
246
	 *
247
	 * @param  string $type
248
	 * @return bool
249
	 */
250 12
	public function isPhoneType($type)
251
	{
252
		// Legacy support.
253 12
		$type = ($type == 'LANDLINE' ? 'FIXED_LINE' : $type);
254
255 12
		return defined('\libphonenumber\PhoneNumberType::' . $type);
256
	}
257
258
}
259