Completed
Push — master ( 645898...c389b9 )
by Anton
09:13 queued 06:21
created

Validator::getChecker()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Validation;
10
11
use Interop\Container\ContainerInterface;
12
use Psr\Log\LoggerAwareInterface;
13
use Spiral\Core\Component;
14
use Spiral\Core\Exceptions\ScopeException;
15
use Spiral\Core\Traits\SaturateTrait;
16
use Spiral\Debug\Traits\LoggerTrait;
17
use Spiral\Models\AccessorInterface;
18
use Spiral\Models\EntityInterface;
19
use Spiral\Translator\Traits\TranslatorTrait;
20
use Spiral\Validation\Configs\ValidatorConfig;
21
use Spiral\Validation\Exceptions\ValidationException;
22
23
/**
24
 * Validator is default implementation of ValidatorInterface. Class support functional rules with
25
 * user parameters. In addition, part of validation rules moved into validation checkers used to
26
 * simplify adding new rules, checkers are resolved using container and can be rebinded in
27
 * application.
28
 *
29
 * Examples:
30
 *      "status" => [
31
 *           ["notEmpty"],
32
 *           ["string::shorter", 10, "error" => "Your string is too short."],
33
 *           [["MyClass", "myMethod"], "error" => "Custom validation failed."]
34
 *      ],
35
 *      "email" => [
36
 *           ["notEmpty", "error" => "Please enter your email address."],
37
 *           ["email", "error" => "Email is not valid."]
38
 *      ],
39
 *      "pin" => [
40
 *           ["string::regexp", "/[0-9]{5}/", "error" => "Invalid pin format, if you don't know your
41
 *                                                           pin, please skip this field."]
42
 *      ],
43
 *      "flag" => ["notEmpty", "boolean"]
44
 *
45
 * In cases where you don't need custom message or check parameters you can use simplified
46
 * rule syntax:
47
 *      "flag" => ["notEmpty", "boolean"]
48
 */
