Completed
Push — develop ( 0b0716...7823b4 )
by Paul
02:04
created

Validator::validateRequired()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 8
nop 1
1
<?php
2
3
namespace GeminiLabs\Castor;
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 View Code Duplication
	protected function getMessage( $attribute, $rule, array $parameters )
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
	protected function getSizeMessage( $attribute, $rule, array $parameters )
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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();
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
	 * @param string $attribute
444
	 *
445
	 * @return bool
446
	 */
447
	protected function validateAccepted( $value, $attribute )
448
	{
449
		$acceptable = ['yes', 'on', '1', 1, true, 'true'];
450
451
		return $this->validateRequired( $attribute, $value ) && in_array( $value, $acceptable, true );
0 ignored issues
show
Unused Code introduced by
The call to Validator::validateRequired() has too many arguments starting with $value.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
452
	}
453
454
	/**
455
	 * Validate a given attribute against a rule.
456
	 *
457
	 * @param string $rule
458
	 * @param string $attribute
459
	 *
460
	 * @return void
461
	 * @throws BadMethodCallException
462
	 */
463
	protected function validateAttribute( $rule, $attribute )
464
	{
465
		list( $rule, $parameters ) = $this->parseRule( $rule );
466
467
		if( $rule == '' )return;
468
469
		$value = $this->getValue( $attribute );
470
471
		// is the value filled or is the attribute required?
472
		// - removed $validatable assignment
473
		$this->validateRequired( $attribute, $value ) || in_array( $rule, $this->implicitRules );
0 ignored issues
show
Unused Code introduced by
The call to Validator::validateRequired() has too many arguments starting with $value.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
474
475
		$method = "validate{$rule}";
476
477
		if( !method_exists( $this, $method )) {
478
			throw new BadMethodCallException( "Method [$method] does not exist." );
479
		}
480
481
		if( !$this->$method( $value, $attribute, $parameters )) {
482
			$this->addFailure( $attribute, $rule, $parameters );
483
		}
484
	}
485
486
	/**
487
	 * Validate the size of an attribute is between a set of values.
488
	 *
489
	 * @param mixed  $value
490
	 * @param string $attribute
491
	 *
492
	 * @return bool
493
	 */
494
	protected function validateBetween( $value, $attribute, array $parameters )
495
	{
496
		$this->requireParameterCount( 2, $parameters, 'between' );
497
498
		$size = $this->getSize( $attribute, $value );
499
500
		return $size >= $parameters[0] && $size <= $parameters[1];
501
	}
502
503
	/**
504
	 * Validate that an attribute is a valid e-mail address.
505
	 *
506
	 * @param mixed $value
507
	 *
508
	 * @return bool
509
	 */
510
	protected function validateEmail( $value )
511
	{
512
		return filter_var( $value, FILTER_VALIDATE_EMAIL ) !== false;
513
	}
514
515
	/**
516
	 * Validate the size of an attribute is less than a maximum value.
517
	 *
518
	 * @param mixed  $value
519
	 * @param string $attribute
520
	 *
521
	 * @return bool
522
	 */
523
	protected function validateMax( $value, $attribute, array $parameters )
524
	{
525
		$this->requireParameterCount( 1, $parameters, 'max' );
526
527
		return $this->getSize( $attribute, $value ) <= $parameters[0];
528
	}
529
530
	/**
531
	 * Validate the size of an attribute is greater than a minimum value.
532
	 *
533
	 * @param mixed  $value
534
	 * @param string $attribute
535
	 *
536
	 * @return bool
537
	 */
538
	protected function validateMin( $value, $attribute, array $parameters )
539
	{
540
		$this->requireParameterCount( 1, $parameters, 'min' );
541
542
		return $this->getSize( $attribute, $value ) >= $parameters[0];
543
	}
544
545
	/**
546
	 * Validate that an attribute is numeric.
547
	 *
548
	 * @param mixed $value
549
	 *
550
	 * @return bool
551
	 */
552
	protected function validateNumeric( $value )
553
	{
554
		return is_numeric( $value );
555
	}
556
557
	/**
558
	 * Validate that a required attribute exists.
559
	 *
560
	 * @param mixed $value
561
	 *
562
	 * @return bool
563
	 */
564
	protected function validateRequired( $value )
565
	{
566
		if( is_string( $value )) {
567
			$value = trim( $value );
568
		}
569
		return is_null( $value ) || empty( $value )
570
			? false
571
			: true;
572
	}
573
}
574