Passed
Push — develop ( c6dd2a...1f8334 )
by Paul
03:38
created

Validator   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 557
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 557
rs 3.8461
c 0
b 0
f 0
wmc 62

29 Methods

Rating   Name   Duplication   Size   Complexity  
A parseParameters() 0 7 2
A validateNumeric() 0 3 1
A parseRule() 0 16 2
A validateAccepted() 0 5 2
A validateAttribute() 0 14 4
A shouldStopValidating() 0 5 3
A validateMin() 0 5 1
A getSize() 0 12 4
A replaceBetween() 0 3 1
A translator() 0 17 4
A getValue() 0 4 2
A setRules() 0 9 3
A snakeCase() 0 9 2
A getRule() 0 11 4
A replaceMin() 0 3 1
A addError() 0 8 2
A hasRule() 0 3 1
A getMessage() 0 9 2
A validateEmail() 0 3 1
A getAttributeType() 0 5 2
A validate() 0 14 4
A validateMax() 0 5 1
A replaceMax() 0 3 1
A getSizeMessage() 0 8 1
A normalizeData() 0 11 2
A validateBetween() 0 7 2
A requireParameterCount() 0 4 2
A addFailure() 0 5 1
A validateRequired() 0 8 4

How to fix   Complexity   

Complex Class

