Validator   C
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 618
Duplicated Lines 9.39 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 70
lcom 1
cbo 4
dl 58
loc 618
rs 5.5169
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A getPropertyValue() 0 15 3
B getRequestParam() 0 16 6
A __construct() 0 7 1
A isValid() 0 4 1
A getShowValidationRules() 0 4 1
A count() 0 4 1
A addError() 5 10 2
A getDefaultMessage() 0 4 1
A getDefaultMessages() 0 4 1
A getError() 0 12 3
A getErrors() 0 12 3
A getFirstError() 0 18 4
A getValue() 0 8 2
A getValues() 0 8 2
A setDefaultMessage() 0 6 1
A setDefaultMessages() 0 6 1
A setErrors() 5 16 4
A setShowValidationRules() 0 6 1
A setValue() 10 10 2
A setValues() 10 10 2
A validate() 0 12 4
A value() 0 8 1
A array() 0 12 2
A object() 16 16 3
A request() 12 12 2
A removeErrors() 0 14 4
A getRulesNames() 0 9 2
A handleValidationException() 0 8 2
A mergeMessages() 0 6 2
B storeErrors() 0 23 4
A validateInput() 0 10 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
/*
4
 * This file is part of the awurth/slim-validation package.
5
 *
6
 * (c) Alexis Wurth <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Awurth\SlimValidation;
13
14
use InvalidArgumentException;
15
use Psr\Http\Message\ServerRequestInterface as Request;
16
use ReflectionClass;
17
use ReflectionProperty;
18
use Respect\Validation\Exceptions\NestedValidationException;
19
use Respect\Validation\Rules\AllOf;
20
21
/**
22
 * Validator.
23
 *
24
 * @author Alexis Wurth <[email protected]>
25
 */
