Completed
Branch develop (c2aa4c)
by Anton
05:17
created

Validator::addMessage()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 7
Ratio 46.67 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 7
loc 15
rs 9.4286
cc 3
eloc 8
nc 3
nop 4
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Validation;
9
10
use Interop\Container\ContainerInterface;
11
use Psr\Log\LoggerAwareInterface;
12
use Spiral\Core\Component;
13
use Spiral\Core\Exceptions\SugarException;
14
use Spiral\Core\Traits\SaturateTrait;
15
use Spiral\Debug\Traits\LoggerTrait;
16
use Spiral\Translator\Traits\TranslatorTrait;
17
use Spiral\Validation\Configs\ValidatorConfig;
18
use Spiral\Validation\Exceptions\ValidationException;
19
20
/**
21
 * Validator is default implementation of ValidatorInterface. Class support functional rules with
22
 * user parameters. In addition, part of validation rules moved into validation checkers used to
23
 * simplify adding new rules, checkers are resolved using container and can be rebinded in
24
 * application.
25
 *
26
 * Examples:
27
 *
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
    /**
50
     * Validator will translate default errors and throw log messages when validation rule fails.
51
     */
52
    use LoggerTrait, TranslatorTrait, SaturateTrait;
53
54
    /**
55
     * Return from validation rule to stop any future field validations. Internal contract.
56
     */
57
    const STOP_VALIDATION = -99;
58
59
    /**
60
     * @var array|\ArrayAccess
61
     */
62
    private $data = [];
63
64
    /**
65
     * Validation rules, see class title for description.
66
     *
67
     * @var array
68
     */
69
    private $rules = [];
70
71
    /**
72
     * Error messages raised while validation.
73
     *
74
     * @var array
75
     */
76
    private $errors = [];
77
78
    /**
79
     * Errors provided from outside.
80
     *
81
     * @var array
82
     */
83
    private $registeredErrors = [];
84
85
    /**
86
     * If rule has no definer error message this text will be used instead. Localizable.
87
     *
88
     * @invisible
89
     * @var string
90
     */
91
    protected $defaultMessage = "[[Condition '{condition}' does not meet.]]";
92
93
    /**
94
     * @invisible
95
     * @var ContainerInterface
96
     */
97
    protected $container = null;
98
99
    /**
100
     * @invisible
101
     * @var ValidatorConfig
102
     */
103
    protected $config = null;
104
105
    /**
106
     * {@inheritdoc}
107
     *
108
     * @param ValidatorConfig    $config
109
     * @param ContainerInterface $container
110
     * @throws SugarException
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
        //Let's get validation from shared container if none provided
122
        $this->config = $this->saturate($config, ValidatorConfig::class);
123
124
        //We can use global container as fallback if no default values were provided
125
        $this->container = $this->saturate($container, ContainerInterface::class);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function setRules(array $rules)
132
    {
133
        if ($this->rules == $rules) {
134
            return $this;
135
        }
136
137
        $this->rules = $rules;
138
        $this->errors = [];
139
140
        return $this;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function setData($data)
147
    {
148
        if ($this->data == $data) {
149
            return $this;
150
        }
151
152
        $this->data = $data;
153
        $this->errors = [];
154
155
        return $this;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function isValid()
162
    {
163
        $this->validate();
164
165
        return empty($this->errors);
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    public function registerError($field, $error)
172
    {
173
        $this->registeredErrors[$field] = $error;
174
175
        return $this;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function flushRegistered()
182
    {
183
        $this->registeredErrors = [];
184
185
        return $this;
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    public function hasErrors()
192
    {
193
        return !$this->isValid();
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function getErrors()
200
    {
201
        $this->validate();
202
203
        return $this->registeredErrors + $this->errors;
204
    }
205
206
    /**
207
     * Receive field from context data or return default value.
208
     *
209
     * @param string $field
210
     * @param mixed  $default
211
     * @return mixed
212
     */
213
    public function field($field, $default = null)
214
    {
215
        $value = isset($this->data[$field]) ? $this->data[$field] : $default;
216
217
        return $value instanceof ValueInterface ? $value->serializeData() : $value;
218
    }
219
220
    /**
221
     * Validate context data with set of validation rules.
222
     */
223
    protected function validate()