Complex classes like Validator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Validator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GeminiLabs\Castor\Services;
4
5
use BadMethodCallException;
6
use InvalidArgumentException;
7
8
class Validator
9
{
10
	/**
11
	 * @var array
12
	 */
13
	public $errors = [];
14
15
	/**
16
	 * The data under validation.
17
	 *
18
	 * @var array
19
	 */
20
	protected $data = [];
21
22
	/**
23
	 * The failed validation rules.
24
	 *
25
	 * @var array
26
	 */
27
	protected $failedRules = [];
28
29
	/**
30
	 * The rules to be applied to the data.
31
	 *
32
	 * @var array
33
	 */
34
	protected $rules = [];
35
36
	/**
37
	 * The size related validation rules.
38
	 *
39
	 * @var array
40
	 */
41
	protected $sizeRules = [
42
		'Between',
43
		'Max',
44
		'Min',
45
	];
46
47
	/**
48
	 * The validation rules that imply the field is required.
49
	 *
50
	 * @var array
51
	 */
52
	protected $implicitRules = [
53
		'Required',
54
	];
55
56
	/**
57
	 * The numeric related validation rules.
58
	 *
59
	 * @var array
60
	 */
61
	protected $numericRules = [
62
		'Numeric',
63
	];
64
65
	/**
66
	 * Run the validator's rules against its data.
67
	 *
68
	 * @param mixed $data
69
	 *
70
	 * @return array
71
	 */
72
	public function validate( $data, array $rules = [] )
73
	{
74
		$this->normalizeData( $data );
75
		$this->setRules( $rules );
76
77
		foreach( $this->rules as $attribute => $rules ) {
78
			foreach( $rules as $rule ) {
79
				$this->validateAttribute( $rule, $attribute );
80
81
				if( $this->shouldStopValidating( $attribute ))break;
82
			}
83
		}
84
85
		return $this->errors;
86
	}
87
88
	/**
89
	 * Add an error message to the validator's collection of errors.
90
	 *
91
	 * @param string $attribute
92
	 * @param string $rule
93
	 *
94
	 * @return void
95
	 */
96
	protected function addError( $attribute, $rule, array $parameters )
97
	{
98
		$message = $this->getMessage( $attribute, $rule, $parameters );
99
100
		$this->errors[ $attribute ]['errors'][] = $message;
101
102
		if( !isset( $this->errors[ $attribute ]['value'] )) {
103
			$this->errors[ $attribute ]['value'] = $this->getValue( $attribute );
104
		}
105
	}
106
107
	/**
108
	 * Add a failed rule and error message to the collection.
109
	 *
110
	 * @param string $attribute
111
	 * @param string $rule
112
	 *
113
	 * @return void
114
	 */
115
	protected function addFailure( $attribute, $rule, array $parameters )
116
	{
117
		$this->addError( $attribute, $rule, $parameters );
118
119
		$this->failedRules[ $attribute ][ $rule ] = $parameters;
120
	}
121
122
	/**
123
	 * Get the data type of the given attribute.
124
	 *
125
	 * @param  string  $attribute
126
	 * @return string
127
	 */
128
	protected function getAttributeType( $attribute )
129
	{
130
		return $this->hasRule( $attribute, $this->numericRules )
131
			? 'numeric'
132
			: 'string';
133
	}
134
135
	/**
136
	 * Get the validation message for an attribute and rule.
137
	 *
138
	 * @param string $attribute
139
	 * @param string $rule
140
	 *
141
	 * @return string|null
142
	 */
143
	protected function getMessage( $attribute, $rule, array $parameters )
144
	{
145
		if( in_array( $rule, $this->sizeRules )) {
146
			return $this->getSizeMessage( $attribute, $rule, $parameters );
147
		}
148
149
		$lowerRule = $this->snakeCase( $rule );
150
151
		return $this->translator( $lowerRule, $rule, $attribute, $parameters );
152
	}
153
154
	/**
155
	 * Get a rule and its parameters for a given attribute.
156
	 *
157
	 * @param string       $attribute
158
	 * @param string|array $rules
159
	 *
160
	 * @return array|null
161
	 */
162
	protected function getRule( $attribute, $rules )
163
	{
164
		if( !array_key_exists( $attribute, $this->rules ))return;
165
166
		$rules = (array) $rules;
167
168
		foreach( $this->rules[ $attribute ] as $rule ) {
169
			list( $rule, $parameters ) = $this->parseRule( $rule );
170
171
			if( in_array( $rule, $rules )) {
172
				return [ $rule, $parameters ];
173
			}
174
		}
175
	}
176
177
	/**
178
	 * Get the size of an attribute.
179
	 *
180
	 * @param string $attribute
181
	 * @param mixed  $value
182
	 *
183
	 * @return mixed
184
	 */
185
	protected function getSize( $attribute, $value )
186
	{
187
		$hasNumeric = $this->hasRule( $attribute, $this->numericRules );
188
189
		if( is_numeric( $value ) && $hasNumeric ) {
190
			return $value;
191
		}
192
		elseif( is_array( $value )) {
193
			return count( $value );
194
		}
195
196
		return mb_strlen( $value );
197
	}
198
199
	/**
200
	 * Get the proper error message for an attribute and size rule.
201
	 *
202
	 * @param string $attribute
203
	 * @param string $rule
204
	 *
205
	 * @return string|null
206
	 */
207
	protected function getSizeMessage( $attribute, $rule, array $parameters )
208
	{
209
		$lowerRule = $this->snakeCase( $rule );
210
		$type = $this->getAttributeType( $attribute );
211
212
		$lowerRule .= ".{$type}";
213
214
		return $this->translator( $lowerRule, $rule, $attribute, $parameters );
215
	}
216
217
	/**
218
	 * Get the value of a given attribute.
219
	 *
220
	 * @param string $attribute
221
	 *
222
	 * @return mixed
223
	 */
224
	protected function getValue( $attribute )
225
	{
226
		if( isset( $this->data[ $attribute ] )) {
227
			return $this->data[ $attribute ];
228
		}
229
	}
230
231
	/**
232
	 * Determine if the given attribute has a rule in the given set.
233
	 *
234
	 * @param string       $attribute
235
	 * @param string|array $rules
236
	 *
237
	 * @return bool
238
	 */
239
	protected function hasRule( $attribute, $rules )
240
	{
241
		return !is_null( $this->getRule( $attribute, $rules ));
242
	}
243
244
	/**
245
	 * Normalize the provided data to an array.
246
	 *
247
	 * @param mixed $data
248
	 *
249
	 * @return $this
250
	 */
251
	protected function normalizeData( $data )
252
	{
253
		// If an object was provided, get its public properties
254
		if( is_object( $data )) {
255
			$this->data = get_object_vars( $data );
256
		}
257
		else {
258
			$this->data = $data;
259
		}
260
261
		return $this;
262
	}
263
264
	/**
265
	 * Parse a parameter list.
266
	 *
267
	 * @param string $rule
268
	 * @param string $parameter
269
	 *
270
	 * @return array
271
	 */
272
	protected function parseParameters( $rule, $parameter )
273
	{
274
		if( strtolower( $rule ) == 'regex' ) {
275
			return [ $parameter ];
276
		}
277
278
		return str_getcsv( $parameter );
279
	}
280
281
	/**
282
	 * Extract the rule name and parameters from a rule.
283
	 *
284
	 * @param string $rule
285
	 *
286
	 * @return array
287
	 */
288
	protected function parseRule( $rule )
289
	{
290
		$parameters = [];
291
292
		// example: {rule}:{parameters}
0 ignored issues
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...
293
		if( strpos( $rule, ':' ) !== false ) {
294
			list( $rule, $parameter ) = explode( ':', $rule, 2 );
295
296
			// example: {parameter1,parameter2,...}
0 ignored issues
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...
297
			$parameters = $this->parseParameters( $rule, $parameter );
298
		}
299
300
		$rule = ucwords( str_replace( ['-', '_'], ' ', trim( $rule )));
301
		$rule = str_replace( ' ', '', $rule );
302
303
		return [ $rule, $parameters ];
304
	}
305
306
	/**
307
	 * Replace all placeholders for the between rule.
308
	 *
309
	 * @param string $message
310
	 *
311
	 * @return string
312
	 */
313
	protected function replaceBetween( $message, array $parameters )
314
	{
315
		return str_replace([':min', ':max'], $parameters, $message );
316
	}
317
318
	/**
319
	 * Replace all placeholders for the max rule.
320
	 *
321
	 * @param string $message
322
	 *
323
	 * @return string
324
	 */
325
	protected function replaceMax( $message, array $parameters )
326
	{
327
		return str_replace( ':max', $parameters[0], $message );
328
	}
329
330
	/**
331
	 * Replace all placeholders for the min rule.
332
	 *
333
	 * @param string $message
334
	 *
335
	 * @return string
336
	 */
337
	protected function replaceMin( $message, array $parameters )
338
	{
339
		return str_replace( ':min', $parameters[0], $message );
340
	}
341
342
	/**
343
	 * Require a certain number of parameters to be present.
344
	 *
345
	 * @param int    $count
346
	 * @param string $rule
347
	 *
348
	 * @return void
349
	 * @throws InvalidArgumentException
350
	 */
351
	protected function requireParameterCount( $count, array $parameters, $rule )
352
	{
353
		if( count( $parameters ) < $count ) {
354
			throw new InvalidArgumentException( "Validation rule $rule requires at least $count parameters." );
355
		}
356
	}
357
358
	/**
359
	 * Set the validation rules.
360
	 *
361
	 * @return $this
362
	 */
363
	protected function setRules( array $rules )
364
	{
365
		foreach( $rules as $key => $rule ) {
366
			$rules[ $key ] = is_string( $rule ) ? explode( '|', $rule ) : $rule;
367
		}
368
369
		$this->rules = $rules;
370
371
		return $this;
372
	}
373
374
	/**
375
	 * Check if we should stop further validations on a given attribute.
376
	 *
377
	 * @param string $attribute
378
	 *
379
	 * @return bool
380
	 */
381
	protected function shouldStopValidating( $attribute )
382
	{
383
		return $this->hasRule( $attribute, $this->implicitRules )
384
			&& isset( $this->failedRules[ $attribute ] )
385
			&& array_intersect( array_keys( $this->failedRules[ $attribute ] ), $this->implicitRules );
386
	}
387
388
	/**
389
	 * Convert a string to snake case.
390
	 *
391
	 * @param string $string
392
	 *
393
	 * @return string
394
	 */
395
	protected function snakeCase( $string )
396
	{
397
		if( !ctype_lower( $string )) {
398
			$string = preg_replace( '/\s+/u', '', $string );
399
			$string = preg_replace( '/(.)(?=[A-Z])/u', '$1_', $string );
400
			$string = mb_strtolower( $string, 'UTF-8' );
401
		}
402
403
		return $string;
404
	}
405
406
	/**
407
	 * Returns a translated message for the attribute
408
	 *
409
	 * @param string $key
410
	 * @param string $rule
411
	 * @param string $attribute
412
	 *
413
	 * @return string|null
414
	 */
415
	protected function translator( $key, $rule, $attribute, array $parameters )
416
	{
417
		$strings = glsr_resolve( 'Strings' )->validation();
0 ignored issues
show
Bug introduced by
The function glsr_resolve was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

417
		$strings = /** @scrutinizer ignore-call */ glsr_resolve( 'Strings' )->validation();
Loading history...
418
419
		$message = isset( $strings[ $key ] )
420
			? $strings[ $key ]
421
			: false;
422
423
		if( !$message )return;
424
425
		$message = str_replace( ':attribute', $attribute, $message );
426
427
		if( method_exists( $this, $replacer = "replace{$rule}" )) {
428
			$message = $this->$replacer( $message, $parameters );
429
		}
430
431
		return $message;
432
	}
433
434
	// Rules Validation
435
	// ---------------------------------------------------------------------------------------------
436
437
	/**
438
	 * Validate that an attribute was "accepted".
439
	 *
440
	 * This validation rule implies the attribute is "required".
441
	 *
442
	 * @param mixed  $value
443
	 *
444
	 * @return bool
445
	 */
446
	protected function validateAccepted( $value )
447
	{
448
		$acceptable = ['yes', 'on', '1', 1, true, 'true'];
449
450
		return $this->validateRequired( $value ) && in_array( $value, $acceptable, true );
451
	}
452
453
	/**
454
	 * Validate a given attribute against a rule.
455
	 *
456
	 * @param string $rule
457
	 * @param string $attribute
458
	 *
459
	 * @return void
460
	 * @throws BadMethodCallException
461
	 */
462
	protected function validateAttribute( $rule, $attribute )
463
	{
464
		list( $rule, $parameters ) = $this->parseRule( $rule );
465
466
		if( $rule == '' )return;
467
468
		$method = "validate{$rule}";
469
470
		if( !method_exists( $this, $method )) {
471
			throw new BadMethodCallException( "Method [$method] does not exist." );
472
		}
473
474
		if( !$this->$method( $this->getValue( $attribute ), $attribute, $parameters )) {
475
			$this->addFailure( $attribute, $rule, $parameters );
476
		}
477
	}
478
479
	/**
480
	 * Validate the size of an attribute is between a set of values.
481
	 *
482
	 * @param mixed  $value
483
	 * @param string $attribute
484
	 *
485
	 * @return bool
486
	 */
487
	protected function validateBetween( $value, $attribute, array $parameters )
488
	{
489
		$this->requireParameterCount( 2, $parameters, 'between' );
490
491
		$size = $this->getSize( $attribute, $value );
492
493
		return $size >= $parameters[0] && $size <= $parameters[1];
494
	}
495
496
	/**
497
	 * Validate that an attribute is a valid e-mail address.
498
	 *
499
	 * @param mixed $value
500
	 *
501
	 * @return bool
502
	 */
503
	protected function validateEmail( $value )
504
	{
505
		return filter_var( $value, FILTER_VALIDATE_EMAIL ) !== false;
506
	}
507
508
	/**
509
	 * Validate the size of an attribute is less than a maximum value.
510
	 *
511
	 * @param mixed  $value
512
	 * @param string $attribute
513
	 *
514
	 * @return bool
515
	 */
516
	protected function validateMax( $value, $attribute, array $parameters )
517
	{
518
		$this->requireParameterCount( 1, $parameters, 'max' );
519
520
		return $this->getSize( $attribute, $value ) <= $parameters[0];
521
	}
522
523
	/**
524
	 * Validate the size of an attribute is greater than a minimum value.
525
	 *
526
	 * @param mixed  $value
527
	 * @param string $attribute
528
	 *
529
	 * @return bool
530
	 */
531
	protected function validateMin( $value, $attribute, array $parameters )
532
	{
533
		$this->requireParameterCount( 1, $parameters, 'min' );
534
535
		return $this->getSize( $attribute, $value ) >= $parameters[0];
536
	}
537
538
	/**
539
	 * Validate that an attribute is numeric.
540
	 *
541
	 * @param mixed $value
542
	 *
543
	 * @return bool
544
	 */
545
	protected function validateNumeric( $value )
546
	{
547
		return is_numeric( $value );
548
	}
549
550
	/**
551
	 * Validate that a required attribute exists.
552
	 *
553
	 * @param mixed $value
554
	 *
555
	 * @return bool
556
	 */
557
	protected function validateRequired( $value )
558
	{
559
		if( is_string( $value )) {
560
			$value = trim( $value );
561
		}
562
		return is_null( $value ) || empty( $value )
563
			? false
564
			: true;
565
	}
566
}
567