Passed
Push — master ( 9917a6...b87e63 )
by Alexander
01:57
created

Validator::before()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
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 = [];
0 ignored issues
show
introduced by
The private property $before is not used, and could be removed.
Loading history...
53
54
    /**
55
     * @var array
56
     */
57
    private $after = [];
0 ignored issues
show
introduced by
The private property $after is not used, and could be removed.
Loading history...
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->dataToValidate = $dataToValidate;
80
81
        foreach ($dataToValidate as $field => $fieldData) {
82
            if (!$this->isFieldDefinitionValid($fieldData)) {
83
                throw new ValidatorExceptions("Bad field definition.");
84
            }
85
86
            $value = $fieldData['value'];
87
            $rules = $fieldData['rules'];
88
89
            foreach ($rules as $ruleName => $ruleArguments) {
90
                if (is_int($ruleName) && is_string($ruleArguments)) {
91
                    $ruleName = $ruleArguments;
92
                    $ruleArguments = [];
93
                } elseif (is_string($ruleName) && !is_array($ruleArguments)) {
94
                    $ruleArguments = [$ruleArguments];
95
                }
96
97
                $continue = $this->validateAgainstRule($field, $value, $ruleName, $ruleArguments);
98
                if (!$continue) {
99
                    break;
100
                }
101
            }
102
        }
103
104
        return $this;
105
    }
106
107
    /**
108
     * @return bool
109
     */
110
    public function passed(): bool
111
    {
112
        return $this->errors()->isEmpty();
113
    }
114
115
    /**
116
     * Validates value against a specific rule and handles errors if the rule validation fails.
117
     *
118
     * @param string $field
119
     * @param mixed $value
120
     * @param string $ruleName
121
     * @param array $ruleArguments
122
     * @return bool
123
     */
124
    private function validateAgainstRule(string $field, $value, string $ruleName, array $ruleArguments): bool
125
    {
126
        $ruleToCall = $this->getRuleToCall($ruleName);
127
        $passed = call_user_func_array($ruleToCall, [$value, $this->dataToValidate, $ruleArguments]);
128
        if (!$passed) {
129
            $this->handleError($field, $value, $ruleName, $ruleArguments);
130
            return $this->canSkipRule($ruleToCall);
131
        }
132
133
        return true;
134
    }
135
136
    /**
137
     * Stores an error.
138
     *
139
     * @param string $field
140
     * @param mixed $value
141
     * @param string $ruleName
142
     * @param array $ruleArguments
143
     */
144
    private function handleError(string $field, $value, $ruleName, $ruleArguments)
145
    {
146
        $this->errors[$ruleName][] = [
147
            'field' => $field,
148
            'value' => $value,
149
            'args' => $ruleArguments,
150
        ];
151
    }
152
153
    /**
154
     * If the rule to call specifically doesn't allowing skipping, then we don't want skip the rule.
155
     *
156
     * @param array $ruleToCall
157
     * @return bool
158
     */
159
    private function canSkipRule(array $ruleToCall): bool
160
    {
161
        if (method_exists($ruleToCall[0], 'canSkip')) {
162
            return call_user_func([$ruleToCall[0], 'canSkip']);
163
        }
164
165
        return true;
166
    }
167
168
    /**
169
     * Check that rule field definition is valid array.
170
     *
171
     * @param $fieldDefinition
172
     * @return bool
173
     */
174
    private function isFieldDefinitionValid($fieldDefinition): bool
175
    {
176
        try {
177
            return (
178
                key_exists('value', $fieldDefinition)
179
                && (key_exists('rules', $fieldDefinition) && is_array($fieldDefinition['rules']))
180
            );
181
        } catch (\Exception $exception) {
182
            return false;
183
        }
184
    }
185
186
    /**
187
     * Gets and instantiates a rule object, e.g. IntRule. If it has
188
     * already been used, it pulls from the stored rule objects.
189
     *
190
     * @param  mixed $ruleName
191
     * @return array|callable
192
     * @throws ValidatorRuleException
193
     */
194
    private function getRuleToCall(string $ruleName): array