224
    {
225
        $this->errors = [];
226
        foreach ($this->rules as $field => $rules) {
227
            foreach ($rules as $rule) {
228
                if (isset($this->errors[$field])) {
229
                    //We are validating field till first error
230
                    continue;
231
                }
232
233
                //Condition either rule itself or first array element
234
                $condition = is_string($rule) ? $rule : $rule[0];
235
236
                if (empty($this->field($field)) && !$this->config->emptyCondition($condition)) {
237
                    //There is no need to validate empty field except for special conditions
238
                    break;
239
                }
240
241
                $result = $this->check(
242
                    $field,
243
                    $this->field($field),
244
                    $condition,
245
                    $arguments = is_string($rule) ? [] : $this->fetchArguments($rule)
246
                );
247
248
                if ($result === true) {
249
                    //No errors
250
                    continue;
251
                }
252
253
                if ($result === self::STOP_VALIDATION) {
254
                    //Validation has to be stopped per rule request
255
                    break;
256
                }
257
258
                if ($result instanceof Checker) {
259
                    //Failed inside checker, this is implementation agreement
260
                    if ($message = $result->getMessage($condition[1])) {
261
                        //Checker provides it's own message for condition
262
                        $this->addMessage(
263
                            $field,
264
                            is_string($rule) ? $message : $this->fetchMessage($rule, $message),
265
                            $condition,
266
                            $arguments
267
                        );
268
269
                        continue;
270
                    }
271
                }
272
273
                //Default message
274
                $message = $this->say($this->defaultMessage);
275
276
                //Recording error message
277
                $this->addMessage(
278
                    $field,
279
                    is_string($rule) ? $message : $this->fetchMessage($rule, $message),
280
                    $condition,
281
                    $arguments
282
                );
283
            }
284
        }
285
    }
286
287
    /**
288
     * Check field with given condition. Can return instance of Checker (data is not valid) to
289
     * clarify error.
290
     *
291
     * @param string $field
292
     * @param mixed  $value
293
     * @param mixed  $condition Reference, can be altered if alias exists.
294
     * @param array  $arguments Rule arguments if any.
295
     * @return bool|Checker
296
     * @throws ValidationException
297
     */
298
    protected function check($field, $value, &$condition, array $arguments = [])
299
    {
300
        $condition = $this->config->resolveCondition($condition);
301
302
        try {
303
            if (strpos($condition, '::')) {
304
                $condition = explode('::', $condition);
305
                if ($this->config->hasChecker($condition[0])) {
306
                    $checker = $this->checker($condition[0]);
307
                    if (!$result = $checker->check($condition[1], $value, $arguments, $this)) {
308
                        //To let validation() method know that message should be handled via Checker
309
                        return $checker;
310
                    }
311
312
                    return $result;
313
                }
314
            }
315
316
            if (is_array($condition)) {
317
                //We are going to resolve class using constructor
318
                $condition[0] = is_object($condition[0])
319
                    ? $condition[0]
320
                    : $this->container->get($condition[0]);
321
            }
322
323
            //Value always coming first
324
            array_unshift($arguments, $value);
325
326
            return call_user_func_array($condition, $arguments);
327
        } catch (\ErrorException $exception) {
328
            $condition = func_get_arg(2);
329 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...
330
                if (is_object($condition[0])) {
331
                    $condition[0] = get_class($condition[0]);
332
                }
333
334
                $condition = join('::', $condition);
335
            }
336
337
            $this->logger()->error(
338
                "Condition '{condition}' failed with '{exception}' while checking '{field}' field.",
339
                compact('condition', 'field') + ['exception' => $exception->getMessage()]
340
            );
341
342
            return false;
343
        }
344
    }
345
346
    /**
347
     * Get or create instance of validation checker.
348
     *
349
     * @param string $name
350
     * @return Checker
351
     * @throws ValidationException
352
     */
353
    protected function checker($name)
354
    {
355
        if (!$this->config->hasChecker($name)) {
356
            throw new ValidationException(
357
                "Unable to create validation checker defined by '{$name}' name."
358
            );
359
        }
360
361
        return $this->container->get($this->config->checkerClass($name));
362
    }
363
364
    /**
365
     * Fetch validation rule arguments from rule definition.
366
     *
367
     * @param array $rule
368
     * @return array
369
     */
370
    private function fetchArguments(array $rule)
371
    {
372
        unset($rule[0], $rule['message'], $rule['error']);
373
374
        return array_values($rule);
375
    }
376
377
    /**
378
     * Fetch error message from rule definition or use default message. Method will check "message"
379
     * and "error" properties of definition.
380
     *
381
     * @param array  $rule
382
     * @param string $message Default message to use.
383
     * @return mixed
384
     */
385
    private function fetchMessage(array $rule, $message)
386
    {
387
        if (isset($rule['message'])) {
388
            $message = $rule['message'];
389
        }
390
391
        if (isset($rule['error'])) {
392
            $message = $rule['error'];
393
        }
394
395
        return $message;
396
    }
397
398
    /**
399
     * Register error message for specified field. Rule definition will be interpolated into
400
     * message.
401
     *
402
     * @param string $field
403
     * @param string $message
404
     * @param mixed  $condition
405
     * @param array  $arguments
406
     */
407
    private function addMessage($field, $message, $condition, array $arguments = [])
408
    {
409 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...
410
            if (is_object($condition[0])) {
411
                $condition[0] = get_class($condition[0]);
412
            }
413
414
            $condition = join('::', $condition);
415
        }
416
417
        $this->errors[$field] = \Spiral\interpolate(
418
            $message,
419
            compact('field', 'condition') + $arguments
420
        );
421
    }
422
}