Completed
Branch feature/pre-split (e801ec)
by Anton
03:11
created

Validator::checker()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
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\Translator\Traits\TranslatorTrait;
18
use Spiral\Validation\Configs\ValidatorConfig;
19
use Spiral\Validation\Exceptions\ValidationException;
20
21
/**
22
 * Validator is default implementation of ValidatorInterface. Class support functional rules with
23
 * user parameters. In addition, part of validation rules moved into validation checkers used to
24
 * simplify adding new rules, checkers are resolved using container and can be rebinded in
25
 * application.
26
 *
27
 * Examples:
28
 *      "status" => [
29
 *           ["notEmpty"],
30
 *           ["string::shorter", 10, "error" => "Your string is too short."],
31
 *           [["MyClass", "myMethod"], "error" => "Custom validation failed."]
32
 *      ],
33
 *      "email" => [
34
 *           ["notEmpty", "error" => "Please enter your email address."],
35
 *           ["email", "error" => "Email is not valid."]
36
 *      ],
37
 *      "pin" => [
38
 *           ["string::regexp", "/[0-9]{5}/", "error" => "Invalid pin format, if you don't know your
39
 *                                                           pin, please skip this field."]
40
 *      ],
41
 *      "flag" => ["notEmpty", "boolean"]
42
 *
43
 * In cases where you don't need custom message or check parameters you can use simplified
44
 * rule syntax:
45
 *      "flag" => ["notEmpty", "boolean"]
46
 */
