Completed
Push — master ( e4e8e1...017c51 )
by Alexis
02:26
created

Validator::setValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 5
Ratio 50 %

Importance

Changes 0
Metric Value
dl 5
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 3
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 array
34
     */
35
    protected $defaultMessages;
36
37
    /**
38
     * The list of validation errors.
39
     *
40
     * @var array
41
     */
42
    protected $errors;
43
44
    /**
45
     * Tells if 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 array $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|int $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
        if (null !== $group && (!is_string($group) || !is_int($group))) {
89
            throw new InvalidArgumentException('The group must be either a string or an integer');
90
        }
91
92
        foreach ($rules as $key => $options) {
93
            $value = $array[$key] ?? null;
94
95
            $this->value($value, $key, $options, $messages, $group);
96
        }
97
98
        return $this;
99
    }
100
101
    /**
102
     * Validates an objects properties with the given rules.
103
     *
104
     * @param object     $object
105
     * @param array      $rules
106
     * @param array      $messages
107
     * @param string|int $group
108
     *
109
     * @return self
110
     */
111 View Code Duplication
    public function object(object $object, 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...
112
    {
113
        if (null !== $group && (!is_string($group) || !is_int($group))) {
114
            throw new InvalidArgumentException('The group must be either a string or an integer');
115
        }
116
117
        foreach ($rules as $property => $options) {
118
            $value = $this->getPropertyValue($object, $property);
119
120
            $this->value($value, $property, $options, $messages, $group);
121
        }
122
123
        return $this;
124
    }
125
126
    /**
127
     * Validates request parameters with the given rules.
128
     *
129
     * @param Request    $request
130
     * @param array      $rules
131
     * @param array      $messages
132
     * @param string|int $group
133
     *
134
     * @return self
135
     */
136 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...
137
    {
138
        if (null !== $group && (!is_string($group) || !is_int($group))) {
139
            throw new InvalidArgumentException('The group must be either a string or an integer');
140
        }
141
142
        foreach ($rules as $param => $options) {
143
            $value = $this->getRequestParam($request, $param);
144
145
            $this->value($value, $param, $options, $messages, $group);
146
        }
147
148
        return $this;
149
    }
150
151
    /**
152
     * Validates request parameters, an array or an objects properties.
153
     *
154
     * @param Request|object|array $input
155
     * @param array                $rules
156
     * @param array                $messages
157
     * @param string|int           $group
158
     *
159
     * @return self
160
     */
161
    public function validate($input, array $rules, array $messages = [], $group = null)
162
    {
163
        if (!is_object($input) && !is_array($input)) {
164
            throw new InvalidArgumentException('The input must be either an object or an array');
165
        }
166
167
        if ($input instanceof Request) {
168
            return $this->request($input, $rules, $messages, $group);
169
        } elseif (is_array($input)) {
170
            return $this->array($input, $rules, $messages, $group);
171
        } elseif (is_object($input)) {
172
            return $this->object($input, $rules, $messages, $group);
173
        }
174
175
        return $this;
176
    }
177
178
    /**
179
     * Validates a single value with the given rules.
180
     *
181
     * @param mixed                  $value
182
     * @param string                 $key
183
     * @param RespectValidator|array $rules
184
     * @param array                  $messages
185
     * @param string|int             $group
186
     *
187
     * @return self
188
     */
189
    public function value($value, $key, $rules, array $messages = [], $group = null)
190
    {
191
        if (null !== $group && (!is_string($group) || !is_int($group))) {
192
            throw new InvalidArgumentException('The group must be either a string or an integer');
193
        }
194
195
        try {
196
            if ($rules instanceof RespectValidator) {
197
                $rules->assert($value);
198
            } else {
199
                if (!is_array($rules) || !isset($rules['rules']) || !($rules['rules'] instanceof RespectValidator)) {
200
                    throw new InvalidArgumentException('Validation rules are missing');
201
                }
202
203
                $rules['rules']->assert($value);
204
            }
205
        } catch (NestedValidationException $e) {
206
            // If the 'message' key exists, set it as only message for this param
207
            if (is_array($rules) && isset($rules['message'])) {
208
                if (!is_string($rules['message'])) {
209
                    throw new InvalidArgumentException(sprintf('Expected custom message to be of type string, %s given', gettype($rules['message'])));
210
                }
211
212
                if (null !== $group) {
213
                    $this->errors[$group][$key] = [$rules['message']];
214
                } else {
215
                    $this->errors[$key] = [$rules['message']];
216
                }
217
            } else {
218
                // If the 'messages' key exists, override global messages
219
                $this->setMessages($e, $key, $rules, $messages, $group);
220
            }
221
        }
222
223 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...
224
            $this->values[$group][$key] = $value;
225
        } else {
226
            $this->values[$key] = $value;
227
        }
228
229
        return $this;
230
    }
