Passed
Push — master ( 042452...c272a3 )
by Paul
05:20
created

Validator::parseRule()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 10
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
crap 6
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use BadMethodCallException;
6
use GeminiLabs\SiteReviews\Helper;
7
use GeminiLabs\SiteReviews\Modules\Validator\ValidationRules;
8
use InvalidArgumentException;
9
10
/**
11
 * @see \Illuminate\Validation\Validator (5.3)
12
 */
13
class Validator
14
{
15
	use ValidationRules;
16
17
	/**
18
	 * @var array
19
	 */
20
	public $errors = [];
21
22
	/**
23
	 * The data under validation.
24
	 * @var array
25
	 */
26
	protected $data = [];
27
28
	/**
29
	 * The failed validation rules.
30
	 * @var array
31
	 */
32
	protected $failedRules = [];
33
34
	/**
35
	 * The rules to be applied to the data.
36
	 * @var array
37
	 */
38
	protected $rules = [];
39
40
	/**
41
	 * The size related validation rules.
42
	 * @var array
43
	 */
44
	protected $sizeRules = [
45
		'Between', 'Max', 'Min',
46
	];
47
48
	/**
49
	 * The validation rules that imply the field is required.
50
	 * @var array
51
	 */
52
	protected $implicitRules = [
53
		'Required',
54
	];
55
56
	/**
57
	 * The numeric related validation rules.
58
	 * @var array
59
	 */
60
	protected $numericRules = [
61
		'Numeric',
62
	];
63
64
	/**
65
	 * Run the validator's rules against its data.
66
	 * @param mixed $data
67
	 * @return array
68
	 */
69
	public function validate( $data, array $rules = [] )
70
	{
71
		$this->normalizeData( $data );
72
		$this->setRules( $rules );
73
		foreach( $this->rules as $attribute => $rules ) {
74
			foreach( $rules as $rule ) {
75
				$this->validateAttribute( $attribute, $rule );
76
				if( $this->shouldStopValidating( $attribute ))break;
77
			}
78
		}
79
		return $this->errors;
80
	}
81
82
	/**
83
	 * Validate a given attribute against a rule.
84
	 * @param string $attribute
85
	 * @param string $rule
86
	 * @return void
87
	 * @throws BadMethodCallException
88
	 */
89
	public function validateAttribute( $attribute, $rule )
90
	{
91
		list( $rule, $parameters ) = $this->parseRule( $rule );
92
		if( $rule == '' )return;
93
		$value = $this->getValue( $attribute );
94
		if( !method_exists( $this, $method = 'validate'.$rule )) {
95
			throw new BadMethodCallException( "Method [$method] does not exist." );
96
		}
97
		if( !$this->$method( $attribute, $value, $parameters )) {
98
			$this->addFailure( $attribute, $rule, $parameters );
99
		}
100
	}
101
102
	/**
103
	 * Add an error message to the validator's collection of errors.
104
	 * @param string $attribute
105
	 * @param string $rule
106
	 * @return void
107
	 */
108
	protected function addError( $attribute, $rule, array $parameters )
109
	{
110
		$message = $this->getMessage( $attribute, $rule, $parameters );
111
		$this->errors[$attribute][] = $message;
112
	}
113
114
	/**
115
	 * Add a failed rule and error message to the collection.
116
	 * @param string $attribute
117
	 * @param string $rule
118
	 * @return void
119
	 */
120
	protected function addFailure( $attribute, $rule, array $parameters )
121
	{
122
		$this->addError( $attribute, $rule, $parameters );
123
		$this->failedRules[$attribute][$rule] = $parameters;
124
	}
125
126
	/**
127
	 * Get the data type of the given attribute.
128
	 * @param string $attribute
129
	 * @return string
130
	 */
131
	protected function getAttributeType( $attribute )
132
	{
133
		return $this->hasRule( $attribute, $this->numericRules )
134
			? 'numeric'
135
			: 'string';
136
	}
137
138
	/**
139
	 * Get the validation message for an attribute and rule.
140
	 * @param string $attribute
141
	 * @param string $rule
142
	 * @return string|null
143
	 */
144
	protected function getMessage( $attribute, $rule, array $parameters )
145
	{
146
		if( in_array( $rule, $this->sizeRules )) {
147
			return $this->getSizeMessage( $attribute, $rule, $parameters );
148
		}
149
		$lowerRule = glsr( Helper::class )->snakeCase( $rule );
150
		return $this->translator( $lowerRule, $rule, $parameters );
151
	}
152
153
	/**
154
	 * Get a rule and its parameters for a given attribute.
155
	 * @param string $attribute
156
	 * @param string|array $rules
157
	 * @return array|null
158
	 */
159
	protected function getRule( $attribute, $rules )
160
	{
161
		if( !array_key_exists( $attribute, $this->rules ))return;
162
		$rules = (array) $rules;
163
		foreach( $this->rules[$attribute] as $rule ) {
164
			list( $rule, $parameters ) = $this->parseRule( $rule );
165
			if( in_array( $rule, $rules )) {
166
				return [$rule, $parameters];
167
			}
168
		}
169
	}
170
171
	/**
172
	 * Get the size of an attribute.
173
	 * @param string $attribute
174
	 * @param mixed $value
175
	 * @return mixed
176
	 */
177
	protected function getSize( $attribute, $value )
178
	{
179
		$hasNumeric = $this->hasRule( $attribute, $this->numericRules );
180
		if( is_numeric( $value ) && $hasNumeric ) {
181
			return $value;
182
		}
183
		elseif( is_array( $value )) {
184
			return count( $value );
185
		}
186
		return mb_strlen( $value );
187
	}
188
189
	/**
190
	 * Get the proper error message for an attribute and size rule.
191
	 * @param string $attribute
192
	 * @param string $rule
193
	 * @return string|null
194
	 */
195
	protected function getSizeMessage( $attribute, $rule, array $parameters )
196
	{
197
		$lowerRule = glsr( Helper::class )->snakeCase( $rule );
198
		$type = $this->getAttributeType( $attribute );
199
		$lowerRule .= '.'.$type;
200
		return $this->translator( $lowerRule, $rule, $parameters );
201
	}
202
203
	/**
204
	 * Get the value of a given attribute.
205
	 * @param string $attribute
206
	 * @return mixed
207
	 */
208
	protected function getValue( $attribute )
209
	{
210
		if( isset( $this->data[$attribute] )) {
211
			return $this->data[$attribute];
212
		}
213
	}
214
215
	/**
216
	 * Determine if the given attribute has a rule in the given set.
217
	 * @param string $attribute
218
	 * @param string|array $rules
219
	 * @return bool
220
	 */
221
	protected function hasRule( $attribute, $rules )
222
	{
223
		return !is_null( $this->getRule( $attribute, $rules ));
224
	}
225
226
	/**
227
	 * Normalize the provided data to an array.
228
	 * @param mixed $data
229
	 * @return void
230
	 */
231
	protected function normalizeData( $data )
232
	{
233
		$this->data = is_object( $data )
234
			? get_object_vars( $data )
235
			: $data;
236
	}
237
238
	/**
239
	 * Parse a parameter list.
240
	 * @param string $rule
241
	 * @param string $parameter
242
	 * @return array
243
	 */
244
	protected function parseParameters( $rule, $parameter )
245
	{
246
		if( strtolower( $rule ) == 'regex' ) {
247
			return [$parameter];
248
		}
249
		return str_getcsv( $parameter );
250
	}
251
252
	/**
253
	 * Extract the rule name and parameters from a rule.
254
	 * @param string $rule
255
	 * @return array
256
	 */
257
	protected function parseRule( $rule )
258
	{
259
		$parameters = [];
260
		// example: {rule}:{parameters}
1 ignored issue
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
261
		if( strpos( $rule, ':' ) !== false ) {
262
			list( $rule, $parameter ) = explode( ':', $rule, 2 );
263
			// example: {parameter1,parameter2,...}
1 ignored issue
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
264
			$parameters = $this->parseParameters( $rule, $parameter );
265
		}
266
		$rule = ucwords( str_replace( ['-', '_'], ' ', trim( $rule )));
267
		$rule = str_replace( ' ', '', $rule );
268
		return [$rule, $parameters];
269
	}
270
271
	/**
272
	 * Set the validation rules.
273
	 * @return void
274
	 */
275
	protected function setRules( array $rules )
276
	{
277
		foreach( $rules as $key => $rule ) {
278
			$rules[$key] = is_string( $rule )
279
				? explode( '|', $rule )
280
				: $rule;
281
		}
282
		$this->rules = $rules;
283
	}
284
285
	/**
286
	 * Check if we should stop further validations on a given attribute.
287
	 * @param string $attribute
288
	 * @return bool
289
	 */
290
	protected function shouldStopValidating( $attribute )
291
	{
292
		return $this->hasRule( $attribute, $this->implicitRules )
293
			&& isset( $this->failedRules[$attribute] )
294
			&& array_intersect( array_keys( $this->failedRules[$attribute] ), $this->implicitRules );
295
	}
296
297
	/**
298
	 * Returns a translated message for the attribute
299
	 * @param string $key
300
	 * @param string $rule
301
	 * @param string $attribute
302
	 * @return string|null
303
	 */
304
	protected function translator( $key, $rule, array $parameters )
305
	{
306
		$strings = [
307
			'accepted' => __( 'This field must be accepted.', 'site-reviews' ),
308
			'between.numeric' => _x( 'This field must be between :min and :max.', ':min, and :max are placeholders and should not be translated.', 'site-reviews' ),
309
			'between.string' => _x( 'This field must be between :min and :max characters.', ':min, and :max are placeholders and should not be translated.', 'site-reviews' ),
310
			'email' => __( 'This must be a valid email address.', 'site-reviews' ),
311
			'max.numeric' => _x( 'This field may not be greater than :max.', ':max is a placeholder and should not be translated.', 'site-reviews' ),
312
			'max.string' => _x( 'This field may not be greater than :max characters.', ':max is a placeholder and should not be translated.', 'site-reviews' ),
313
			'min.numeric' => _x( 'This field must be at least :min.', ':min is a placeholder and should not be translated.', 'site-reviews' ),
314
			'min.string' => _x( 'This field must be at least :min characters.', ':min is a placeholder and should not be translated.', 'site-reviews' ),
315
			'regex' => __( 'The format is invalid.', 'site-reviews' ),
316
			'required' => __( 'This field is required.', 'site-reviews' ),
317
		];
318
		$message = isset( $strings[$key] )
319
			? $strings[$key]
320
			: false;
321
		if( !$message )return;
322
		if( method_exists( $this, $method = 'replace'.$rule )) {
323
			$message = $this->$method( $message, $parameters );
324
		}
325
		return $message;
326
	}
327
}
328