Completed
Pull Request — master (#167)
by Valentin
05:17 queued 59s
created

Validator::fetchMessage()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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