231
232
    /**
233
     * Gets the error count.
234
     *
235
     * @return int
236
     */
237
    public function count()
238
    {
239
        return count($this->errors);
240
    }
241
242
    /**
243
     * Adds an error for a parameter.
244
     *
245
     * @param string $param
246
     * @param string $message
247
     * @param string $group
248
     *
249
     * @return self
250
     */
251 View Code Duplication
    public function addError($param, $message, $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...
252
    {
253
        if (null !== $group) {
254
            $this->errors[$group][$param][] = $message;
255
        } else {
256
            $this->errors[$param][] = $message;
257
        }
258
259
        return $this;
260
    }
261
262
    /**
263
     * Gets all default messages.
264
     *
265
     * @return array
266
     */
267
    public function getDefaultMessages()
268
    {
269
        return $this->defaultMessages;
270
    }
271
272
    /**
273
     * Gets all errors.
274
     *
275
     * @return array
276
     */
277
    public function getErrors()
278
    {
279
        return $this->errors;
280
    }
281
282
    /**
283
     * Gets the first error of a parameter.
284
     *
285
     * @param string $param
286
     * @param string $group
287
     *
288
     * @return string
289
     */
290
    public function getFirstError($param, $group = null)
291
    {
292
        if (null !== $group) {
293
            if (isset($this->errors[$group][$param])) {
294
                $first = array_slice($this->errors[$group][$param], 0, 1);
295
296
                return array_shift($first);
297
            }
298
        } elseif (isset($this->errors[$param])) {
299
            $first = array_slice($this->errors[$param], 0, 1);
300
301
            return array_shift($first);
302
        }
303
304
        return '';
305
    }
306
307
    /**
308
     * Gets errors of a parameter.
309
     *
310
     * @param string $param
311
     * @param string $group
312
     *
313
     * @return array
314
     */
315
    public function getParamErrors($param, $group = null)
316
    {
317
        if (null !== $group) {
318
            return $this->errors[$group][$param] ?? [];
319
        }
320
321
        return $this->errors[$param] ?? [];
322
    }
323
324
    /**
325
     * Gets the error of a validation rule for a parameter.
326
     *
327
     * @param string $param
328
     * @param string $rule
329
     * @param string $group
330
     *
331
     * @return string
332
     */
333 View Code Duplication
    public function getParamRuleError($param, $rule, $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...
334
    {
335
        if (null !== $group) {
336
            return $this->errors[$group][$param][$rule] ?? '';
337
        }
338
339
        return $this->errors[$param][$rule] ?? '';
340
    }
341
342
    /**
343
     * Gets the value of a request parameter in validated data.
344
     *
345
     * @param string $param
346
     * @param string $group
347
     *
348
     * @return string
349
     */
350 View Code Duplication
    public function getValue($param, $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...
351
    {
352
        if (null !== $group) {
353
            return $this->values[$group][$param] ?? '';
354
        }
355
356
        return $this->values[$param] ?? '';
357
    }
358
359
    /**
360
     * Gets the validated data.
361
     *
362
     * @return array
363
     */
364
    public function getValues()
365
    {
366
        return $this->values;
367
    }
368
369
    /**
370
     * Gets the error storage mode.
371
     *
372
     * @return int
373
     */
374
    public function getErrorStorageMode()
375
    {
376
        return $this->errorStorageMode;
377
    }
378
379
    /**
380
     * Sets the default error message for a validation rule.
381
     *
382
     * @param string $rule
383
     * @param string $message
384
     *
385
     * @return self
386
     */
387
    public function setDefaultMessage($rule, $message)
388
    {
389
        $this->defaultMessages[$rule] = $message;
390
391
        return $this;
392
    }
393
394
    /**
395
     * Sets default error messages.
396
     *
397
     * @param array $messages
398
     *
399
     * @return self
400
     */
401
    public function setDefaultMessages(array $messages)
402
    {
403
        $this->defaultMessages = $messages;
404
405
        return $this;
406
    }
407
408
    /**
409
     * Sets all errors.
410
     *
411
     * @param array $errors
412
     *
413
     * @return self
414
     */
415
    public function setErrors(array $errors)
416
    {
417
        $this->errors = $errors;
418
419
        return $this;
420
    }
421
422
    /**
423
     * Sets the error storage mode.
424
     *
425
     * @param int $errorStorageMode
426
     *
427
     * @return self
428
     */
429
    public function setErrorStorageMode(int $errorStorageMode)
430
    {
431
        $this->errorStorageMode = $errorStorageMode;
432
433
        return $this;
434
    }
435
436
    /**
437
     * Sets the errors of a parameter.
438
     *
439
     * @param string $param
440
     * @param array  $errors
441
     * @param string $group
442
     *
443
     * @return self
444
     */
445 View Code Duplication
    public function setParamErrors($param, array $errors, $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...
446
    {
447
        if (null !== $group) {
448
            $this->errors[$group][$param] = $errors;
449
        } else {
450
            $this->errors[$param] = $errors;
451
        }
452
453
        return $this;
454
    }
455
456
    /**
457
     * Sets the value of a request parameter.
458
     *
459
     * @param string $param
460
     * @param mixed  $value
461
     * @param string $group
462
     *
463
     * @return self
464
     */
465
    public function setValue($param, $value, $group = null)
466
    {
467 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...
468
            $this->values[$group][$param] = $value;
469
        } else {
470
            $this->values[$param] = $value;
471
        }
472
473
        return $this;
474
    }
475
476
    /**
477
     * Sets the values of request parameters.
478
     *
479
     * @param array $values
480
     *
481
     * @return self
482
     */
483
    public function setValues(array $values)
484
    {
485
        $this->values = $values;
486
487
        return $this;
488
    }
489
490
    /**
491
     * Gets the value of a property of an object.
492
     *
493
     * @param object $object
494
     * @param string $propertyName
495
     * @param mixed  $default
496
     *
497
     * @return mixed
498
     */
499
    protected function getPropertyValue(object $object, string $propertyName, $default = null)
500
    {
501
        if (!property_exists($object, $propertyName)) {
502
            return $default;
503
        }
504
505
        $reflectionProperty = new ReflectionProperty($object, $propertyName);
506
        $reflectionProperty->setAccessible(true);
507
508
        return $reflectionProperty->getValue($object);
509
    }
510
511
    /**
512
     * Fetch request parameter value from body or query string (in that order).
513
     *
514
     * @param  Request $request
515
     * @param  string  $key The parameter key.
516
     * @param  string  $default The default value.
517
     *
518
     * @return mixed The parameter value.
519
     */
520
    protected function getRequestParam(Request $request, $key, $default = null)
521
    {
522
        $postParams = $request->getParsedBody();
523
        $getParams = $request->getQueryParams();
524
525
        $result = $default;
526
        if (is_array($postParams) && isset($postParams[$key])) {
527
            $result = $postParams[$key];
528
        } elseif (is_object($postParams) && property_exists($postParams, $key)) {
529
            $result = $postParams->$key;
530
        } elseif (isset($getParams[$key])) {
531
            $result = $getParams[$key];
532
        }
533
534
        return $result;
535
    }
536
537
    /**
538
     * Sets error messages after validation.
539
     *
540
     * @param NestedValidationException $e
541
     * @param string                    $param
542
     * @param AbstractComposite|array   $options
543
     * @param array                     $messages
544
     * @param string|int                $group
545
     */
546
    protected function setMessages(NestedValidationException $e, $param, $options, array $messages = [], $group = null)
547
    {
548
        $paramRules = $options instanceof RespectValidator ? $options->getRules() : $options['rules']->getRules();
549
550
        // Get the names of all rules used for this param
551
        $rulesNames = [];
552
        foreach ($paramRules as $rule) {
553
            $rulesNames[] = lcfirst((new ReflectionClass($rule))->getShortName());
554
        }
555
556
        $params = [
557
            $e->findMessages($rulesNames)
558
        ];
559
560
        // If default messages are defined
561
        if (!empty($this->defaultMessages)) {
562
            $params[] = $e->findMessages($this->defaultMessages);
563
        }
564
565
        // If global messages are defined
566
        if (!empty($messages)) {
567
            $params[] = $e->findMessages($messages);
568
        }
569
570
        // If individual messages are defined
571
        if (is_array($options) && isset($options['messages'])) {
572
            if (!is_array($options['messages'])) {
573
                throw new InvalidArgumentException(sprintf('Expected custom individual messages to be of type array, %s given', gettype($options['messages'])));
574
            }
575
576
            $params[] = $e->findMessages($options['messages']);
577
        }
578
579
        $errors = array_filter(call_user_func_array('array_merge', $params));
580
581
        if (null !== $group) {
582
            $this->errors[$group][$param] = $this->errorStorageMode === self::MODE_ASSOCIATIVE ? $errors : array_values($errors);
583
        } else {
584
            $this->errors[$param] = $this->errorStorageMode === self::MODE_ASSOCIATIVE ? $errors : array_values($errors);
585
        }
586
    }
587
}
588