Completed
Push — master ( 017c51...952a61 )
by Alexis
01:34
created

Validator::request()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Importance

Changes 0
Metric Value
dl 10
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 4
1
<?php
2
3
namespace Awurth\SlimValidation;
4
5
use InvalidArgumentException;
6
use Psr\Http\Message\ServerRequestInterface as Request;
7
use ReflectionClass;
8
use ReflectionProperty;
9
use Respect\Validation\Exceptions\NestedValidationException;
10
use Respect\Validation\Rules\AbstractComposite;
11
use Respect\Validation\Validator as RespectValidator;
12
13
/**
14
 * Validator.
15
 *
16
 * @author Alexis Wurth <[email protected]>
17
 */
18
class Validator
19
{
20
    const MODE_ASSOCIATIVE = 1;
21
    const MODE_INDEXED = 2;
22
23
    /**
24
     * The validated data.
25
     *
26
     * @var array
27
     */
28
    protected $values;
29
30
    /**
31
     * The default error messages for the given rules.
32
     *
33
     * @var string[]
34
     */
35
    protected $defaultMessages;
36
37
    /**
38
     * The list of validation errors.
39
     *
40
     * @var string[]
41
     */
42
    protected $errors;
43
44
    /**
45
     * Tells whether errors should be stored in an associative array
46
     * or in an indexed array.
47
     *
48
     * @var int
49
     */
50
    protected $errorStorageMode;
51
52
    /**
53
     * Constructor.
54
     *
55
     * @param int      $errorStorageMode
56
     * @param string[] $defaultMessages
57
     */
58
    public function __construct(int $errorStorageMode = self::MODE_ASSOCIATIVE, array $defaultMessages = [])
59
    {
60
        $this->errorStorageMode = $errorStorageMode;
61
        $this->defaultMessages = $defaultMessages;
62
        $this->errors = [];
63
        $this->values = [];
64
    }
65
66
    /**
67
     * Tells if there is no error.
68
     *
69
     * @return bool
70
     */
71
    public function isValid()
72
    {
73
        return empty($this->errors);
74
    }
75
76
    /**
77
     * Validates an array with the given rules.
78
     *
79
     * @param array  $array
80
     * @param array  $rules
81
     * @param array  $messages
82
     * @param string $group
83
     *
84
     * @return self
85
     */
86 View Code Duplication
    public function array(array $array, array $rules, array $messages = [], $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...
87
    {
88
        foreach ($rules as $key => $options) {
89
            $value = $array[$key] ?? null;
90
91
            $this->value($value, $key, $options, $messages, $group);
92
        }
93
94
        return $this;
95
    }
96
97
    /**
98
     * Validates an objects properties with the given rules.
99
     *
100
     * @param object     $object
101
     * @param array      $rules
102
     * @param array      $messages
103
     * @param string|int $group
104
     *
105
     * @return self
106
     */
107
    public function object(object $object, array $rules, array $messages = [], $group = null)
108
    {
109
        foreach ($rules as $property => $options) {
110
            $value = $this->getPropertyValue($object, $property);
111
112
            $this->value($value, $property, $options, $messages, $group);
113
        }
114
115
        return $this;
116
    }
117
118
    /**
119
     * Validates request parameters with the given rules.
120
     *
121
     * @param Request $request
122
     * @param array   $rules
123
     * @param array   $messages
124
     * @param string  $group
125
     *
126
     * @return self
127
     */
128 View Code Duplication
    public function request(Request $request, array $rules, array $messages = [], $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...
129
    {
130
        foreach ($rules as $param => $options) {
131
            $value = $this->getRequestParam($request, $param);
132
133
            $this->value($value, $param, $options, $messages, $group);
134
        }
135
136
        return $this;
137
    }
138
139
    /**
140
     * Validates request parameters, an array or an objects properties.
141
     *
142
     * @param Request|object|array $input
143
     * @param array                $rules
144
     * @param array                $messages
145
     * @param string               $group
146
     *
147
     * @return self
148
     */
149
    public function validate($input, array $rules, array $messages = [], $group = null)
150
    {
151
        if (!is_object($input) && !is_array($input)) {
152
            throw new InvalidArgumentException('The input must be either an object or an array');
153
        }
154
155
        if ($input instanceof Request) {
156
            return $this->request($input, $rules, $messages, $group);
157
        } elseif (is_array($input)) {
158
            return $this->array($input, $rules, $messages, $group);
159
        } elseif (is_object($input)) {
160
            return $this->object($input, $rules, $messages, $group);
161
        }
162
163
        return $this;
164
    }
165
166
    /**
167
     * Validates a single value with the given rules.
168
     *
169
     * @param mixed                  $value
170
     * @param string                 $key
171
     * @param RespectValidator|array $rules
172
     * @param array                  $messages
173
     * @param string                 $group
174
     *
175
     * @return self
176
     */
177
    public function value($value, $key, $rules, array $messages = [], $group = null)
178
    {
179
        try {
180
            if ($rules instanceof RespectValidator) {
181
                $rules->assert($value);
182
            } else {
183
                if (!is_array($rules) || !isset($rules['rules']) || !($rules['rules'] instanceof RespectValidator)) {
184
                    throw new InvalidArgumentException('Validation rules are missing');
185
                }
186
187
                $rules['rules']->assert($value);
188
            }
189
        } catch (NestedValidationException $e) {
190
            // If the 'message' key exists, set it as only message for this param
191
            if (is_array($rules) && isset($rules['message'])) {
192
                if (!is_string($rules['message'])) {
193
                    throw new InvalidArgumentException(sprintf('Expected custom message to be of type string, %s given', gettype($rules['message'])));
194
                }
195
196
                $this->setErrors([$rules['message']], $key, $group);
197
            } else {
198
                // If the 'messages' key exists, override global messages
199
                $this->setMessages($e, $key, $rules, $messages, $group);
200
            }
201
        }
202
203
        $this->setValue($key, $value, $group);
204
205
        return $this;
206
    }
207
208
    /**
209
     * Gets the error count.
210
     *
211
     * @return int
212
     */
213
    public function count()
214
    {
215
        return count($this->errors);
216
    }
217
218
    /**
219
     * Adds a validation error.
220
     *
221
     * @param string $key
222
     * @param string $message
223
     * @param string $group
224
     *
225
     * @return self
226
     */
227
    public function addError($key, $message, $group = null)
228
    {
229 View Code Duplication
        if (null !== $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...
230
            $this->errors[$group][$key][] = $message;
231
        } else {
232
            $this->errors[$key][] = $message;
233
        }
234
235
        return $this;
236
    }
237
238
    /**
239
     * Gets one default messages.
240
     *
241
     * @param string $key
242
     *
243
     * @return string
244
     */
245
    public function getDefaultMessage($key)
246
    {
247
        return $this->defaultMessages[$key] ?? '';
248
    }
249
250
    /**
251
     * Gets all default messages.
252
     *
253
     * @return string[]
254
     */
255
    public function getDefaultMessages()
256
    {
257
        return $this->defaultMessages;
258
    }
259
260
    /**
261
     * Gets one error.
262
     *
263
     * @param string $key
264
     * @param string $index
265
     * @param string $group
266
     *
267
     * @return string
268
     */
269
    public function getError($key, $index = null, $group = null)
270
    {
271
        if (null === $index) {
272
            return $this->getFirstError($key, $group);
273
        }
274
275
        if (null !== $group) {
276
            return $this->errors[$group][$key][$index] ?? '';
277
        }
278
279
        return $this->errors[$key][$index] ?? '';
280
    }
281
282
    /**
283
     * Gets multiple errors.
284
     *
285
     * @param string $key
286
     * @param string $group
287
     *
288
     * @return string[]
289
     */
290
    public function getErrors($key = null, $group = null)
291
    {
292
        if (null !== $key) {
293
            if (null !== $group) {
294
                return $this->errors[$group][$key] ?? [];
295
            }
296
297
            return $this->errors[$key] ?? [];
298
        }
299
300
        return $this->errors;
301
    }
302
303
    /**
304
     * Gets the first error of a parameter.
305
     *
306
     * @param string $key
307
     * @param string $group
308
     *
309
     * @return string
310
     */
311
    public function getFirstError($key, $group = null)
312
    {
313 View Code Duplication
        if (null !== $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...
314
            if (isset($this->errors[$group][$key])) {
315
                $first = array_slice($this->errors[$group][$key], 0, 1);
316
317
                return array_shift($first);
318
            }
319
        }
320
321 View Code Duplication
        if (isset($this->errors[$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...
322
            $first = array_slice($this->errors[$key], 0, 1);
323
324
            return array_shift($first);
325
        }
326
327
        return '';
328
    }
329
330
    /**
331
     * Gets the value from the validated data.
332
     *
333
     * @param string $key
334
     * @param string $group
335
     *
336
     * @return string
337
     */
338
    public function getValue($key, $group = null)
339
    {
340
        if (null !== $group) {
341
            return $this->values[$group][$key] ?? '';
342
        }
343
344
        return $this->values[$key] ?? '';
345
    }
346
347
    /**
348
     * Gets the validated data.
349
     *
350
     * @return array
351
     */
352
    public function getValues()
353
    {
354
        return $this->values;
355
    }
356
357
    /**
358
     * Gets the error storage mode.
359
     *
360
     * @return int
361
     */
362
    public function getErrorStorageMode()
363
    {
364
        return $this->errorStorageMode;
365
    }
366
367
    /**
368
     * Removes validation errors.
369
     *
370
     * @param string $key
371
     * @param string $group
372
     *
373
     * @return self
374
     */
375
    public function removeErrors($key = null, $group = null)
376
    {
377
        if (null !== $group) {
378
            if (null !== $key) {
379 View Code Duplication
                if (isset($this->errors[$group][$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...
380
                    $this->errors[$group][$key] = [];
381
                }
382
            } elseif (isset($this->errors[$group])) {
383
                $this->errors[$group] = [];
384
            }
385 View Code Duplication
        } elseif (null !== $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...
386
            if (isset($this->errors[$key])) {
387
                $this->errors[$key] = [];
388
            }
389
        }
390
391
        return $this;
392
    }
393
394
    /**
395
     * Sets the default error message for a validation rule.
396
     *
397
     * @param string $rule
398
     * @param string $message
399
     *
400
     * @return self
401
     */
402
    public function setDefaultMessage($rule, $message)
403
    {
404
        $this->defaultMessages[$rule] = $message;
405
406
        return $this;
407
    }
408
409
    /**
410
     * Sets default error messages.
411
     *
412
     * @param string[] $messages
413
     *
414
     * @return self
415
     */
416
    public function setDefaultMessages(array $messages)
417
    {
418
        $this->defaultMessages = $messages;
419
420
        return $this;
421
    }
422
423
    /**
424
     * Sets validation errors.
425
     *
426
     * @param string[] $errors
427
     * @param string   $key
428
     * @param string   $group
429
     *
430
     * @return self
431
     */
432
    public function setErrors(array $errors, $key = null, $group = null)
433
    {
434
        if (null !== $group) {
435 View Code Duplication
            if (null !== $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...
436
                $this->errors[$group][$key] = $errors;
437
            } else {
438
                $this->errors[$group] = $errors;
439
            }
440
        } elseif (null !== $key) {
441
            $this->errors[$key] = $errors;
442
        } else {
443
            $this->errors = $errors;
444
        }
445
446
        return $this;
447
    }
448
449
    /**
450
     * Sets the error storage mode.
451
     *
452
     * @param int $errorStorageMode
453
     *
454
     * @return self
455
     */
456
    public function setErrorStorageMode(int $errorStorageMode)
457
    {
458
        $this->errorStorageMode = $errorStorageMode;
459
460
        return $this;
461
    }
462
463
    /**
464
     * Sets the value of a parameter.
465
     *
466
     * @param string $key
467
     * @param mixed  $value
468
     * @param string $group
469
     *
470
     * @return self
471
     */
472
    public function setValue($key, $value, $group = null)
473
    {
474
        if (null !== $group) {
475
            $this->values[$group][$key] = $value;
476
        } else {
477
            $this->values[$key] = $value;
478
        }
479
480
        return $this;
481
    }
482
483
    /**
484
     * Sets the values of request parameters.
485
     *
486
     * @param array $values
487
     *
488
     * @return self
489
     */
490
    public function setValues(array $values)
491
    {
492
        $this->values = $values;
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  $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
     * Fetch request parameter value from body or query string (in that order).
524
     *
525
     * @param  Request $request
526
     * @param  string  $key The parameter key.
527
     * @param  string  $default The default value.
528
     *
529
     * @return mixed The parameter value.
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
     * Sets error messages after validation.
550
     *
551
     * @param NestedValidationException $e
552
     * @param string                    $key
553
     * @param AbstractComposite|array   $options
554
     * @param array                     $messages
555
     * @param string                    $group
556
     */
557
    protected function setMessages(NestedValidationException $e, $key, $options, array $messages = [], $group = null)
558
    {
559
        $rules = $options instanceof RespectValidator ? $options->getRules() : $options['rules']->getRules();
560
561
        // Get the names of all rules used for this param
562
        $rulesNames = [];
563
        foreach ($rules as $rule) {
564
            $rulesNames[] = lcfirst((new ReflectionClass($rule))->getShortName());
565
        }
566
567
        $errors = [
568
            $e->findMessages($rulesNames)
569
        ];
570
571
        // If default messages are defined
572
        if (!empty($this->defaultMessages)) {
573
            $errors[] = $e->findMessages($this->defaultMessages);
574
        }
575
576
        // If global messages are defined
577
        if (!empty($messages)) {
578
            $errors[] = $e->findMessages($messages);
579
        }
580
581
        // If individual messages are defined
582
        if (is_array($options) && isset($options['messages'])) {
583
            if (!is_array($options['messages'])) {
584
                throw new InvalidArgumentException(sprintf('Expected custom individual messages to be of type array, %s given', gettype($options['messages'])));
585
            }
586
587
            $errors[] = $e->findMessages($options['messages']);
588
        }
589
590
        $errors = array_filter(call_user_func_array('array_merge', $errors));
591
592
        $this->setErrors($this->errorStorageMode === self::MODE_ASSOCIATIVE ? $errors : array_values($errors), $key, $group);
593
    }
594
}
595