Completed
Push — master ( a97ce7...6f972b )
by Jan
03:17
created

Validator::getMessagesForViolations()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 18
rs 9.9666
1
<?php
2
3
namespace Kontrolio;
4
5
use Kontrolio\Data\Attribute;
6
use Kontrolio\Data\Data;
7
use Kontrolio\Error\Errors;
8
use Kontrolio\Rules\Core\Sometimes;
9
use Kontrolio\Rules\Core\UntilFirstFailure;
10
use Kontrolio\Rules\ArrayNormalizer;
11
use Kontrolio\Rules\Instantiator;
12
use Kontrolio\Rules\StopsFurtherValidationInterface;
13
use Kontrolio\Rules\Parser;
14
use Kontrolio\Rules\Repository;
15
use Kontrolio\Rules\CallableRuleWrapper;
16
use Kontrolio\Rules\RuleInterface;
17
use UnexpectedValueException;
18
19
/**
20
 * Main class of the validation service.
21
 *
22
 * @package Kontrolio
23
 */
24
class Validator implements ValidatorInterface
25
{
26
    /**
27
     * Data to validate.
28
     *
29
     * @var Data
30
     */
31
    protected $data;
32
    
33
    /**
34
     * Raw validation rules.
35
     *
36
     * @var array
37
     */
38
    protected $rules = [];
39
40
    /**
41
     * Formatted validation rules.
42
     *
43
     * @var array
44
     */
45
    protected $normalizedRules = [];
46
47
    /**
48
     * Validation messages.
49
     *
50
     * @var array
51
     */
52
    protected $messages = [];
53
54
    private $repository;
55
    private $normalizer;
56
57
    /**
58
     * Validation errors.
59
     *
60
     * @var Errors
61
     */
62
    protected $errors;
63
64
    /**
65
     * Flag indicating that the validation should stop.
66
     *
67
     * @var bool
68
     */
69
    protected $shouldStopOnFirstFailure = false;
70
71
    /**
72
     * Flag indication that the validation of the current attribute should stop.
73
     *
74
     * @var bool
75
     */
76
    protected $shouldStopWithinAttribute = false;
77
78
    /**
79
     * Flag indicating that the validation can be bypassed.
80
     *
81
     * @var bool
82
     */
83
    private $bypass = false;
84
85
    /**
86
     * Validator constructor.
87
     *
88
     * @param array $data
89
     * @param array $rules
90
     * @param array $messages
91
     */
92
    public function __construct(array $data, array $rules, array $messages = [])
93
    {
94
        $this->data = new Data($data);
95
        $this->rules = $rules;
96
        $this->messages = $messages;
97
        $instantiator = new Instantiator();
98
        $this->repository = new Repository($instantiator);
99
        $this->normalizer = new ArrayNormalizer(new Parser($this->repository));
100
        $this->errors = new Errors($messages);
101
    }
102
103
    /**
104
     * Extends available rules with new ones.
105
     *
106
     * @param array $rules
107
     *
108
     * @return $this
109
     */
110
    public function extend(array $rules)
111
    {
112
        $this->repository->add($rules);
113
114
        return $this;
115
    }
116
117
    /**
118
     * Returns all available validation rules.
119
     *
120
     * @return array
121
     */
122
    public function getAvailable()
123
    {
124
        return $this->repository->all();
125
    }
126
127
    /**
128
     * Returns data that's being validated.
129
     *
130
     * @return array
131
     */
132
    public function getData()
133
    {
134
        return $this->data->raw();
135
    }
136
137
    /**
138
     * Sets data that need to be validated.
139
     *
140
     * @param array $data
141
     *
142
     * @return $this
143
     */
144
    public function setData(array $data)
145
    {
146
        $this->data = new Data($data);
147
148
        return $this;
149
    }
150
151
    /**
152
     * Returns validation rules.
153
     *
154
     * @return array
155
     */
156
    public function getRules()
157
    {
158
        return $this->rules;
159
    }
160
161
    /**
162
     * Checks proper format of the validation rules and sets them for the validator.
163
     *
164
     * @param array $rules
165
     *
166
     * @return $this
167
     * @throws UnexpectedValueException
168
     */
169
    public function setRules(array $rules)
170
    {
171
        $this->rules = $rules;
172
        $this->normalizedRules = $this->normalizer->normalize($rules);
173
174
        return $this;
175
    }
176
177
    /**
178
     * Returns validation messages.
179
     *
180
     * @return array
181
     */
182
    public function getMessages()
183
    {
184
        return $this->messages;
185
    }
186
187
    /**
188
     * Sets validation messages.
189
     *
190
     * @param array $messages
191
     *
192
     * @return $this
193
     */
194
    public function setMessages(array $messages)
195
    {
196
        $this->messages = $messages;
197
198
        return $this;
199
    }
200
201
    /**
202
     * Validates given data.
203
     *
204
     * @return bool
205
     * @throws UnexpectedValueException
206
     */
207
    public function validate()
208
    {
209
        if (empty($this->normalizedRules)) {
210
            $this->normalizedRules = $this->normalizer->normalize($this->rules);
211
        }
212
213
        foreach ($this->normalizedRules as $attrName => $rules) {
214
            $this->bypass = false;
215
            $this->shouldStopWithinAttribute = false;
216
217
            foreach ($rules as $rule) {
218
                if ($rule instanceof UntilFirstFailure) {
219
                    $this->shouldStopWithinAttribute = true;
220
                }
221
222
                $attribute = $this->data->get($attrName);
223
                $rule = $this->resolveRule($rule, $attribute);
224
225
                $this->handle($rule, $attribute);
226
227
                if ($this->shouldProceedToTheNextAttribute($attrName)) {
228
                    continue 2;
229
                }
230
                
231
                if ($this->shouldStopOnFailure($rule, $attrName)) {
232
                    return false;
233
                }
234
            }
235
        }
236
        
237
        return $this->errors->isEmpty();
238
    }
239
240
    /**
241
     * Processes a data attribute and creates new validation error message if the validation failed.
242
     *
243
     * @param RuleInterface $rule
244
     * @param Attribute $attribute
245
     * @throws UnexpectedValueException
246
     */
247
    protected function handle($rule, Attribute $attribute)
248
    {
249
        if ($rule instanceof Sometimes && $attribute->isEmpty()) {
250
            $this->bypass = true;
251
252
            return;
253
        }
254
255
        $raw = $attribute->getValue();
256
257
        if ($rule->isValid($raw) ||
258
            $rule->canSkipValidation($raw) ||
259
            ($rule->emptyValueAllowed() && $attribute->isEmpty())) {
260
            return;
261
        }
262
263
        $this->errors->add($attribute, $rule);
264
    }
265
266
    /**
267
     * Resolves validation rule according to actual rule type.
268
     *
269
     * @param callable|RuleInterface $rule
270
     * @param mixed $value
271
     *
272
     * @return RuleInterface
273
     * @throws UnexpectedValueException
274
     */
275
    protected function resolveRule($rule, Attribute $value)
276
    {
277
        if ($rule instanceof RuleInterface) {
278
            return $rule;
279
        }
280
        
281
        if (is_callable($rule)) {
282
            return new CallableRuleWrapper($rule($value->getValue()));
283
        }
284
285
        throw new UnexpectedValueException(sprintf('Rule must implement `%s` or be callable.', RuleInterface::class));
286
    }
287
288
    /**
289
     * Determines whether validation should stop on the first failure.
290
     *
291
     * @param string $attribute
292
     * @param RuleInterface $rule
293
     *
294
     * @return bool
295
     */
296
    protected function shouldStopOnFailure(RuleInterface $rule, $attribute)
297
    {
298
        return (
299
            $rule instanceof StopsFurtherValidationInterface ||
300
            $this->shouldStopOnFirstFailure ||
301
            $this->shouldStopWithinAttribute
302
        ) && $this->errors->has($attribute);
303
    }
304
305
    /**
306
     * Determines whether validator should proceed to the next attribute
307
     *
308
     * @param string $attribute
309
     *
310
     * @return bool
311
     */
312
    protected function shouldProceedToTheNextAttribute($attribute)
313
    {
314
        return $this->bypass || ($this->shouldStopWithinAttribute && $this->errors->has($attribute));
315
    }
316
317
    /**
318
     * Checks whether there are validation errors.
319
     *
320
     * @return bool
321
     */
322
    public function hasErrors()
323
    {
324
        return !$this->errors->isEmpty();
325
    }
326
327
    /**
328
     * Returns all validation errors.
329
     *
330
     * @return array
331
     */
332
    public function getErrors()
333
    {
334
        return $this->errors->raw();
335
    }
336
337
    /**
338
     * Returns a plain validation errors array without their attribute names.
339
     *
340
     * @return array
341
     */
342
    public function getErrorsList()
343
    {
344
        return $this->errors->flatten();
345
    }
346
347
    /**
348
     * Determines whether validation should stop on the first failure.
349
     *
350
     * @param bool $stop
351
     *
352
     * @return $this
353
     */
354
    public function shouldStopOnFirstFailure($stop = true)
355
    {
356
        $this->shouldStopOnFirstFailure = $stop;
357
358
        return $this;
359
    }
360
}
361