195
    {
196
        if (isset($this->instantiatedRules[$ruleName])) {
197
            return [$this->instantiatedRules[$ruleName], 'run'];
198
        }
199
200
        if (!isset($this->rulesMap[$ruleName])) {
201
            throw new ValidatorRuleException("Bad rules map definition.");
202
        }
203
204
        $rule = $this->rulesMap[$ruleName];
205
        if (is_callable($rule)) {
206
            return [$rule, '__invoke'];
207
        }
208
209
        if (is_string($rule) && class_exists($rule)) {
210
            $ruleClass = $this->rulesMap[$ruleName];
211
            $rule = new $ruleClass();
212
        }
213
214
        if ($rule instanceof RuleInterface) {
215
            $this->instantiatedRules[$ruleName] = $rule;
216
            return [$rule, 'run'];
217
        }
218
219
        throw new ValidatorRuleException("Bad rule definition.");
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function errors(): MessageBagInterface
226
    {
227
        $messages = [];
228
229
        foreach ($this->errors as $rule => $items) {
230
            foreach ($items as $item) {
231
                $field = $item['field'];
232
                $message = $this->fetchMessage($rule);
233
                $messages[$field][] = $this->replaceMessageFormat($message, $item);
234
            }
235
        }
236
237
        return new MessageBag($messages);
238
    }
239
240
    /**
241
     * Fetch the message for an error rule.
242
     *
243
     * @param string $ruleName
244
     * @return string
245
     * @throws ValidatorRuleException
246
     */
247
    private function fetchMessage(string $ruleName): string
248
    {
249
        if (isset($this->messages[$ruleName])) {
250
            return $this->messages[$ruleName];
251
        }
252
253
        if (isset($this->instantiatedRules[$ruleName])) {
254
            return $this->instantiatedRules[$ruleName]->errorMessage();
255
        }
256
257
        throw new ValidatorRuleException("You must define error message for rule `{$ruleName}`.");
258
    }
259
260
    /**
261
     * Replaces message variables.
262
     *
263
     * @param  string $message
264
     * @param  array $item
265
     * @return string
266
     * @throws ValidatorExceptions
267
     */
268
    private function replaceMessageFormat(string $message, array $item): string
269
    {
270
        if (!empty($item['args'])) {
271
            $args = $item['args'];
272
            $argReplace = array_map(function ($i) {
273
                return "{\${$i}}";
274
            }, array_keys($args));
275
            $args[] = count($item['args']);
276
            $argReplace[] = '{$#}';
277
            $args[] = implode(', ', $item['args']);
278
            $argReplace[] = '{$*}';
279
            $message = str_replace($argReplace, $args, $message);
280
        }
281
282
        if (!key_exists('value', $item) || !key_exists('field', $item)) {
283
            throw new ValidatorExceptions("Bad error message format.");
284
        }
285
        $value = $this->prepareValueForMessage($item['value']);
286
        $field = $this->prepareFieldForMessage($item['field']);
287
288
        $message = str_replace(
289
            ['{field}', '{value}'],
290
            [$field, $value],
291
            $message
292
        );
293
294
        return $message;
295
    }
296
297
    /**
298
     * @param mixed $rawValue
299
     * @return string
300
     */
301
    private function prepareValueForMessage($rawValue): string
302
    {
303
        if (is_scalar($rawValue)) {
304
            $value = (string)$rawValue;
305
        } else {
306
            $value = print_r($rawValue, true);
307
        }
308
        return $value;
309
    }
310
311
    /**
312
     * @param $rawField
313
     * @return string
314
     */
315
    private function prepareFieldForMessage($rawField): string
316
    {
317
        if (isset($this->dataToValidate[$rawField]['alias'])
318
            && is_string($this->dataToValidate[$rawField]['alias'])
319
            && '' !== $this->dataToValidate[$rawField]['alias']
320
        ) {
321
            return $this->dataToValidate[$rawField]['alias'];
322
        }
323
        return $rawField;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329
    public function addRuleMessage(string $rule, string $message): ValidatorInterface
330
    {
331
        if (empty($rule) || empty($message)) {
332
            throw new ValidatorRuleException('Properties `$rule` and `$message` can\'t be empty string.');
333
        }
334
        $this->messages[$rule] = $message;
335
        return $this;
336
    }
337
338
    /**
339
     * {@inheritdoc}
340
     */
341
    public function addRuleMessages(array $messages): ValidatorInterface
342
    {
343
        array_walk($messages, function (string $message, string $rule) {
344
            $this->addRuleMessage($rule, $message);
345
        });
346
        return $this;
347
    }
348
349
    /**
350
     * {@inheritdoc}
351
     */
352
    public function addRule(string $name, $rule): ValidatorInterface
353
    {
354
        if ('' === $name) {
355
            throw new ValidatorRuleException("Rule name must be not empty string");
356
        }
357
358
        if (is_string($rule) || ($rule instanceof RuleInterface) || is_callable($rule)) {
359
            $this->rulesMap[$name] = $rule;
360
            return $this;
361
        }
362
363
        throw new ValidatorRuleException("Rule must be callable, full class name as string or object which implemented RuleInterface.");
364
    }
365
}