47
class Validator extends Component implements ValidatorInterface, LoggerAwareInterface
48
{
49
    use LoggerTrait, TranslatorTrait, SaturateTrait;
50
51
    /**
52
     * Return from validation rule to stop any future field validations. Internal contract.
53
     */
54
    const STOP_VALIDATION = -99;
55
56
    /**
57
     * @invisible
58
     * @var ValidatorConfig
59
     */
60
    private $config = null;
61
62
    /**
63
     * @var array|\Traversable|\ArrayAccess
64
     */
65
    private $data = [];
66
67
    /**
68
     * Validation rules, see class title for description.
69
     *
70
     * @var array
71
     */
72
    private $rules = [];
73
74
    /**
75
     * Error messages raised while validation.
76
     *
77
     * @var array
78
     */
79
    private $errors = [];
80
81
    /**
82
     * Errors provided from outside.
83
     *
84
     * @var array
85
     */
86
    private $registeredErrors = [];
87
88
    /**
89
     * If rule has no definer error message this text will be used instead. Localizable.
90
     *
91
     * @invisible
92
     * @var string
93
     */
94
    protected $defaultMessage = "[[Condition '{condition}' does not meet.]]";
95
96
    /**
97
     * @invisible
98
     * @var ContainerInterface
99
     */
100
    protected $container = null;
101
102
    /**
103
     * {@inheritdoc}
104
     *
105
     * @param array                           $rules     Validation rules.
106
     * @param array|\Traversable|\ArrayAccess $data      Data or model to be validated.
107
     * @param ValidatorConfig                 $config    Saturated using shared container
108
     * @param ContainerInterface              $container Saturated using shared container
109
     *
110
     * @throws ScopeException
111
     */
112
    public function __construct(
113
        array $rules = [],
114
        $data = [],
115
        ValidatorConfig $config = null,
116
        ContainerInterface $container = null
117
    ) {
118
        $this->data = $data;
119
        $this->rules = $rules;
120
121
        $this->config = $this->saturate($config, ValidatorConfig::class);
122
        $this->container = $this->saturate($container, ContainerInterface::class);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 View Code Duplication
    public function setRules(array $rules): ValidatorInterface
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
        if ($this->rules == $rules) {
131
            return $this;
132
        }
133
134
        $this->rules = $rules;
135
        $this->reset();
136
137
        return $this;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143 View Code Duplication
    public function setData($data): ValidatorInterface
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...
144
    {
145
        if ($this->data == $data) {
146
            return $this;
147
        }
148
149
        $this->data = $data;
150
        $this->reset();
151
152
        return $this;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function getData()
159
    {
160
        return $this->data;
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    public function isValid(): bool
167
    {
168
        $this->validate();
169
170
        return empty($this->errors) && empty($this->registeredErrors);
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function registerError(string $field, string $error): ValidatorInterface
177
    {
178
        $this->registeredErrors[$field] = $error;
179
180
        return $this;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function flushRegistered(): ValidatorInterface
187
    {
188
        $this->registeredErrors = [];
189
190
        return $this;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function hasErrors(): bool
197
    {
198
        return !$this->isValid();
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function getErrors(): array
205
    {
206
        $this->validate();
207
208
        return $this->registeredErrors + $this->errors;
209
    }
210
211
    /**
212
     * Receive field from context data or return default value.
213
     *
214
     * @param string $field
215
     * @param mixed  $default
216
     *
217
     * @return mixed
218
     */
219
    public function getValue(string $field, $default = null)
220
    {
221
        $value = isset($this->data[$field]) ? $this->data[$field] : $default;
222
223
        return $value instanceof ValueInterface ? $value->serializeData() : $value;
224
    }
225
226
    /**
227
     * Reset validation state.
228
     */
229
    public function reset()
230
    {
231
        $this->errors = [];
232
        $this->registeredErrors = [];
233
    }
234
235
    /**
236
     * Validate context data with set of validation rules.
237
     */
238
    protected function validate()
239
    {
240
        $this->errors = [];
241
        foreach ($this->rules as $field => $rules) {
242
243
            foreach ($rules as $rule) {
244
                if (isset($this->errors[$field])) {
245
                    //We are validating field till first error
246
                    continue;
247
                }
248
249
                //Condition is either rule itself or first array element
250
                $condition = is_string($rule) ? $rule : $rule[0];
251
                $arguments = is_string($rule) ? [] : $this->fetchArguments($rule);
252
253
                if (empty($this->getValue($field)) && !$this->config->emptyCondition($condition)) {
254
                    //There is no need to validate empty field except for special conditions
255
                    break;
256
                }
257
258
                $result = $this->check($field, $this->getValue($field), $condition, $arguments);
259
260
                if ($result === true) {
261
                    //No errors
262
                    continue;
263
                }
264
265
                if ($result === self::STOP_VALIDATION) {
266
                    //Validation has to be stopped per rule request
267
                    break;
268
                }
269
270
                if ($result instanceof CheckerInterface) {
271
                    //Failed inside checker, this is implementation agreement
272
                    if ($message = $result->getMessage($condition[1])) {
273
274
                        //Checker provides it's own message for condition
275
                        $this->addMessage(
276
                            $field,
277
                            is_string($rule) ? $message : $this->fetchMessage($rule, $message),
278
                            $condition,
279
                            $arguments
280
                        );
281
282
                        continue;
283
                    }
284
                }
285
286
                //Default message
287
                $message = $this->say($this->defaultMessage);
288
289
                //Recording error message
290
                $this->addMessage(
291
                    $field,
292
                    is_string($rule) ? $message : $this->fetchMessage($rule, $message),
293
                    $condition,
294
                    $arguments
295
                );
296
            }
297
        }
298
    }
299
300
    /**
301
     * Check field with given condition. Can return instance of Checker (data is not valid) to
302
     * clarify error.
303
     *
304
     * @param string $field
305
     * @param mixed  $value
306
     * @param mixed  $condition Reference, can be altered if alias exists.
307
     * @param array  $arguments Rule arguments if any.
308
     *
309
     * @return bool|CheckerInterface
310
     * @throws ValidationException
311
     */
312
    protected function check(string $field, $value, &$condition, array $arguments = [])
313
    {
314
        //Supports both class::func and class:func
1 ignored issue
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...
315
        $condition = str_replace('::', ':', $this->config->resolveAlias($condition));
316
317
        try {
318
            if (strpos($condition, ':')) {
319
                $condition = explode(':', $condition);
320
                if ($this->config->hasChecker($condition[0])) {
321
322
                    $checker = $this->makeChecker($condition[0]);
323
                    if (!$result = $checker->check($condition[1], $value, $arguments, $this)) {
0 ignored issues
show
Unused Code introduced by
The call to CheckerInterface::check() has too many arguments starting with $this.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
324
                        //To let validation() method know that message should be handled via Checker
325
                        return $checker;
326
                    }
327
328
                    return $result;
329
                }
330
            }
331
332
            if (is_array($condition)) {
333
                //We are going to resolve class using constructor
334
                $condition[0] = is_object($condition[0])
335
                    ? $condition[0]
336
                    : $this->container->get($condition[0]);
337
            }
338
339
            //Value always coming first
340
            array_unshift($arguments, $value);
341
342
            return call_user_func_array($condition, $arguments);
343
        } catch (\Exception $e) {
344
            $condition = func_get_arg(2);
345 View Code Duplication
            if (is_array($condition)) {
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...
346
                if (is_object($condition[0])) {
347
                    $condition[0] = get_class($condition[0]);
348
                }
349
350
                $condition = join('::', $condition);
351
            }
352
353
            $this->logger()->error(
354
                "Condition '{condition}' failed with '{exception}' while checking '{field}' field.",
355
                compact('condition', 'field') + ['exception' => $e->getMessage()]
356
            );
357
358
            return false;
359
        }
360
    }
361
362
    /**
363
     * Get or create instance of validation checker.
364
     *
365
     * @param string $name
366
     *
367
     * @return CheckerInterface
368
     * @throws ValidationException
369
     */
370
    protected function makeChecker(string $name): CheckerInterface
371
    {
372
        if (!$this->config->hasChecker($name)) {
373
            throw new ValidationException(
374
                "Unable to create validation checker defined by '{$name}' name."
375
            );
376
        }
377
378
        return $this->container->get($this->config->checkerClass($name))->withValidator($this);
379
    }
380
381
    /**
382
     * Fetch validation rule arguments from rule definition.
383
     *
384
     * @param array $rule
385
     *
386
     * @return array
387
     */
388
    private function fetchArguments(array $rule): array
389
    {
390
        unset($rule[0], $rule['message'], $rule['error']);
391
392
        return array_values($rule);
393
    }
394
395
    /**
396
     * Fetch error message from rule definition or use default message. Method will check "message"
397
     * and "error" properties of definition.
398
     *
399
     * @param array  $rule
400
     * @param string $message Default message to use.
401
     *
402
     * @return string
403
     */
404
    private function fetchMessage(array $rule, string $message): string
405
    {
406
        if (isset($rule['message'])) {
407
            return $rule['message'];
408
        }
409
410
        if (isset($rule['error'])) {
411
            return $rule['error'];
412
        }
413
414
        return $message;
415
    }
416
417
    /**
418
     * Register error message for specified field. Rule definition will be interpolated into
419
     * message.
420
     *
421
     * @param string $field
422
     * @param string $message
423
     * @param mixed  $condition
424
     * @param array  $arguments
425
     */
426
    private function addMessage(string $field, string $message, $condition, array $arguments = [])
427
    {
428 View Code Duplication
        if (is_array($condition)) {
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...
429
            if (is_object($condition[0])) {
430
                $condition[0] = get_class($condition[0]);
431
            }
432
433
            $condition = join('::', $condition);
434
        }
435
436
        $this->errors[$field] = \Spiral\interpolate(
437
            $message,
438
            compact('field', 'condition') + $arguments
439
        );
440
    }
441
}