49
class Validator extends Component implements ValidatorInterface, LoggerAwareInterface
50
{
51
    use LoggerTrait, TranslatorTrait, SaturateTrait;
52
53
    /**
54
     * Return from validation rule to stop any future field validations. Internal contract.
55
     */
56
    const STOP_VALIDATION = -99;
57
58
    /**
59
     * @invisible
60
     * @var ValidatorConfig
61
     */
62
    private $config = null;
63
64
    /**
65
     * @var array|\ArrayAccess
66
     */
67
    private $data = [];
68
69
    /**
70
     * Validation rules, see class title for description.
71
     *
72
     * @var array
73
     */
74
    private $rules = [];
75
76
    /**
77
     * Error messages raised while validation.
78
     *
79
     * @var array
80
     */
81
    private $errors = [];
82
83
    /**
84
     * Errors provided from outside.
85
     *
86
     * @var array
87
     */
88
    private $registeredErrors = [];
89
90
    /**
91
     * If rule has no definer error message this text will be used instead. Localizable.
92
     *
93
     * @invisible
94
     * @var string
95
     */
96
    protected $defaultMessage = "[[Condition '{condition}' does not meet.]]";
97
98
    /**
99
     * @invisible
100
     * @var ContainerInterface
101
     */
102
    protected $container = null;
103
104
    /**
105
     * {@inheritdoc}
106
     *
107
     * @param array              $rules     Validation rules.
108
     * @param array|\ArrayAccess $data      Data or model to be validated.
109
     * @param ValidatorConfig    $config    Saturated using shared container
110
     * @param ContainerInterface $container Saturated using shared container
111
     *
112
     * @throws ScopeException
113
     */
114
    public function __construct(
115
        array $rules = [],
116
        $data = [],
117
        ValidatorConfig $config = null,
118
        ContainerInterface $container = null
119
    ) {
120
        $this->data = $data;
121
        $this->rules = $rules;
122
123
        $this->config = $this->saturate($config, ValidatorConfig::class);
124
        $this->container = $this->saturate($container, ContainerInterface::class);
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function setRules(array $rules): ValidatorInterface
131
    {
132
        if ($this->rules == $rules) {
133
            return $this;
134
        }
135
136
        $this->rules = $rules;
137
        $this->reset();
138
139
        return $this;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function setData($data): ValidatorInterface
146
    {
147
        $data = $this->extractData($data);
148
        if ($this->data === $data) {
149
            return $this;
150
        }
151
152
        $this->data = $data;
153
        $this->reset();
154
155
        return $this;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function getData()
162
    {
163
        return $this->data;
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function isValid(): bool
170
    {
171
        $this->validate();
172
173
        return empty($this->errors) && empty($this->registeredErrors);
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function registerError(string $field, string $error): ValidatorInterface
180
    {
181
        $this->registeredErrors[$field] = $error;
182
183
        return $this;
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189
    public function flushRegistered(): ValidatorInterface
190
    {
191
        $this->registeredErrors = [];
192
193
        return $this;
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function hasErrors(): bool
200
    {
201
        return !$this->isValid();
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207
    public function getErrors(): array
208
    {
209
        $this->validate();
210
211
        return $this->registeredErrors + $this->errors;
212
    }
213
214
    /**
215
     * Receive field from context data or return default value.
216
     *
217
     * @param string $field
218
     * @param mixed  $default
219
     *
220
     * @return mixed
221
     */
222
    public function getValue(string $field, $default = null)
223
    {
224
        $value = isset($this->data[$field]) ? $this->data[$field] : $default;
225
226
        return ($value instanceof EntityInterface || $value instanceof AccessorInterface)
227
            ? $value->packValue()
0 ignored issues
show
Bug introduced by
The method packValue does only exist in Spiral\Models\AccessorInterface, but not in Spiral\Models\EntityInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
228
            : $value;
229
    }
230
231
    /**
232
     * Reset validation state.
233
     */
234
    public function reset()
235
    {
236
        $this->errors = [];
237
        $this->registeredErrors = [];
238
    }
239
240
    /**
241
     * Validate context data with set of validation rules.
242
     */
243
    protected function validate()
244
    {
245
        $this->errors = [];
246
        foreach ($this->rules as $field => $rules) {
247
248
            foreach ($rules as $rule) {
249
                if (isset($this->errors[$field])) {
250
                    //We are validating field till first error
251
                    continue;
252
                }
253
254
                //Condition is either rule itself or first array element
255
                $condition = is_string($rule) ? $rule : $rule[0];
256
                $arguments = is_string($rule) ? [] : $this->fetchArguments($rule);
257
258
                if (empty($this->getValue($field)) && !$this->config->emptyCondition($condition)) {
259
                    //There is no need to validate empty field except for special conditions
260
                    break;
261
                }
262
263
                $result = $this->check($field, $this->getValue($field), $condition, $arguments);
264
265
                if ($result === true) {
266
                    //No errors
267
                    continue;
268
                }
269
270
                if ($result === self::STOP_VALIDATION) {
271
                    //Validation has to be stopped per rule request
272
                    break;
273
                }
274
275
                if ($result instanceof CheckerInterface) {
276
                    //Failed inside checker, this is implementation agreement
277
                    if ($message = $result->getMessage($condition[1])) {
278
279
                        //Checker provides it's own message for condition
280
                        $this->addMessage(
281
                            $field,
282
                            is_string($rule) ? $message : $this->fetchMessage($rule, $message),
283
                            $condition,
284
                            $arguments
285
                        );
286
287
                        continue;
288
                    }
289
                }
290
291
                //Default message
292
                $message = $this->say($this->defaultMessage);
293
294
                //Recording error message
295
                $this->addMessage(
296
                    $field,
297
                    is_string($rule) ? $message : $this->fetchMessage($rule, $message),
298
                    $condition,
299
                    $arguments
300
                );
301
            }
302
        }
303
    }
304
305
    /**
306
     * Check field with given condition. Can return instance of Checker (data is not valid) to
307
     * clarify error.
308
     *
309
     * @param string $field
310
     * @param mixed  $value
311
     * @param mixed  $condition Reference, can be altered if alias exists.
312
     * @param array  $arguments Rule arguments if any.
313
     *
314
     * @return bool|CheckerInterface
315
     * @throws ValidationException
316
     */
317
    protected function check(string $field, $value, &$condition, array $arguments = [])
318
    {
319
        //Supports both class::func and class:func
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
320
        $condition = str_replace('::', ':', $this->config->resolveAlias($condition));
321
322
        try {
323
            if (!is_array($condition) && strpos($condition, ':')) {
324
                $condition = explode(':', $condition);
325
                if ($this->hasChecker($condition[0])) {
326
                    $checker = $this->getChecker($condition[0]);
327
                    $result = $checker->check($condition[1], $value, $arguments);
328
329
                    if ($result === false) {
330
                        //To let validation() method know that message should be handled via Checker
331
                        return $checker;
332
                    }
333
334
                    return $result;
335
                }
336
            }
337
338
            if (is_array($condition)) {
339
                //We are going to resolve class using constructor
340
                $condition[0] = is_object($condition[0])
341
                    ? $condition[0]
342
                    : $this->container->get($condition[0]);
343
            }
344
345
            //Value always coming first
346
            array_unshift($arguments, $value);
347
348
            return call_user_func_array($condition, $arguments);
349
        } catch (\Error $e) {
350
            throw new ValidationException("Invalid rule definition", $e->getCode(), $e);
351
        } catch (\Throwable $e) {
352
            $this->logException($field, func_get_arg(2), $e);
353
354
            return false;
355
        }
356
    }
357
358
    /**
359
     * Does validation config has alias defined for a given checker name or class exists
360
     *
361
     * @param string $name
362
     *
363
     * @return bool
364
     */
365
    protected function hasChecker(string $name): bool
366
    {
367
        if ($this->config->hasChecker($name)) {
368
            return true;
369
        }
370
371
        if (class_exists($name)) {
372
            $checker = $this->container->get($name);
373
374
            return $checker instanceof CheckerInterface;
375
        }
376
377
        return false;
378
    }
379
380
    /**
381
     * Get or create instance of validation checker.
382
     *
383
     * @param string $name
384
     *
385
     * @return CheckerInterface
386
     * @throws ValidationException
387
     */
388
    protected function getChecker(string $name): CheckerInterface
389
    {
390
        if (!$this->hasChecker($name)) {
391
            throw new ValidationException(
392
                "Unable to create validation checker defined by '{$name}' name"
393
            );
394
        }
395
396
        /** @var string $name */
397
        $name = $this->config->hasChecker($name) ? $this->config->checkerClass($name) : $name;
398
399
        return $this->container->get($name)->withValidator($this);
400
    }
401
402
    /**
403
     * Fetch validation rule arguments from rule definition.
404
     *
405
     * @param array $rule
406
     *
407
     * @return array
408
     */
409
    private function fetchArguments(array $rule): array
410
    {
411
        unset($rule[0], $rule['message'], $rule['error']);
412
413
        return array_values($rule);
414
    }
415
416
    /**
417
     * Fetch error message from rule definition or use default message. Method will check "message"
418
     * and "error" properties of definition.
419
     *
420
     * @param array  $rule
421
     * @param string $message Default message to use.
422
     *
423
     * @return string
424
     */
425
    private function fetchMessage(array $rule, string $message): string
426
    {
427
        if (isset($rule['message'])) {
428
            return $rule['message'];
429
        }
430
431
        if (isset($rule['error'])) {
432
            return $rule['error'];
433
        }
434
435
        return $message;
436
    }
437
438
    /**
439
     * Register error message for specified field. Rule definition will be interpolated into
440
     * message.
441
     *
442
     * @param string $field
443
     * @param string $message
444
     * @param mixed  $condition
445
     * @param array  $arguments
446
     */
447
    private function addMessage(string $field, string $message, $condition, array $arguments = [])
448
    {
449
        if (is_array($condition)) {
450
            if (is_object($condition[0])) {
451
                $condition[0] = get_class($condition[0]);
452
            }
453
454
            $condition = join('::', $condition);
455
        }
456
457
        $this->errors[$field] = \Spiral\interpolate(
458
            $message,
459
            compact('field', 'condition') + $arguments
460
        );
461
    }
462
463
    /**
464
     * @param string     $field
465
     * @param array      $condition
466
     * @param \Throwable $e
467
     */
468
    protected function logException(string $field, $condition, \Throwable $e)
469
    {
470
        if (is_array($condition)) {
471
            if (is_object($condition[0])) {
472
                $condition[0] = get_class($condition[0]);
473
            }
474
475
            $condition = join('::', $condition);
476
        }
477
478
        $this->logger()->error(
479
            "Condition '{condition}' failed with '{e}' while checking '{field}' field.",
480
            compact('condition', 'field') + ['e' => $e->getMessage()]
481
        );
482
    }
483
484
    /**
485
     * @param array|\ArrayAccess|EntityInterface $data
486
     *
487
     * @return array
488
     */
489
    private function extractData($data): array
490
    {
491
        if ($data instanceof EntityInterface) {
492
            return $data->getFields();
493
        }
494
495
        //Under consideration, might not be required
496
        if ($data instanceof \ArrayAccess && $data instanceof \Traversable) {
497
            $result = [];
498
            foreach ($data as $key => $value) {
499
                $result[$key] = $value;
500
            }
501
502
            return $result;
503
        }
504
505
        return $data;
506
    }
507
}