Completed
Push — master ( 5fe3af...b86b7f )
by Propa
09:13
created

PhoneValidator::checkCountries()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6

Importance

Changes 5
Bugs 2 Features 1
Metric Value
cc 6
eloc 6
c 5
b 2
f 1
nc 6
nop 1
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 6
rs 8.8571
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 27
	public function __construct(PhoneNumberUtil $lib)
64
	{
65 27
		$this->lib = $lib;
66 27
	}
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 27
	public function validatePhone($attribute, $value, array $parameters, $validator)
80
	{
81 27
		$this->data = array_dot($validator->getData());
82
83 27
		$this->assignParameters($parameters);
84
85 24
		$this->checkCountries($attribute);
86
87
		// If autodetecting, let's first try without a country.
88
		// Otherwise use provided countries as default.
89 21
		if ($this->autodetect || ($this->lenient && empty($this->countries))) {
90 6
			array_unshift($this->countries, null);
91 4
		}
92
93 21
		foreach ($this->countries as $country) {
94 21
			if ($this->isValidNumber($value, $country)) {
95 21
				return true;
96
			}
97 14
		}
98
99
		// All specified country validations have failed.
100 21
		return false;
101
	}
102
103
	/**
104
	 * Parses the supplied validator parameters.
105
	 *
106
	 * @param array $parameters
107
	 * @throws \Propaganistas\LaravelPhone\Exceptions\InvalidParameterException
108
	 */
109 27
	protected function assignParameters(array $parameters)
110
	{
111 27
		$types = array();
112
113 27
		foreach ($parameters as $parameter) {
114
			// First check if the parameter is some phone type configuration.
115 21
			if ($this->isPhoneType($parameter)) {
116 12
				$types[] = strtoupper($parameter);
117 20
			} elseif ($this->isPhoneCountry($parameter)) {
118 15
				$this->countries[] = strtoupper($parameter);
119 16
			} elseif ($parameter == 'AUTO') {
120 3
				$this->autodetect = true;
121 11
			} elseif ($parameter == 'LENIENT') {
122 3
				$this->lenient = true;
123 2
			}
124
			// Lastly check if it is an input field containing the country.
125 6
			elseif ($this->isInputField($parameter)) {
126 3
				$this->countryField = $parameter;
127 2
			} else {
128
				// Force developers to write proper code.
129 9
				throw new InvalidParameterException($parameter);
130
			}
131 18
		}
132
133 24
		$this->types = $this->parseTypes($types);
134
135 24
	}
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 24
	protected function checkCountries($attribute)
146
	{
147 24
		$countryField = (is_null($this->countryField) ? $attribute . '_country' : $this->countryField);
148
149 24
		if ($this->isInputField($countryField)) {
150 12
			$this->countries = array(array_get($this->data, $countryField));
151 21
		} elseif (!$this->autodetect && !$this->lenient && empty($this->countries)) {
152 3
			throw new NoValidCountryFoundException;
153
		}
154 21
	}
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 21
	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 21
			$phoneNumber = $this->lib->parse($number, $country);
169
170
			// Check if type is allowed.
171 21
			if (empty($this->types) || in_array($this->lib->getNumberType($phoneNumber), $this->types)) {
172
173
				// Lenient validation.
174 21
				if ($this->lenient) {
175 3
					return $this->lib->isPossibleNumber($phoneNumber, $country);
176
				}
177
178
				// For automatic detection, the number should have a country code.
179 21
				if ($phoneNumber->hasCountryCode()) {
180
181
					// Automatic detection:
182 21
					if ($this->autodetect) {
183
						// Validate if the international phone number is valid for its contained country.
184 3
						return $this->lib->isValidNumber($phoneNumber);
185
					}
186
187
					// Validate number against the specified country.
188 18
					return $this->lib->isValidNumberForRegion($phoneNumber, $country);
189
				}
190
			}
191
192 12
		} catch (NumberParseException $e) {
193
			// Proceed to default validation error.
194
		}
195
196 15
		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 24
		array_walk($types, function(&$type) {
209 9
			$type = constant('\libphonenumber\PhoneNumberType::' . $type);
210 24
		});
211
212
		// Add in the unsure number type if applicable.
213 24
		if (array_intersect([PhoneNumberType::FIXED_LINE, PhoneNumberType::MOBILE], $types)) {
214 9
			$types[] = PhoneNumberType::FIXED_LINE_OR_MOBILE;
215 6
		}
216
217 24
		return $types;
218
	}
219
220
	/**
221
	 * Checks if the given field is an actual input field.
222
	 *
223
	 * @param string $field
224
	 * @return bool
225
	 */
226 27
	public function isInputField($field)
227
	{
228 27
		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 18
	public function isPhoneCountry($country)
238
	{
239 18
		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 21
	public function isPhoneType($type)
249
	{
250
		// Legacy support.
251 21
		$type = ($type == 'LANDLINE' ? 'FIXED_LINE' : $type);
252
253 21
		return defined('\libphonenumber\PhoneNumberType::' . strtoupper($type));
254
	}
255
256
}
257