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 ) |
|
|
|
|
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 ) |
|
|
|
|
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} |
|
|
|
|
293
|
|
|
if( strpos( $rule, ':' ) !== false ) { |
294
|
|
|
list( $rule, $parameter ) = explode( ':', $rule, 2 ); |
295
|
|
|
|
296
|
|
|
// example: {parameter1,parameter2,...} |
|
|
|
|
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 ); |
|
|
|
|
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 ); |
|
|
|
|
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
|
|
|
|
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.