Validator::isFieldDefinitionValid()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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