Completed
Branch 09branch (1e97b6)
by Anton
02:49
created

Validator::logException()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 7
Ratio 46.67 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 3
dl 7
loc 15
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 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...
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 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...
146
    {
147
        if ($this->data == $data) {
148
            return $this;
149
        }
150
151
        $this->data = $data;
152
        $this->reset();
153
154
        return $this;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function getData()
161
    {
162
        return $this->data;
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function isValid(): bool
169
    {
170
        $this->validate();
171
172
        return empty($this->errors) && empty($this->registeredErrors);
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178
    public function registerError(string $field, string $error): ValidatorInterface
179
    {
180
        $this->registeredErrors[$field] = $error;
181
182
        return $this;
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function flushRegistered(): ValidatorInterface
189
    {
190
        $this->registeredErrors = [];
191
192
        return $this;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function hasErrors(): bool
199
    {
200
        return !$this->isValid();
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function getErrors(): array
207
    {
208
        $this->validate();
209
210
        return $this->registeredErrors + $this->errors;
211
    }
212
213
    /**
214
     * Receive field from context data or return default value.
215
     *
216
     * @param string $field
217
     * @param mixed  $default
218
     *
219
     * @return mixed
220
     */
221
    public function getValue(string $field, $default = null)
222
    {
223
        $value = isset($this->data[$field]) ? $this->data[$field] : $default;
224
225
        return ($value instanceof EntityInterface || $value instanceof AccessorInterface)
226
            ? $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...
227
            : $value;
228
    }
229
230
    /**
231
     * Reset validation state.
232
     */
233
    public function reset()
234
    {
235
        $this->errors = [];
236
        $this->registeredErrors = [];
237
    }
238
239
    /**
240
     * Validate context data with set of validation rules.
241
     */
242
    protected function validate()
243
    {
244
        $this->errors = [];
245
        foreach ($this->rules as $field => $rules) {
246
247
            foreach ($rules as $rule) {
248
                if (isset($this->errors[$field])) {
249
                    //We are validating field till first error
250
                    continue;
251
                }
252
253
                //Condition is either rule itself or first array element
254
                $condition = is_string($rule) ? $rule : $rule[0];
255
                $arguments = is_string($rule) ? [] : $this->fetchArguments($rule);
256
257
                if (empty($this->getValue($field)) && !$this->config->emptyCondition($condition)) {
258
                    //There is no need to validate empty field except for special conditions
259
                    break;
260
                }
261
262
                $result = $this->check($field, $this->getValue($field), $condition, $arguments);
263
264
                if ($result === true) {
265
                    //No errors
266
                    continue;
267
                }
268
269
                if ($result === self::STOP_VALIDATION) {
270
                    //Validation has to be stopped per rule request
271
                    break;
272
                }
273
274
                if ($result instanceof CheckerInterface) {
275
                    //Failed inside checker, this is implementation agreement
276
                    if ($message = $result->getMessage($condition[1])) {
277
278
                        //Checker provides it's own message for condition
279
                        $this->addMessage(
280
                            $field,
281
                            is_string($rule) ? $message : $this->fetchMessage($rule, $message),
282
                            $condition,
283
                            $arguments
284
                        );
285
286
                        continue;
287
                    }
288
                }
289
290
                //Default message
291
                $message = $this->say($this->defaultMessage);
292
293
                //Recording error message
294
                $this->addMessage(
295
                    $field,
296
                    is_string($rule) ? $message : $this->fetchMessage($rule, $message),
297
                    $condition,
298
                    $arguments
299
                );
300
            }
301
        }
302
    }
303
304
    /**
305
     * Check field with given condition. Can return instance of Checker (data is not valid) to
306
     * clarify error.
307
     *
308
     * @param string $field
309
     * @param mixed  $value
310
     * @param mixed  $condition Reference, can be altered if alias exists.
311
     * @param array  $arguments Rule arguments if any.
312
     *
313
     * @return bool|CheckerInterface
314
     * @throws ValidationException
315
     */
316
    protected function check(string $field, $value, &$condition, array $arguments = [])
317
    {
318
        //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...
319
        $condition = str_replace('::', ':', $this->config->resolveAlias($condition));
320
321
        try {
322
            if (!is_array($condition) && strpos($condition, ':')) {
323
                $condition = explode(':', $condition);
324
                if ($this->config->hasChecker($condition[0])) {
325
326
                    $checker = $this->makeChecker($condition[0]);
327
                    if (!$result = $checker->check($condition[1], $value, $arguments)) {
328
                        //To let validation() method know that message should be handled via Checker
329
                        return $checker;
330
                    }
331
332
                    return $result;
333
                }
334
            }
335
336
            if (is_array($condition)) {
337
                //We are going to resolve class using constructor
338
                $condition[0] = is_object($condition[0])
339
                    ? $condition[0]
340
                    : $this->container->get($condition[0]);
341
            }
342
343
            //Value always coming first
344
            array_unshift($arguments, $value);
345
346
            return call_user_func_array($condition, $arguments);
347
        } catch (\Error $e) {
348
            throw new ValidationException("Invalid rule definition", $e->getCode(), $e);
349
        } catch (\Throwable $e) {
350
            $this->logException($field, func_get_arg(2), $e);
351
352
            return false;
353
        }
354
    }
355
356
    /**
357
     * Get or create instance of validation checker.
358
     *
359
     * @param string $name
360
     *
361
     * @return CheckerInterface
362
     * @throws ValidationException
363
     */
364
    protected function makeChecker(string $name): CheckerInterface
365
    {
366
        if (!$this->config->hasChecker($name)) {
367
            throw new ValidationException(
368
                "Unable to create validation checker defined by '{$name}' name."
369
            );
370
        }
371
372
        return $this->container->get($this->config->checkerClass($name))->withValidator($this);
373
    }
374
375
    /**
376
     * Fetch validation rule arguments from rule definition.
377
     *
378
     * @param array $rule
379
     *
380
     * @return array
381
     */
382
    private function fetchArguments(array $rule): array
383
    {
384
        unset($rule[0], $rule['message'], $rule['error']);
385
386
        return array_values($rule);
387
    }
388
389
    /**
390
     * Fetch error message from rule definition or use default message. Method will check "message"
391
     * and "error" properties of definition.
392
     *
393
     * @param array  $rule
394
     * @param string $message Default message to use.
395
     *
396
     * @return string
397
     */
398
    private function fetchMessage(array $rule, string $message): string
399
    {
400
        if (isset($rule['message'])) {
401
            return $rule['message'];
402
        }
403
404
        if (isset($rule['error'])) {
405
            return $rule['error'];
406
        }
407
408
        return $message;
409
    }
410
411
    /**
412
     * Register error message for specified field. Rule definition will be interpolated into
413
     * message.
414
     *
415
     * @param string $field
416
     * @param string $message
417
     * @param mixed  $condition
418
     * @param array  $arguments
419
     */
420
    private function addMessage(string $field, string $message, $condition, array $arguments = [])
421
    {
422 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...
423
            if (is_object($condition[0])) {
424
                $condition[0] = get_class($condition[0]);
425
            }
426
427
            $condition = join('::', $condition);
428
        }
429
430
        $this->errors[$field] = \Spiral\interpolate(
431
            $message,
432
            compact('field', 'condition') + $arguments
433
        );
434
    }
435
436
    /**
437
     * @param string     $field
438
     * @param array      $condition
439
     * @param \Throwable $e
440
     */
441
    protected function logException(string $field, $condition, \Throwable $e)
442
    {
443 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...
444
            if (is_object($condition[0])) {
445
                $condition[0] = get_class($condition[0]);
446
            }
447
448
            $condition = join('::', $condition);
449
        }
450
451
        $this->logger()->error(
452
            "Condition '{condition}' failed with '{e}' while checking '{field}' field.",
453
            compact('condition', 'field') + ['e' => $e->getMessage()]
454
        );
455
    }
456
}