26
class Validator
27
{
28
    /**
29
     * The default error messages for the given rules.
30
     *
31
     * @var string[]
32
     */
33
    protected $defaultMessages;
34
35
    /**
36
     * The list of validation errors.
37
     *
38
     * @var string[]
39
     */
40
    protected $errors;
41
42
    /**
43
     * Tells whether errors should be stored in an associative array
44
     * with validation rules as the key, or in an indexed array.
45
     *
46
     * @var bool
47
     */
48
    protected $showValidationRules;
49
50
    /**
51
     * The validated data.
52
     *
53
     * @var array
54
     */
55
    protected $values;
56
57
    /**
58
     * Constructor.
59
     *
60
     * @param bool     $showValidationRules
61
     * @param string[] $defaultMessages
62
     */
63
    public function __construct(bool $showValidationRules = true, array $defaultMessages = [])
64
    {
65
        $this->showValidationRules = $showValidationRules;
66
        $this->defaultMessages = $defaultMessages;
67
        $this->errors = [];
68
        $this->values = [];
69
    }
70
71
    /**
72
     * Tells if there is no error.
73
     *
74
     * @return bool
75
     */
76
    public function isValid(): bool
77
    {
78
        return empty($this->errors);
79
    }
80
81
    /**
82
     * Validates an array with the given rules.
83
     *
84
     * @param array         $array
85
     * @param AllOf[]|array $rules
86
     * @param string|null   $group
87
     * @param string[]      $messages
88
     * @param mixed|null    $default
89
     *
90
     * @return self
91
     */
92
    public function array(array $array, array $rules, string $group = null, array $messages = [], $default = null): self
93
    {
94
        foreach ($rules as $key => $options) {
95
            $config = new Configuration($options, $key, $group, $default);
96
97
            $value = $array[$key] ?? $config->getDefault();
98
99
            $this->validateInput($value, $config, $messages);
100
        }
101
102
        return $this;
103
    }
104
105
    /**
106
     * Validates an objects properties with the given rules.
107
     *
108
     * @param object        $object
109
     * @param AllOf[]|array $rules
110
     * @param string|null   $group
111
     * @param string[]      $messages
112
     * @param mixed|null    $default
113
     *
114
     * @return self
115
     */
116 View Code Duplication
    public function object($object, array $rules, string $group = null, array $messages = [], $default = null): self
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...
117
    {
118
        if (!is_object($object)) {
119
            throw new InvalidArgumentException('The first argument should be an object');
120
        }
121
122
        foreach ($rules as $property => $options) {
123
            $config = new Configuration($options, $property, $group, $default);
124
125
            $value = $this->getPropertyValue($object, $property, $config->getDefault());
126
127
            $this->validateInput($value, $config, $messages);
128
        }
129
130
        return $this;
131
    }
132
133
    /**
134
     * Validates request parameters with the given rules.
135
     *
136
     * @param Request       $request
137
     * @param AllOf[]|array $rules
138
     * @param string|null   $group
139
     * @param string[]      $messages
140
     * @param mixed|null    $default
141
     *
142
     * @return self
143
     */
144 View Code Duplication
    public function request(Request $request, array $rules, string $group = null, array $messages = [], $default = null): self
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...
145
    {
146
        foreach ($rules as $param => $options) {
147
            $config = new Configuration($options, $param, $group, $default);
148
149
            $value = $this->getRequestParam($request, $param, $config->getDefault());
150
151
            $this->validateInput($value, $config, $messages);
152
        }
153
154
        return $this;
155
    }
156
157
    /**
158
     * Validates request parameters, an array or an objects properties.
159
     *
160
     * @param Request|mixed $input
161
     * @param AllOf[]|array $rules
162
     * @param string|null   $group
163
     * @param string[]      $messages
164
     * @param mixed|null    $default
165
     *
166
     * @return self
167
     */
168
    public function validate($input, array $rules, string $group = null, array $messages = [], $default = null): self
169
    {
170
        if ($input instanceof Request) {
171
            return $this->request($input, $rules, $group, $messages, $default);
172
        } elseif (is_array($input)) {
173
            return $this->array($input, $rules, $group, $messages, $default);
174
        } elseif (is_object($input)) {
175
            return $this->object($input, $rules, $group, $messages, $default);
176
        }
177
178
        return $this->value($input, $rules, null, $group, $messages);
179
    }
180
181
    /**
182
     * Validates a single value with the given rules.
183
     *
184
     * @param mixed       $value
185
     * @param AllOf|array $rules
186
     * @param string      $key
187
     * @param string|null $group
188
     * @param string[]    $messages
189
     *
190
     * @return self
191
     */
192
    public function value($value, $rules, string $key, string $group = null, array $messages = []): self
193
    {
194
        $config = new Configuration($rules, $key, $group);
195
196
        $this->validateInput($value, $config, $messages);
197
198
        return $this;
199
    }
200
201
    /**
202
     * Gets the error count.
203
     *
204
     * @return int
205
     */
206
    public function count(): int
207
    {
208
        return count($this->errors);
209
    }
210
211
    /**
212
     * Adds a validation error.
213
     *
214
     * @param string      $key
215
     * @param string      $message
216
     * @param string|null $group
217
     *
218
     * @return self
219
     */
220
    public function addError(string $key, string $message, string $group = null): self
221
    {
222 View Code Duplication
        if (!empty($group)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
223
            $this->errors[$group][$key][] = $message;
224
        } else {
225
            $this->errors[$key][] = $message;
226
        }
227
228
        return $this;
229
    }
230
231
    /**
232
     * Gets one default messages.
233
     *
234
     * @param string $key
235
     *
236
     * @return string
237
     */
238
    public function getDefaultMessage($key): string
239
    {
240
        return $this->defaultMessages[$key] ?? '';
241
    }
242
243
    /**
244
     * Gets all default messages.
245
     *
246
     * @return string[]
247
     */
248
    public function getDefaultMessages(): array
249
    {
250
        return $this->defaultMessages;
251
    }
252
253
    /**
254
     * Gets one error.
255
     *
256
     * @param string          $key
257
     * @param string|int|null $index
258
     * @param string|null     $group
259
     *
260
     * @return string
261
     */
262
    public function getError(string $key, $index = null, $group = null)
263
    {
264
        if (null === $index) {
265
            return $this->getFirstError($key, $group);
266
        }
267
268
        if (!empty($group)) {
269
            return $this->errors[$group][$key][$index] ?? '';
270
        }
271
272
        return $this->errors[$key][$index] ?? '';
273
    }
274
275
    /**
276
     * Gets multiple errors.
277
     *
278
     * @param string|null $key
279
     * @param string|null $group
280
     *
281
     * @return string[]
282
     */
283
    public function getErrors(string $key = null, string $group = null): array
284
    {
285
        if (!empty($key)) {
286
            if (!empty($group)) {
287
                return $this->errors[$group][$key] ?? [];
288
            }
289
290
            return $this->errors[$key] ?? [];
291
        }
292
293
        return $this->errors;
294
    }
295
296
    /**
297
     * Gets the first error of a parameter.
298
     *
299
     * @param string      $key
300
     * @param string|null $group
301
     *
302
     * @return string
303
     */
304
    public function getFirstError(string $key, string $group = null)
305
    {
306
        if (!empty($group)) {
307
            if (isset($this->errors[$group][$key])) {
308
                $first = array_slice($this->errors[$group][$key], 0, 1);
309
310
                return array_shift($first);
311
            }
312
        }
313
314
        if (isset($this->errors[$key])) {
315
            $first = array_slice($this->errors[$key], 0, 1);
316
317
            return array_shift($first);
318
        }
319
320
        return '';
321
    }
322
323
    /**
324
     * Gets a value from the validated data.
325
     *
326
     * @param string      $key
327
     * @param string|null $group
328
     *
329
     * @return mixed
330
     */
331
    public function getValue(string $key, string $group = null)
332
    {
333
        if (!empty($group)) {
334
            return $this->values[$group][$key] ?? null;
335
        }
336
337
        return $this->values[$key] ?? null;
338
    }
339
340
    /**
341
     * Gets the validated data.
342
     *
343
     * @param string|null $group
344
     *
345
     * @return array
346
     */
347
    public function getValues(string $group = null)
348
    {
349
        if (!empty($group)) {
350
            return $this->values[$group] ?? [];
351
        }
352
353
        return $this->values;
354
    }
355
356
    /**
357
     * Gets the errors storage mode.
358
     *
359
     * @return bool
360
     */
361
    public function getShowValidationRules(): bool
362
    {
363
        return $this->showValidationRules;
364
    }
365
366
    /**
367
     * Removes validation errors.
368
     *
369
     * @param string|null $key
370
     * @param string|null $group
371
     *
372
     * @return self
373
     */
374
    public function removeErrors(string $key = null, string $group = null): self
375
    {
376
        if (!empty($group)) {
377
            if (!empty($key)) {
378
                unset($this->errors[$group][$key]);
379
            } else {
380
                unset($this->errors[$group]);
381
            }
382
        } elseif (!empty($key)) {
383
            unset($this->errors[$key]);
384
        }
385
386
        return $this;
387
    }
388
389
    /**
390
     * Sets the default error message for a validation rule.
391
     *
392
     * @param string $rule
393
     * @param string $message
394
     *
395
     * @return self
396
     */
397
    public function setDefaultMessage(string $rule, string $message): self
398
    {
399
        $this->defaultMessages[$rule] = $message;
400
401
        return $this;
402
    }
403
404
    /**
405
     * Sets default error messages.
406
     *
407
     * @param string[] $messages
408
     *
409
     * @return self
410
     */
411
    public function setDefaultMessages(array $messages): self
412
    {
413
        $this->defaultMessages = $messages;
414
415
        return $this;
416
    }
417
418
    /**
419
     * Sets validation errors.
420
     *
421
     * @param string[]    $errors
422
     * @param string|null $key
423
     * @param string|null $group
424
     *
425
     * @return self
426
     */
427
    public function setErrors(array $errors, string $key = null, string $group = null): self
428
    {
429
        if (!empty($group)) {
430 View Code Duplication
            if (!empty($key)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
431
                $this->errors[$group][$key] = $errors;
432
            } else {
433
                $this->errors[$group] = $errors;
434
            }
435
        } elseif (!empty($key)) {
436
            $this->errors[$key] = $errors;
437
        } else {
438
            $this->errors = $errors;
439
        }
440
441
        return $this;
442
    }
443
444
    /**
445
     * Sets the errors storage mode.
446
     *
447
     * @param bool $showValidationRules
448
     *
449
     * @return self
450
     */
451
    public function setShowValidationRules(bool $showValidationRules): self
452
    {
453
        $this->showValidationRules = $showValidationRules;
454
455
        return $this;
456
    }
457
458
    /**
459
     * Sets the value of a parameter.
460
     *
461
     * @param string      $key
462
     * @param mixed       $value
463
     * @param string|null $group
464
     *
465
     * @return self
466
     */
467 View Code Duplication
    public function setValue(string $key, $value, string $group = null): self
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...
468
    {
469
        if (!empty($group)) {
470
            $this->values[$group][$key] = $value;
471
        } else {
472
            $this->values[$key] = $value;
473
        }
474
475
        return $this;
476
    }
477
478
    /**
479
     * Sets values of validated data.
480
     *
481
     * @param array       $values
482
     * @param string|null $group
483
     *
484
     * @return self
485
     */
486 View Code Duplication
    public function setValues(array $values, string $group = null): self
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...
487
    {
488
        if (!empty($group)) {
489
            $this->values[$group] = $values;
490
        } else {
491
            $this->values = $values;
492
        }
493
494
        return $this;
495
    }
496
497
    /**
498
     * Gets the value of a property of an object.
499
     *
500
     * @param object     $object
501
     * @param string     $propertyName
502
     * @param mixed|null $default
503
     *
504
     * @return mixed
505
     */
506
    protected function getPropertyValue($object, string $propertyName, $default = null)
507
    {
508
        if (!is_object($object)) {
509
            throw new InvalidArgumentException('The first argument should be an object');
510
        }
511
512
        if (!property_exists($object, $propertyName)) {
513
            return $default;
514
        }
515
516
        $reflectionProperty = new ReflectionProperty($object, $propertyName);
517
        $reflectionProperty->setAccessible(true);
518
519
        return $reflectionProperty->getValue($object);
520
    }
521
522
    /**
523
     * Fetches a request parameter's value from the body or query string (in that order).
524
     *
525
     * @param Request     $request
526
     * @param string      $key
527
     * @param string|null $default
528
     *
529
     * @return mixed
530
     */
531
    protected function getRequestParam(Request $request, $key, $default = null)
532
    {
533
        $postParams = $request->getParsedBody();
534
        $getParams = $request->getQueryParams();
535
536
        $result = $default;
537
        if (is_array($postParams) && isset($postParams[$key])) {
538
            $result = $postParams[$key];
539
        } elseif (is_object($postParams) && property_exists($postParams, $key)) {
540
            $result = $postParams->$key;
541
        } elseif (isset($getParams[$key])) {
542
            $result = $getParams[$key];
543
        }
544
545
        return $result;
546
    }
547
548
    /**
549
     * Gets the name of all rules of a group of rules.
550
     *
551
     * @param AllOf $rules
552
     *
553
     * @return string[]
554
     */
555
    protected function getRulesNames(AllOf $rules): array
556
    {
557
        $rulesNames = [];
558
        foreach ($rules->getRules() as $rule) {
559
            $rulesNames[] = lcfirst((new ReflectionClass($rule))->getShortName());
560
        }
561
562
        return $rulesNames;
563
    }
564
565
    /**
566
     * Handles a validation exception.
567
     *
568
     * @param NestedValidationException $e
569
     * @param Configuration             $config
570
     * @param string[]                  $messages
571
     */
572
    protected function handleValidationException(NestedValidationException $e, Configuration $config, array $messages = [])
573
    {
574
        if ($config->hasMessage()) {
575
            $this->setErrors([$config->getMessage()], $config->getKey(), $config->getGroup());
576
        } else {
577
            $this->storeErrors($e, $config, $messages);
578
        }
579
    }
580
581
    /**
582
     * Merges default messages, global messages and individual messages.
583
     *
584
     * @param array $errors
585
     *
586
     * @return string[]
587
     */
588
    protected function mergeMessages(array $errors): array
589
    {
590
        $errors = array_filter(call_user_func_array('array_merge', $errors));
591
592
        return $this->showValidationRules ? $errors : array_values($errors);
593
    }
594
595
    /**
596
     * Sets error messages after validation.
597
     *
598
     * @param NestedValidationException $e
599
     * @param Configuration             $config
600
     * @param string[]                  $messages
601
     */
602
    protected function storeErrors(NestedValidationException $e, Configuration $config, array $messages = [])
603
    {
604
        $errors = [
605
            $e->findMessages($this->getRulesNames($config->getValidationRules()))
606
        ];
607
608
        // If default messages are defined
609
        if (!empty($this->defaultMessages)) {
610
            $errors[] = $e->findMessages($this->defaultMessages);
611
        }
612
613
        // If global messages are defined
614
        if (!empty($messages)) {
615
            $errors[] = $e->findMessages($messages);
616
        }
617
618
        // If individual messages are defined
619
        if ($config->hasMessages()) {
620
            $errors[] = $e->findMessages($config->getMessages());
621
        }
622
623
        $this->setErrors($this->mergeMessages($errors), $config->getKey(), $config->getGroup());
624
    }
625
626
    /**
627
     * Executes the validation of a value and handles errors.
628
     *
629
     * @param mixed         $input
630
     * @param Configuration $config
631
     * @param string[]      $messages
632
     */
633
    protected function validateInput($input, Configuration $config, array $messages = [])
634
    {
635
        try {
636
            $config->getValidationRules()->assert($input);
637
        } catch (NestedValidationException $e) {
638
            $this->handleValidationException($e, $config, $messages);
639
        }
640
641
        $this->setValue($config->getKey(), $input, $config->getGroup());
642
    }
643
}
644