Completed
Push — master ( e83e5b...214be9 )
by Alexander
08:35
created

Validator::validate()   D

Complexity

Conditions 9
Paths 9

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 9
nop 1
dl 0
loc 31
rs 4.909
c 0
b 0
f 0
1
<?php
2
3
4
namespace SolBianca\Validator;
5
6
7
use SolBianca\Validator\Exceptions\ValidatorExceptions;
8
use SolBianca\Validator\Exceptions\ValidatorRuleException;
9
use SolBianca\Validator\Interfaces\MessageBagInterface;
10
use SolBianca\Validator\Interfaces\RuleInterface;
11
use SolBianca\Validator\Interfaces\ValidatorInterface;
12
use SolBianca\Validator\Rules\ArrayRule;
13
use SolBianca\Validator\Rules\BetweenRule;
14
use SolBianca\Validator\Rules\BoolRule;
15
use SolBianca\Validator\Rules\EmailRule;
16
use SolBianca\Validator\Rules\IntRule;
17
use SolBianca\Validator\Rules\IpRule;
18
use SolBianca\Validator\Rules\MatchesRule;
19
use SolBianca\Validator\Rules\MaxRule;
20
use SolBianca\Validator\Rules\MinRule;
21
use SolBianca\Validator\Rules\NumberRule;
22
use SolBianca\Validator\Rules\RegexRule;
23
use SolBianca\Validator\Rules\RequiredRule;
24
use SolBianca\Validator\Rules\UrlRule;
25
26
class Validator implements ValidatorInterface
27
{
28
    private $rulesMap = [
29
        'array' => ArrayRule::class,
30
        'between' => BetweenRule::class,
31
        'bool' => BoolRule::class,
32
        'email' => EmailRule::class,
33
        'int' => IntRule::class,
34
        'ip' => IpRule::class,
35
        'matches' => MatchesRule::class,
36
        'max' => MaxRule::class,
37
        'min' => MinRule::class,
38
        'number' => NumberRule::class,
39
        'regex' => RegexRule::class,
40
        'required' => RequiredRule::class,
41
        'url' => UrlRule::class,
42
    ];
43
44
    /**
45
     * @var RuleInterface[]
46
     */
47
    private $instantiatedRules = [];
48
49
    /**
50
     * @var array
51
     */
52
    private $before = [];
53
54
    /**
55
     * @var array
56
     */
57
    private $after = [];
58
59
    /**
60
     * @var array
61
     */
62
    private $errors = [];
63
64
    /**
65
     * @var array
66
     */
67
    private $dataToValidate = [];
68
69
    /**
70
     * @var array
71
     */
72
    private $messages = [];
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function validate(array $dataToValidate): ValidatorInterface
78
    {
79
        $this->runBeforeCallbacks();
80
        $this->dataToValidate = $dataToValidate;
81
82
        foreach ($dataToValidate as $field => $fieldData) {
83
            if (!$this->isFieldDefinitionValid($fieldData)) {
84
                throw new ValidatorExceptions("Bad field definition.");
85
            }
86
87
            $value = $fieldData['value'];
88
            $rules = $fieldData['rules'];
89
90
            foreach ($rules as $ruleName => $ruleArguments) {
91
                if (is_int($ruleName) && is_string($ruleArguments)) {
92
                    $ruleName = $ruleArguments;
93
                    $ruleArguments = [];
94
                } elseif (is_string($ruleName) && !is_array($ruleArguments)) {
95
                    $ruleArguments = [$ruleArguments];
96
                }
97
98
                $continue = $this->validateAgainstRule($field, $value, $ruleName, $ruleArguments);
99
                if (!$continue) {
100
                    break;
101
                }
102
            }
103
        }
104
105
        $this->runAfterCallbacks();
106
107
        return $this;
108
    }
109
110
    /**
111
     * @return bool
112
     */
113
    public function passed(): bool
114
    {
115
        return $this->errors()->isEmpty();
116
    }
117
118
    /**
119
     * Validates value against a specific rule and handles errors if the rule validation fails.
120
     *
121
     * @param string $field
122
     * @param mixed $value
123
     * @param string $ruleName
124
     * @param array $ruleArguments
125
     * @return bool
126
     */
127
    private function validateAgainstRule(string $field, $value, string $ruleName, array $ruleArguments): bool
128
    {
129
        $ruleToCall = $this->getRuleToCall($ruleName);
130
        $passed = call_user_func_array($ruleToCall, [$value, $this->dataToValidate, $ruleArguments]);
131
        if (!$passed) {
132
            $this->handleError($field, $value, $ruleName, $ruleArguments);
133
            return $this->canSkipRule($ruleToCall);
134
        }
135
136
        return true;
137
    }
138
139
    /**
140
     * Stores an error.
141
     *
142
     * @param string $field
143
     * @param mixed $value
144
     * @param string $ruleName
145
     * @param array $ruleArguments
146
     */
147
    private function handleError(string $field, $value, $ruleName, $ruleArguments)
148
    {
149
        $this->errors[$ruleName][] = [
150
            'field' => $field,
151
            'value' => $value,
152
            'args' => $ruleArguments,
153
        ];
154
    }
155
156
    /**
157
     * If the rule to call specifically doesn't allowing skipping, then we don't want skip the rule.
158
     *
159
     * @param array $ruleToCall
160
     * @return bool
161
     */
162
    private function canSkipRule(array $ruleToCall): bool
163
    {
164
        if (method_exists($ruleToCall[0], 'canSkip')) {
165
            return call_user_func([$ruleToCall[0], 'canSkip']);
166
        }
167
168
        return true;
169
    }
170
171
    /**
172
     * Check that rule field definition is valid array.
173
     *
174
     * @param $fieldDefinition
175
     * @return bool
176
     */
177
    private function isFieldDefinitionValid($fieldDefinition): bool
178
    {
179
        try {
180
            return (
181
                key_exists('value', $fieldDefinition)
182
                && (key_exists('rules', $fieldDefinition) && is_array($fieldDefinition['rules']))
183
            );
184
        } catch (\Exception $exception) {
185
            return false;
186
        }
187
    }
188
189
    /**
190
     * Gets and instantiates a rule object, e.g. IntRule. If it has
191
     * already been used, it pulls from the stored rule objects.
192
     *
193
     * @param  mixed $ruleName
194
     * @return array|callable
195
     * @throws ValidatorRuleException
196
     */
197
    private function getRuleToCall(string $ruleName): array
198
    {
199
        if (isset($this->instantiatedRules[$ruleName])) {
200
            return [$this->instantiatedRules[$ruleName], 'run'];
201
        }
202
203
        if (!isset($this->rulesMap[$ruleName])) {
204
            throw new ValidatorRuleException("Bad rules map definition.");
205
        }
206
207
        $rule = $this->rulesMap[$ruleName];
208
        if (is_callable($rule)) {
209
            return [$rule, '__invoke'];
210
        }
211
212
        if (is_string($rule) && class_exists($rule)) {
213
            $ruleClass = $this->rulesMap[$ruleName];
214
            $rule = new $ruleClass();
215
        }
216
217
        if ($rule instanceof RuleInterface) {
218
            $this->instantiatedRules[$ruleName] = $rule;
219
            return [$rule, 'run'];
220
        }
221
222
        throw new ValidatorRuleException("Bad rule definition.");
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228
    public function errors(): MessageBagInterface
229
    {
230
        $messages = [];
231
232
        foreach ($this->errors as $rule => $items) {
233
            foreach ($items as $item) {
234
                $field = $item['field'];
235
                $message = $this->fetchMessage($rule);
236
                $messages[$field][] = $this->replaceMessageFormat($message, $item);
237
            }
238
        }
239
240
        return new MessageBag($messages);
241
    }
242
243
    /**
244
     * Fetch the message for an error rule.
245
     *
246
     * @param string $ruleName
247
     * @return string
248
     * @throws ValidatorRuleException
249
     */
250
    private function fetchMessage(string $ruleName): string
251
    {
252
        if (isset($this->messages[$ruleName])) {
253
            return $this->messages[$ruleName];
254
        }
255
256
        if (isset($this->instantiatedRules[$ruleName])) {
257
            return $this->instantiatedRules[$ruleName]->errorMessage();
258
        }
259
260
        throw new ValidatorRuleException("You must define error message for rule `{$ruleName}`.");
261
    }
262
263
    /**
264
     * Replaces message variables.
265
     *
266
     * @param  string $message
267
     * @param  array $item
268
     * @return string
269
     * @throws ValidatorExceptions
270
     */
271
    private function replaceMessageFormat(string $message, array $item): string
272
    {
273
        if (!empty($item['args'])) {
274
            $args = $item['args'];
275
            $argReplace = array_map(function ($i) {
276
                return "{\${$i}}";
277
            }, array_keys($args));
278
            $args[] = count($item['args']);
279
            $argReplace[] = '{$#}';
280
            $args[] = implode(', ', $item['args']);
281
            $argReplace[] = '{$*}';
282
            $message = str_replace($argReplace, $args, $message);
283
        }
284
285
        if (!key_exists('value', $item) || !key_exists('field', $item)) {
286
            throw new ValidatorExceptions("Bad error message format.");
287
        }
288
        $value = $this->prepareValueForMessage($item['value']);
289
        $field = $this->prepareFieldForMessage($item['field']);
290
291
        $message = str_replace(
292
            ['{field}', '{value}'],
293
            [$field, $value],
294
            $message
295
        );
296
297
        return $message;
298
    }
299
300
    /**
301
     * @param mixed $rawValue
302
     * @return string
303
     */
304
    private function prepareValueForMessage($rawValue): string
305
    {
306
        if (is_scalar($rawValue)) {
307
            $value = (string)$rawValue;
308
        } else {
309
            $value = print_r($rawValue, true);
310
        }
311
        return $value;
312
    }
313
314
    /**
315
     * @param $rawField
316
     * @return string
317
     */
318
    private function prepareFieldForMessage($rawField): string
319
    {
320
        if (isset($this->dataToValidate[$rawField]['alias'])
321
            && is_string($this->dataToValidate[$rawField]['alias'])
322
            && '' !== $this->dataToValidate[$rawField]['alias']
323
        ) {
324
            return $this->dataToValidate[$rawField]['alias'];
325
        }
326
        return $rawField;
327
    }
328
329
    /**
330
     * {@inheritdoc}
331
     */
332
    public function addRuleMessage(string $rule, string $message): ValidatorInterface
333
    {
334
        if (empty($rule) || empty($message)) {
335
            throw new ValidatorRuleException('Properties `$rule` and `$message` can\'t be empty string.');
336
        }
337
        $this->messages[$rule] = $message;
338
        return $this;
339
    }
340
341
    /**
342
     * {@inheritdoc}
343
     */
344
    public function addRuleMessages(array $messages): ValidatorInterface
345
    {
346
        array_walk($messages, function (string $message, string $rule) {
347
            $this->addRuleMessage($rule, $message);
348
        });
349
        return $this;
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355
    public function addRule(string $name, $rule): ValidatorInterface
356
    {
357
        if ('' === $name) {
358
            throw new ValidatorRuleException("Rule name must be not empty string");
359
        }
360
361
        if (is_string($rule) || ($rule instanceof RuleInterface) || is_callable($rule)) {
362
            $this->rulesMap[$name] = $rule;
363
            return $this;
364
        }
365
366
        throw new ValidatorRuleException("Rule must be callable, full class name as string or object which implemented RuleInterface.");
367
    }
368
369
    /**
370
     * Run callbacks before validation
371
     */
372
    private function runBeforeCallbacks()
373
    {
374
        foreach ($this->before as $before) {
375
            call_user_func_array($before, [$this]);
376
        }
377
    }
378
379
    /**
380
     * Register an before validation callback.
381
     *
382
     * @param  callable $closure
383
     * @return ValidatorInterface
384
     */
385
    public function before(callable $closure): ValidatorInterface
386
    {
387
        $this->before[] = $closure;
388
        return $this;
389
    }
390
391
    /**
392
     * Run callbacks after validation
393
     */
394
    private function runAfterCallbacks()
395
    {
396
        foreach ($this->after as $after) {
397
            call_user_func_array($after, [$this]);
398
        }
399
    }
400
401
    /**
402
     * Register an after validation callback.
403
     *
404
     * @param  callable $closure
405
     * @return ValidatorInterface
406
     */
407
    public function after(callable $closure): ValidatorInterface
408
    {
409
        $this->after[] = $closure;
410
        return $this;
411
    }
412
}