Validator::validateValue()   F
last analyzed

Complexity

Conditions 68
Paths 408

Size

Total Lines 187

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 109
CRAP Score 68

Importance

Changes 0
Metric Value
dl 0
loc 187
ccs 109
cts 109
cp 1
rs 0.6576
c 0
b 0
f 0
cc 68
nc 408
nop 2
crap 68

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
/**
3
 * Validation.
4
 *
5
 * @copyright Copyright (c) 2016 Starweb AB
6
 * @license   BSD 3-Clause
7
 */
8
9
namespace Starlit\Validation;
10
11
use Symfony\Contracts\Translation\TranslatorInterface as SymfonyTranslatorInterface;
12
13
/**
14
 * A simple validator.
15
 */
16
class Validator
17
{
18
    /**
19
     * @var array
20
     */
21
    protected static $validRuleProperties = [
22
        'required',
23
        'nonEmpty',
24
        'min',
25
        'max',
26
        'minLength',
27
        'maxLength',
28
        'regexp',
29
        'regexpExpl',
30
        'email',
31
        'textKey',
32
        'date',
33
        'dateTime',
34
        'nullable',
35
        'custom'
36
    ];
37
38
    /**
39
     * @var ValidatorTranslatorInterface
40
     */
41
    protected $translator;
42
43
    /**
44
     * @var array
45
     */
46
    protected $validatedData = [];
47
48
    /**
49
     * @var array
50
     */
51
    protected $fieldsRuleProperties = [];
52
53
    /**
54
     * Constructor.
55
     *
56
     * @param array                                                        $fieldsRuleProperties
57
     * @param ValidatorTranslatorInterface|SymfonyTranslatorInterface|null $translator
58
     */
59 47
    public function __construct(array $fieldsRuleProperties = [], $translator = null)
60
    {
61 47
        $this->fieldsRuleProperties = $fieldsRuleProperties;
62
63 47
        if (!$translator) {
64 46
            $this->translator = new DefaultValidatorTranslator();
65 5
        } elseif ($translator instanceof ValidatorTranslatorInterface) {
66 2
            $this->translator = $translator;
67 3
        } elseif ($translator instanceof SymfonyTranslatorInterface) {
68 2
            $this->translator = new SymfonyTranslatorProxy($translator);
69
        } else {
70 1
            throw new \InvalidArgumentException("Translator must implement ValidatorTranslatorInterface");
71
        }
72 47
    }
73
74
    /**
75
     * @param array $newFieldsRuleProperties
76
     */
77 10
    public function addFieldsRuleProperties(array $newFieldsRuleProperties): void
78
    {
79 10
        foreach ($newFieldsRuleProperties as $fieldName => $newRuleProperties) {
80 10
            if (isset($this->fieldsRuleProperties[$fieldName])) {
81 1
                $this->fieldsRuleProperties[$fieldName] = array_merge(
82 1
                    $this->fieldsRuleProperties[$fieldName],
83 1
                    $newRuleProperties
84
                );
85
            } else {
86 10
                $this->fieldsRuleProperties[$fieldName] = $newRuleProperties;
87
            }
88
        }
89 10
    }
90
91
    /**
92
     * @param string $fieldName
93
     */
94 1
    public function removeFieldRuleProperties(string $fieldName): void
95
    {
96 1
        unset($this->fieldsRuleProperties[$fieldName]);
97 1
    }
98
99
    /**
100
     * @param string $fieldName
101
     * @return array
102
     */
103 2
    public function getFieldRuleProperties(string $fieldName): array
104
    {
105 2
        if (isset($this->fieldsRuleProperties[$fieldName])) {
106 2
            return $this->fieldsRuleProperties[$fieldName];
107
        }
108
109 1
        return [];
110
    }
111
112
    /**
113
     * Validate and return error messages (if any).
114
     *
115
     * @param array|null $data The data (e.g. from a form post) to be validated and set
116
     * @return array An array with all (if any) of error messages
117
     */
118 10
    public function validate(?array $data): array
119
    {
120 10
        $errorMsgs = [];
121 10
        foreach ($this->fieldsRuleProperties as $fieldName => $ruleProperties) {
122
            // Get value to validate
123 10
            if (isset($data[$fieldName])) {
124 9
                $value = $data[$fieldName];
125
126
                // Trim all values unless explicitly set to not
127 9
                if (is_string($value) && (!isset($ruleProperties['trim']) || $ruleProperties['trim'] === true)) {
128 9
                    $value = trim($value);
129
                }
130
                // allow a nullable value to be added as validated data
131 10
            } elseif (!empty($ruleProperties['nullable']) && array_key_exists($fieldName, $data)) {
132 1
                $value = null;
133
                // Don't validate empty values that are not set and not required
134 10
            } elseif (empty($ruleProperties['required']) && empty($ruleProperties['nonEmpty'])) {
135 10
                continue;
136
            } else {
137
                // Empty string as default (not null because that adds complexity to checks)
138 1
                $value = '';
139
            }
140
141
            // Validate value against each of the fields' rules
142 10
            $errorMsg = $this->validateValue($value, $ruleProperties);
143
144
            // If field has any error messages, add to error array, otherwise add value to validated data
145 10
            if ($errorMsg) {
146 1
                $errorMsgs[$fieldName] = $errorMsg;
147
            } else {
148 10
                $this->validatedData[$fieldName] = $value;
149
            }
150
        }
151
152 10
        return $errorMsgs;
153
    }
154
155
    /**
156
     * @param mixed $value
157
     * @param array $ruleProperties
158
     * @return string|null
159
     */
160 40
    public function validateValue($value, array $ruleProperties): ?string
161
    {
162
        // Field name
163 40
        if (isset($ruleProperties['textKey'])) {
164 1
            $fieldName = $this->translator->trans($ruleProperties['textKey']);
165 1
            unset($ruleProperties['textKey']);
166
        } else {
167 39
            $fieldName = $this->translator->trans('errorTheNoneSpecifiedField');
168
        }
169
170
        // Unset rule properties that are not rules
171 40
        $rules = $ruleProperties;
172 40
        unset($rules['textKey']);
173 40
        unset($rules['regexpExpl']);
174 40
        unset($rules['trim']);
175
176 40
        $isValueSet = (is_scalar($value) || is_null($value)) && ((string) $value) !== '';
177
178 40
        foreach ($rules as $rule => $ruleContents) {
179 40
            $errorMsg = null;
180
181
            switch ($rule) {
182 40
                case 'required':
183 9
                    if (!is_bool($ruleContents)) {
184 1
                        throw new \InvalidArgumentException("Invalid required validation rule[{$rule}]");
185
                    }
186
187 8
                    if ($ruleContents && !$isValueSet) {
188 4
                        $errorMsg = $this->translator->trans('errorFieldXIsRequired', ['%field%' => $fieldName]);
189
                    }
190
191 8
                    break;
192 35
                case 'nonEmpty':
193 4
                    if (!is_bool($ruleContents)) {
194 1
                        throw new \InvalidArgumentException("Invalid nonEmpty validation rule[{$rule}]");
195
                    }
196
197 3
                    if ($ruleContents && empty($value)) {
198 2
                        $errorMsg = $this->translator->trans('errorFieldXIsRequired', ['%field%' => $fieldName]);
199
                    }
200
201 3
                    break;
202 31
                case 'min':
203 3
                    if (!is_numeric($ruleContents)) {
204 1
                        throw new \InvalidArgumentException("Invalid min validation rule[{$ruleContents}]");
205
                    }
206
207 2
                    if ($isValueSet && (!is_numeric($value) || $value < $ruleContents)) {
208 1
                        $errorMsg = $this->translator->trans(
209 1
                            'errorFieldMustBeMinNumber',
210 1
                            ['%field%' => $fieldName, '%number%' => $ruleContents]
211
                        );
212
                    }
213
214 2
                    break;
215 30
                case 'max':
216 3
                    if (!is_numeric($ruleContents)) {
217 1
                        throw new \InvalidArgumentException("Invalid max validation rule[{$ruleContents}]");
218
                    }
219
220 2
                    if ($isValueSet && (!is_numeric($value) || $value > $ruleContents)) {
221 1
                        $errorMsg = $this->translator->trans(
222 1
                            'errorFieldMustBeMaxNumber',
223
                            [
224 1
                                '%field%'  => $fieldName,
225 1
                                '%number%' => $ruleContents
226
                            ]
227
                        );
228
                    }
229
230 2
                    break;
231 29
                case 'minLength':
232 6
                    if (!is_int($ruleContents) || $ruleContents < 1) {
233 1
                        throw new \InvalidArgumentException("Invalid min length validation rule[{$ruleContents}]");
234
                    }
235
236 5
                    if ($isValueSet && is_string($value) && mb_strlen($value) < $ruleContents) {
237 1
                        $errorMsg = $this->translator->trans(
238 1
                            'errorFieldMustBeMinXLength',
239 1
                            ['%field%' => $fieldName, '%numberOf%' => $ruleContents]
240
                        );
241
                    }
242
243 5
                    break;
244 25
                case 'maxLength':
245 4
                    if (!is_int($ruleContents) || $ruleContents < 1) {
246 1
                        throw new \InvalidArgumentException("Invalid max length validation rule[{$ruleContents}]");
247
                    }
248
249 3
                    if ($isValueSet && is_string($value) && mb_strlen($value) > $ruleContents) {
250 1
                        $errorMsg = $this->translator->trans(
251 1
                            'errorFieldMustBeMaxXLength',
252 1
                            ['%field%' => $fieldName, '%numberOf%' => $ruleContents]
253
                        );
254
                    }
255
256 3
                    break;
257 23
                case 'length':
258 4
                    if (!is_int($ruleContents) || $ruleContents < 1) {
259 1
                        throw new \InvalidArgumentException("Invalid length validation rule[{$ruleContents}]");
260
                    }
261
262 3
                    if ($isValueSet && is_string($value) && mb_strlen($value) !== $ruleContents) {
263 1
                        $errorMsg = $this->translator->trans(
264 1
                            'errorFieldMustBeXLength',
265 1
                            ['%field%' => $fieldName, '%numberOf%' => $ruleContents]
266
                        );
267
                    }
268
269 3
                    break;
270 20
                case 'regexp':
271 3
                    if (!$ruleContents) {
272 1
                        throw new \InvalidArgumentException("Invalid regexp validation rule[{$ruleContents}]");
273
                    }
274
275 2
                    if ($isValueSet && !preg_match('/' . $ruleContents . '/', (string) $value)) {
276 1
                        $errorMsg = $this->translator->trans(
277 1
                            'errorFieldInvalidFormat',
278 1
                            ['%field%' => $fieldName]
279
                        );
280
281 1
                        if (isset($ruleProperties['regexpExpl'])) {
282 1
                            $errorMsg .= $this->translator->trans(
283 1
                                'errorFieldValidCharactersAreX',
284 1
                                ['%characters%' => $ruleProperties['regexpExpl']]
285
                            );
286
                        }
287
                    }
288
289 2
                    break;
290 19
                case 'email':
291 4
                    if (!is_bool($ruleContents)) {
292 1
                        throw new \InvalidArgumentException("Invalid email validation rule[{$rule}]");
293
                    }
294
295 3
                    if ($ruleContents && $isValueSet && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
296 1
                        $errorMsg = $this->translator->trans('errorInvalidEmail');
297
                    }
298
299 3
                    break;
300 17
                case 'date':
301
                    // No break
302 15
                case 'dateTime':
303 6
                    if (!is_bool($ruleContents)) {
304 1
                        throw new \InvalidArgumentException("Invalid date validation rule[{$rule}]");
305
                    }
306
307 5
                    if ($ruleContents && $isValueSet) {
308
                        $isValueOk = function ($format) use ($value) {
309 4
                            return (\DateTime::createFromFormat($format, $value) !== false
310 4
                                && !\DateTime::getLastErrors()["warning_count"]
311 4
                                && !\DateTime::getLastErrors()["error_count"]);
312 4
                        };
313
314
                        // Allow datetime with and without seconds
315 4
                        if (($rule === 'date' && !$isValueOk('Y-m-d'))
316 3
                            || ($rule === 'dateTime' && !$isValueOk('Y-m-d H:i')
317 4
                                && !$isValueOk('Y-m-d H:i:s'))
318
                        ) {
319 2
                            $errorMsg = $this->translator->trans(
320 2
                                ($rule === 'date') ? 'errorInvalidDate' : 'errorInvalidDateTime'
321
                            );
322
                        }
323
                    }
324
325 5
                    break;
326 11
                case 'custom':
327 2
                    if (!is_callable($ruleContents)) {
328 1
                        throw new \InvalidArgumentException("Invalid custom validation rule[{$rule}]");
329
                    }
330
331 1
                    $errorMsg = $ruleContents($value);
332
333 1
                    break;
334 9
                case 'nullable':
335 8
                    break;
336
                default:
337 1
                    throw new \InvalidArgumentException("Unknown validation rule[{$rule}]");
338
            }
339
340 28
            if ($errorMsg) {
341 28
                return $errorMsg;
342
            }
343
        }
344
345 21
        return null;
346
    }
347
348
    /**
349
     * @param string $key
350
     * @param mixed  $default
351
     * @return array|mixed
352
     */
353 5
    public function getValidatedData(string $key = null, $default = null)
354
    {
355 5
        if ($key !== null) {
356 1
            return isset($this->validatedData[$key]) ? $this->validatedData[$key] : $default;
357
        } else {
358 5
            return $this->validatedData;
359
        }
360
    }
361
362
    /**
363
     * @return array
364
     */
365 1
    public static function getValidRuleProperties(): array
366
    {
367 1
        return static::$validRuleProperties;
368
    }
369
}
370