Completed
Push — master ( 818dca...c08f34 )
by Propa
03:22
created

PhoneValidator::isInputField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 2
eloc 2
nc 2
nop 1
crap 2
1
<?php namespace Propaganistas\LaravelPhone;
2
3
use libphonenumber\NumberParseException;
4
use libphonenumber\PhoneNumberType;
5
use libphonenumber\PhoneNumberUtil;
6
use Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException;
7
use Propaganistas\LaravelPhone\Exceptions\InvalidParameterException;
8
9
class PhoneValidator
10
{
11
12
	/**
13
	 * @var \libphonenumber\PhoneNumberUtil
14
	 */
15
	protected $lib;
16
17
	/**
18
	 * Dotted validator data.
19
	 *
20
	 * @var array
21
	 */
22
	protected $data;
23
24
	/**
25
	 * Whether the country should be auto-detected.
26
	 *
27
	 * @var bool
28
	 */
29
	protected $autodetect = false;
30
31
	/**
32
	 * Whether to allow lenient checking of numbers (i.e. landline without area codes).
33
	 *
34
	 * @var bool
35
	 */
36
	protected $lenient = false;
37
38
	/**
39
	 * The field to use for country if not passed as a parameter.
40
	 *
41
	 * @var string|null
42
	 */
43
	protected $countryField = null;
44
45
	/**
46
	 * Countries to validate against.
47
	 *
48
	 * @var array
49
	 */
50
	protected $countries = [];
51
52
	/**
53
	 * Transformed phone number types to validate against.
54
	 *
55
	 * @var array
56
	 */
57
	protected $types = [];
58
59
	/**
60
	 * PhoneValidator constructor.
61
	 */
62 27
	public function __construct(PhoneNumberUtil $lib)
63
	{
64 27
		$this->lib = $lib;
65 27
	}
66
67
	/**
68
	 * Validates a phone number.
69
	 *
70
	 * @param  string $attribute
71
	 * @param  mixed  $value
72
	 * @param  array  $parameters
73
	 * @param  object $validator
74
	 * @return bool
75
	 * @throws \Propaganistas\LaravelPhone\Exceptions\InvalidParameterException
76
	 * @throws \Propaganistas\LaravelPhone\Exceptions\NoValidCountryFoundException
77
	 */
78 27
	public function validatePhone($attribute, $value, array $parameters, $validator)
79
	{
80 27
		$this->data = array_dot($validator->getData());
81
82 27
		$this->assignParameters($parameters);
83
84 24
		$this->checkCountries($attribute);
85
86
		// If autodetecting, let's first try without a country.
87
		// Otherwise use provided countries as default.
88 21
		if ($this->autodetect || ($this->lenient && empty($this->countries))) {
89 6
			array_unshift($this->countries, null);
90 4
		}
91
92 21
		foreach ($this->countries as $country) {
93 21
			if ($this->isValidNumber($value, $country)) {
94 21
				return true;
95
			}
96 14
		}
97
98
		// All specified country validations have failed.
99 21
		return false;
100
	}
101
102
	/**
103
	 * Parses the supplied validator parameters.
104
	 *
105
	 * @param array $parameters
106
	 * @throws \Propaganistas\LaravelPhone\Exceptions\InvalidParameterException
107
	 */
108 27
	protected function assignParameters(array $parameters)
109
	{
110 27
		$types = array();
111
112 27
		foreach ($parameters as $parameter) {
113 21
			if ($this->isInputField($parameter)) {
114 3
				$this->countryField = $parameter;
115 2
			} else {
116 21
				$parameter = strtoupper($parameter);
117
118 21
				if ($this->isPhoneCountry($parameter)) {
119 15
					$this->countries[] = $parameter;
120 20
				} elseif ($this->isPhoneType($parameter)) {
121 12
					$types[] = $parameter;
122 15
				} elseif ($parameter == 'AUTO') {
123 3
					$this->autodetect = true;
124 8
				} elseif ($parameter == 'LENIENT') {
125 3
					$this->lenient = true;
126 2
				} else {
127
					// Force developers to write proper code.
128 9
					throw new InvalidParameterException($parameter);
129
				}
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 ($value = $this->isInputField($countryField)) {
150 12
			$this->countries = array($value);
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 and returns the value if applicable.
222
	 * Null otherwise.
223
	 *
224
	 * @param string $field
225
	 * @return mixed|null
226
	 */
227 27
	public function isInputField($field)
228
	{
229 27
		return isset($this->data[$field]) ? $this->data[$field] : null;
230
	}
231
232
	/**
233
	 * Checks if the supplied string is a valid country code using some arbitrary country validation.
234
	 * If using a package based on umpirsky/country-list, invalidate the option 'ZZ => Unknown or invalid region'.
235
	 *
236
	 * @param  string $country
237
	 * @return bool
238
	 */
239 21
	public function isPhoneCountry($country)
240
	{
241 21
		return (strlen($country) === 2 && ctype_alpha($country) && ctype_upper($country) && $country != 'ZZ');
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 18
	public function isPhoneType($type)
251
	{
252
		// Legacy support.
253 18
		$type = ($type == 'LANDLINE' ? 'FIXED_LINE' : $type);
254
255 18
		return defined('\libphonenumber\PhoneNumberType::' . $type);
256
	}
257
258
}
259