Completed
Push — master ( f1a274...c14c84 )
by Alexis
01:37
created

Validator::storeErrors()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
cc 5
eloc 14
nc 16
nop 5
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()
77
    {
78
        return empty($this->errors);
79
    }
80
81
    /**
82
     * Validates an array with the given rules.
83
     *
84
     * @param array    $array
85
     * @param array    $rules
86
     * @param string   $group
87
     * @param string[] $messages
88
     *
89
     * @return self
90
     */
91 View Code Duplication
    public function array(array $array, array $rules, $group = null, array $messages = [])
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...
92
    {
93
        foreach ($rules as $key => $options) {
94
            $value = $array[$key] ?? null;
95
96
            $this->value($value, $options, $key, $group, $messages);
97
        }
98
99
        return $this;
100
    }
101
102
    /**
103
     * Validates an objects properties with the given rules.
104
     *
105
     * @param object   $object
106
     * @param array    $rules
107
     * @param string   $group
108
     * @param string[] $messages
109
     *
110
     * @return self
111
     */
112
    public function object($object, array $rules, $group = null, array $messages = [])
113
    {
114
        if (!is_object($object)) {
115
            throw new InvalidArgumentException('The first argument should be an object');
116
        }
117
118
        foreach ($rules as $property => $options) {
119
            $value = $this->getPropertyValue($object, $property);
120
121
            $this->value($value, $options, $property, $group, $messages);
122
        }
123
124
        return $this;
125
    }
126
127
    /**
128
     * Validates request parameters with the given rules.
129
     *
130
     * @param Request  $request
131
     * @param array    $rules
132
     * @param string   $group
133
     * @param string[] $messages
134
     *
135
     * @return self
136
     */
137 View Code Duplication
    public function request(Request $request, array $rules, $group = null, array $messages = [])
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...
138
    {
139
        foreach ($rules as $param => $options) {
140
            $value = $this->getRequestParam($request, $param);
141
142
            $this->value($value, $options, $param, $group, $messages);
143
        }
144
145
        return $this;
146
    }
147
148
    /**
149
     * Validates request parameters, an array or an objects properties.
150
     *
151
     * @param Request|object|array $input
152
     * @param array                $rules
153
     * @param string               $group
154
     * @param string[]             $messages
155
     *
156
     * @return self
157
     */
158
    public function validate($input, array $rules, $group = null, array $messages = [])
159
    {
160
        if (!is_object($input) && !is_array($input)) {
161
            throw new InvalidArgumentException('The input must be either an object or an array');
162
        }
163
164
        if ($input instanceof Request) {
165
            return $this->request($input, $rules, $group, $messages);
166
        } elseif (is_array($input)) {
167
            return $this->array($input, $rules, $group, $messages);
168
        } elseif (is_object($input)) {
169
            return $this->object($input, $rules, $group, $messages);
170
        }
171
172
        return $this;
173
    }
174
175
    /**
176
     * Validates a single value with the given rules.
177
     *
178
     * @param mixed       $value
179
     * @param AllOf|array $rules
180
     * @param string      $key
181
     * @param string      $group
182
     * @param string[]    $messages
183
     *
184
     * @return self
185
     */
186
    public function value($value, $rules, $key, $group = null, array $messages = [])
187
    {
188
        $configuration = new Configuration($rules);
189
190
        try {
191
            $configuration->getValidationRules()->assert($value);
192
        } catch (NestedValidationException $e) {
193
            $this->handleException($e, $configuration, $key, $group, $messages);
194
        }
195
196
        $this->setValue($key, $value, $group);
197
198
        return $this;
199
    }
200
201
    /**
202
     * Gets the error count.
203
     *
204
     * @return int
205
     */
206
    public function count()
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 $group
217
     *
218
     * @return self
219
     */
220
    public function addError($key, $message, $group = null)
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)
239
    {
240
        return $this->defaultMessages[$key] ?? '';
241
    }
242
243
    /**
244
     * Gets all default messages.
245
     *
246
     * @return string[]
247
     */
248
    public function getDefaultMessages()
249
    {
250
        return $this->defaultMessages;
251
    }
252
253
    /**
254
     * Gets one error.
255
     *
256
     * @param string $key
257
     * @param string $index
258
     * @param string $group
259
     *
260
     * @return string
261
     */
262
    public function getError($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 $key
279
     * @param string $group
280
     *
281
     * @return string[]
282
     */
283
    public function getErrors($key = null, $group = null)
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 $group
301
     *
302
     * @return string
303
     */
304
    public function getFirstError($key, $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 $group
328
     *
329
     * @return mixed
330
     */
331
    public function getValue($key, $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 $group
344
     *
345
     * @return array
346
     */
347
    public function getValues($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()
362
    {
363
        return $this->showValidationRules;
364
    }
365
366
    /**
367
     * Removes validation errors.
368
     *
369
     * @param string $key
370
     * @param string $group
371
     *
372
     * @return self
373
     */
374
    public function removeErrors($key = null, $group = null)
375
    {
376
        if (!empty($group)) {
377
            if (!empty($key)) {
378
                if (isset($this->errors[$group][$key])) {
379
                    unset($this->errors[$group][$key]);
380
                }
381
            } elseif (isset($this->errors[$group])) {
382
                unset($this->errors[$group]);
383
            }
384
        } elseif (!empty($key)) {
385
            if (isset($this->errors[$key])) {
386
                unset($this->errors[$key]);
387
            }
388
        }
389
390
        return $this;
391
    }
392
393
    /**
394
     * Sets the default error message for a validation rule.
395
     *
396
     * @param string $rule
397
     * @param string $message
398
     *
399
     * @return self
400
     */
401
    public function setDefaultMessage($rule, $message)
402
    {
403
        $this->defaultMessages[$rule] = $message;
404
405
        return $this;
406
    }
407
408
    /**
409
     * Sets default error messages.
410
     *
411
     * @param string[] $messages
412
     *
413
     * @return self
414
     */
415
    public function setDefaultMessages(array $messages)
416
    {
417
        $this->defaultMessages = $messages;
418
419
        return $this;
420
    }
421
422
    /**
423
     * Sets validation errors.
424
     *
425
     * @param string[] $errors
426
     * @param string   $key
427
     * @param string   $group
428
     *
429
     * @return self
430
     */
431
    public function setErrors(array $errors, $key = null, $group = null)
432
    {
433
        if (!empty($group)) {
434 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...
435
                $this->errors[$group][$key] = $errors;
436
            } else {
437
                $this->errors[$group] = $errors;
438
            }
439
        } elseif (!empty($key)) {
440
            $this->errors[$key] = $errors;
441
        } else {
442
            $this->errors = $errors;
443
        }
444
445
        return $this;
446
    }
447
448
    /**
449
     * Sets the errors storage mode.
450
     *
451
     * @param bool $showValidationRules
452
     *
453
     * @return self
454
     */
455
    public function setShowValidationRules(bool $showValidationRules)
456
    {
457
        $this->showValidationRules = $showValidationRules;
458
459
        return $this;
460
    }
461
462
    /**
463
     * Sets the value of a parameter.
464
     *
465
     * @param string $key
466
     * @param mixed  $value
467
     * @param string $group
468
     *
469
     * @return self
470
     */
471 View Code Duplication
    public function setValue($key, $value, $group = null)
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...
472
    {
473
        if (!empty($group)) {
474
            $this->values[$group][$key] = $value;
475
        } else {
476
            $this->values[$key] = $value;
477
        }
478
479
        return $this;
480
    }
481
482
    /**
483
     * Sets values of validated data.
484
     *
485
     * @param array  $values
486
     * @param string $group
487
     *
488
     * @return self
489
     */
490 View Code Duplication
    public function setValues(array $values, $group = null)
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...
491
    {
492
        if (!empty($group)) {
493
            $this->values[$group] = $values;
494
        } else {
495
            $this->values = $values;
496
        }
497
498
        return $this;
499
    }
500
501
    /**
502
     * Gets the value of a property of an object.
503
     *
504
     * @param object $object
505
     * @param string $propertyName
506
     * @param mixed  $default
507
     *
508
     * @return mixed
509
     */
510
    protected function getPropertyValue($object, string $propertyName, $default = null)
511
    {
512
        if (!is_object($object)) {
513
            throw new InvalidArgumentException('The first argument should be an object');
514
        }
515
516
        if (!property_exists($object, $propertyName)) {
517
            return $default;
518
        }
519
520
        $reflectionProperty = new ReflectionProperty($object, $propertyName);
521
        $reflectionProperty->setAccessible(true);
522
523
        return $reflectionProperty->getValue($object);
524
    }
525
526
    /**
527
     * Fetch request parameter value from body or query string (in that order).
528
     *
529
     * @param  Request $request
530
     * @param  string  $key The parameter key.
531
     * @param  string  $default The default value.
532
     *
533
     * @return mixed The parameter value.
534
     */
535
    protected function getRequestParam(Request $request, $key, $default = null)
536
    {
537
        $postParams = $request->getParsedBody();
538
        $getParams = $request->getQueryParams();
539
540
        $result = $default;
541
        if (is_array($postParams) && isset($postParams[$key])) {
542
            $result = $postParams[$key];
543
        } elseif (is_object($postParams) && property_exists($postParams, $key)) {
544
            $result = $postParams->$key;
545
        } elseif (isset($getParams[$key])) {
546
            $result = $getParams[$key];
547
        }
548
549
        return $result;
550
    }
551
552
    /**
553
     * Handles a validation exception.
554
     *
555
     * @param NestedValidationException $e
556
     * @param Configuration             $configuration
557
     * @param string                    $key
558
     * @param string                    $group
559
     * @param string[]                  $messages
560
     */
561
    protected function handleException(NestedValidationException $e, $configuration, $key, $group = null, array $messages = [])
562
    {
563
        // If the 'message' key exists, set it as only message for this param
564
        if ($configuration->hasMessage()) {
565
            $this->setErrors([$configuration->getMessage()], $key, $group);
566
        } else {
567
            // If the 'messages' key exists, override global messages
568
            $this->storeErrors($e, $configuration, $key, $group, $messages);
569
        }
570
    }
571
572
    /**
573
     * Merges default messages, global messages and individual messages.
574
     *
575
     * @param array $errors
576
     *
577
     * @return string[]
578
     */
579
    protected function mergeMessages(array $errors)
580
    {
581
        $errors = array_filter(call_user_func_array('array_merge', $errors));
582
583
        return $this->showValidationRules ? $errors : array_values($errors);
584
    }
585
586
    /**
587
     * Sets error messages after validation.
588
     *
589
     * @param NestedValidationException $e
590
     * @param Configuration             $configuration
591
     * @param string                    $key
592
     * @param string                    $group
593
     * @param string[]                  $messages
594
     */
595
    protected function storeErrors(NestedValidationException $e, $configuration, $key, $group = null, array $messages = [])
596
    {
597
        $rules = $configuration->getValidationRules()->getRules();
598
599
        // Get the names of all rules used for this param
600
        $rulesNames = [];
601
        foreach ($rules as $rule) {
602
            $rulesNames[] = lcfirst((new ReflectionClass($rule))->getShortName());
603
        }
604
605
        $errors = [
606
            $e->findMessages($rulesNames)
607
        ];
608
609
        // If default messages are defined
610
        if (!empty($this->defaultMessages)) {
611
            $errors[] = $e->findMessages($this->defaultMessages);
612
        }
613
614
        // If global messages are defined
615
        if (!empty($messages)) {
616
            $errors[] = $e->findMessages($messages);
617
        }
618
619
        // If individual messages are defined
620
        if ($configuration->hasMessages()) {
621
            $errors[] = $e->findMessages($configuration->getMessages());
622
        }
623
624
        $this->setErrors($this->mergeMessages($errors), $key, $group);
625
    }
626
}
627