Completed
Push — master ( c66b28...4ad149 )
by Andreas
02:09
created

Validator   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 354
Duplicated Lines 19.49 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 98.68%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 80
c 1
b 0
f 0
lcom 1
cbo 3
dl 69
loc 354
ccs 150
cts 152
cp 0.9868
rs 4.8717

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 4
A addFieldsRuleProperties() 0 13 3
A removeFieldRuleProperties() 0 4 1
A getFieldRuleProperties() 0 8 2
D validate() 0 33 9
D validateValue() 69 184 57
A getValidatedData() 0 8 3
A getValidRuleProperties() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Validator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Validator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Utils.
4
 *
5
 * @copyright Copyright (c) 2016 Starweb / Ehandelslogik i Lund AB
6
 * @license   BSD 3-Clause
7
 */
8
9
namespace Starlit\Utils\Validation;
10
11
use Symfony\Component\Translation\TranslatorInterface as SymfonyTranslatorInterface;
12
13
/**
14
 * A crude 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
        'custom'
35
    ];
36
37
    /**
38
     * @var ValidatorTranslatorInterface
39
     */
40
    protected $translator;
41
42
    /**
43
     * Fields to be validated and their rules.
44
     *
45
     * @var array
46
     */
47
    protected $fieldRules;
48
49
    /**
50
     * @var array
51
     */
52
    protected $validatedData = [];
53
54
    /**
55
     * @var array
56
     */
57
    protected $fieldsRuleProperties;
58
59
    /**
60
     * Constructor.
61
     *
62
     * @param array                                                        $fieldsRuleProperties
63
     * @param ValidatorTranslatorInterface|SymfonyTranslatorInterface|null $translator
64
     */
65 28
    public function __construct(array $fieldsRuleProperties = [], $translator = null)
66
    {
67 28
        $this->fieldsRuleProperties = $fieldsRuleProperties;
68
69 28
        if (!$translator) {
70 1
            $this->translator = new DefaultValidatorTranslator();
71
        } elseif ($translator instanceof ValidatorTranslatorInterface) {
72 2
            $this->translator = $translator;
73
        } elseif ($translator instanceof SymfonyTranslatorInterface) {
74 27
            $this->translator = new SymfonyTranslatorProxy($translator);
75
        } else {
76 1
            throw new \InvalidArgumentException("Translator must implement ValidatorTranslatorInterface");
77
        }
78 28
    }
79
80
    /**
81
     * @param array $newFieldsRuleProperties
82
     */
83 3
    public function addFieldsRuleProperties(array $newFieldsRuleProperties)
84
    {
85 3
        foreach ($newFieldsRuleProperties as $fieldName => $newRuleProperties) {
86 3
            if (isset($this->fieldsRuleProperties[$fieldName])) {
87 1
                $this->fieldsRuleProperties[$fieldName] = array_merge(
88 1
                    $this->fieldsRuleProperties[$fieldName],
89
                    $newRuleProperties
90
                );
91
            } else {
92 3
                $this->fieldsRuleProperties[$fieldName] = $newRuleProperties;
93
            }
94
        }
95 3
    }
96
97
    /**
98
     * @param string $fieldName
99
     */
100 1
    public function removeFieldRuleProperties($fieldName)
101
    {
102 1
        unset($this->fieldsRuleProperties[$fieldName]);
103 1
    }
104
105
    /**
106
     * @param string $fieldName
107
     * @return array
108
     */
109 2
    public function getFieldRuleProperties($fieldName)
110
    {
111 2
        if (isset($this->fieldsRuleProperties[$fieldName])) {
112 2
            return $this->fieldsRuleProperties[$fieldName];
113
        }
114
115 1
        return [];
116
    }
117
118
    /**
119
     * Validate and return error messages (if any).
120
     *
121
     * @param array|null $data The data (e.g. from a form post) to be validated and set
122
     * @return array An array with all (if any) of error messages
123
     */
124 3
    public function validate($data)
125
    {
126 3
        $errorMsgs = [];
127 3
        foreach ($this->fieldsRuleProperties as $fieldName => $ruleProperties) {
128
            // Get value to validate
129 3
            if (isset($data[$fieldName])) {
130 3
                $value = $data[$fieldName];
131
132
                // Trim all values unless explicitly set to not
133 3
                if (is_string($value) && (!isset($ruleProperties['trim']) || $ruleProperties['trim'] === true)) {
134 3
                    $value = trim($value);
135
                }
136
            // Don't validate empty values that are not set and not required
137 3
            } elseif (empty($ruleProperties['required']) && empty($ruleProperties['nonEmpty'])) {
138 3
                continue;
139
            } else {
140
                // Empty string as default (not null because that adds complexity to checks)
141 1
                $value = '';
142
            }
143
144
            // Validate value against each of the fields' rules
145 3
            $errorMsg = $this->validateValue($value, $ruleProperties);
146
147
            // If field has any error messages, add to error array, otherwise add value to validated data
148 3
            if ($errorMsg) {
149 1
                $errorMsgs[$fieldName] = $errorMsg;
150
            } else {
151 3
                $this->validatedData[$fieldName] = $value;
152
            }
153
        }
154
155 3
        return $errorMsgs;
156
    }
157
158
    /**
159
     * @param mixed $value
160
     * @param array $ruleProperties
161
     * @return string|null
162
     */
163 23
    public function validateValue($value, array $ruleProperties)
164
    {
165
        // Field name
166 23
        if (isset($ruleProperties['textKey'])) {
167 1
            $fieldName = $this->translator->trans($ruleProperties['textKey']);
168 1
            unset($ruleProperties['textKey']);
169
        } else {
170 22
            $fieldName = $this->translator->trans('errorTheNoneSpecifiedField');
171
        }
172
173
        // Unset rule properties that are not rules
174 23
        $rules = $ruleProperties;
175 23
        unset($rules['textKey']);
176 23
        unset($rules['regexpExpl']);
177 23
        unset($rules['trim']);
178
179 23
        foreach ($rules as $rule => $ruleContents) {
180 23
            $errorMsg = null;
181
182
            switch ($rule) {
183 23
                case 'required':
184 4
                    if (!is_bool($ruleContents)) {
185 1
                        throw new \InvalidArgumentException("Invalid required validation rule[{$rule}]");
186
                    }
187
188 3
                    if ($ruleContents && (((string) $value) === '')) {
189 3
                        $errorMsg = $this->translator->trans('errorFieldXIsRequired', ['%field%' => $fieldName]);
190
                    }
191
192 3
                    break;
193 20
                case 'nonEmpty':
194 2
                    if (!is_bool($ruleContents)) {
195 1
                        throw new \InvalidArgumentException("Invalid nonEmpty validation rule[{$rule}]");
196
                    }
197
198 1
                    if ($ruleContents && empty($value)) {
199 1
                        $errorMsg = $this->translator->trans('errorFieldXIsRequired', ['%field%' => $fieldName]);
200
                    }
201
202 1
                    break;
203 18 View Code Duplication
                case 'min':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204 2
                    if (!is_numeric($ruleContents)) {
205 1
                        throw new \InvalidArgumentException("Invalid min validation rule[{$ruleContents}]");
206
                    }
207
208 1
                    if ($value < $ruleContents) {
209
                        $fieldName =
210 1
                        $errorMsg = $this->translator->trans(
211 1
                            'errorFieldMustBeMinNumber',
212 1
                            ['%field%' => $fieldName, '%number%' => $ruleContents]
213
                        );
214
                    }
215
216 1
                    break;
217 17 View Code Duplication
                case 'max':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218 2
                    if (!is_numeric($ruleContents)) {
219 1
                        throw new \InvalidArgumentException("Invalid max validation rule[{$ruleContents}]");
220
                    }
221
222 1
                    if ($value > $ruleContents) {
223 1
                        $errorMsg = $this->translator->trans(
224 1
                            'errorFieldMustBeMaxNumber',
225
                            [
226 1
                                '%field%'  => $fieldName,
227 1
                                '%number%' => $ruleContents
228
                            ]
229
                        );
230
                    }
231
232 1
                    break;
233 16 View Code Duplication
                case 'minLength':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
234 4
                    if (!is_int($ruleContents) || $ruleContents < 1) {
235 1
                        throw new \InvalidArgumentException("Invalid min length validation rule[{$ruleContents}]");
236
                    }
237
238 3
                    if (mb_strlen($value) < $ruleContents) {
239 1
                        $errorMsg = $this->translator->trans(
240 1
                            'errorFieldMustBeMinXLength',
241 1
                            ['%field%' => $fieldName, '%numberOf%' => $ruleContents]
242
                        );
243
                    }
244
245 3
                    break;
246 13 View Code Duplication
                case 'maxLength':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247 2
                    if (!is_int($ruleContents) || $ruleContents < 1) {
248 1
                        throw new \InvalidArgumentException("Invalid max length validation rule[{$ruleContents}]");
249
                    }
250
251 1
                    if ((((string) $value) !== '') && mb_strlen($value) > $ruleContents) {
252 1
                        $errorMsg = $this->translator->trans(
253 1
                            'errorFieldMustBeMaxXLength',
254 1
                            ['%field%' => $fieldName, '%numberOf%' => $ruleContents]
255
                        );
256
                    }
257
258 1
                    break;
259 12 View Code Duplication
                case 'length':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
260 2
                    if (!is_int($ruleContents) || $ruleContents < 1) {
261 1
                        throw new \InvalidArgumentException("Invalid length validation rule[{$ruleContents}]");
262
                    }
263
264 1
                    if ((((string) $value) !== '') && mb_strlen($value) !== $ruleContents) {
265 1
                        $errorMsg = $this->translator->trans(
266 1
                            'errorFieldMustBeXLength',
267 1
                            ['%field%' => $fieldName, '%numberOf%' => $ruleContents]
268
                        );
269
                    }
270
271 1
                    break;
272 10
                case 'regexp':
273 2
                    if (!$ruleContents) {
274 1
                        throw new \InvalidArgumentException("Invalid regexp validation rule[{$ruleContents}]");
275
                    }
276
277 1
                    if ((((string) $value) !== '') && !preg_match('/' . $ruleContents . '/', $value)) {
278 1
                        $errorMsg = $this->translator->trans(
279 1
                            'errorFieldInvalidFormat',
280 1
                            ['%field%' => $fieldName]
281
                        );
282
283 1
                        if (isset($ruleProperties['regexpExpl'])) {
284 1
                            $errorMsg .= $this->translator->trans(
285 1
                                'errorFieldValidCharactersAreX',
286 1
                                ['%characters%' => $ruleProperties['regexpExpl']]
287
                            );
288
                        }
289
                    }
290
291 1
                    break;
292 9
                case 'email':
293 2
                    if (!is_bool($ruleContents)) {
294 1
                        throw new \InvalidArgumentException("Invalid email validation rule[{$rule}]");
295
                    }
296
297 1
                    if ($ruleContents && (((string) $value) !== '') && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
298 1
                        $errorMsg = $this->translator->trans('errorInvalidEmail');
299
                    }
300
301 1
                    break;
302 8
                case 'date':
303
                    // No break
304 6
                case 'dateTime':
305 5
                    if (!is_bool($ruleContents)) {
306 1
                        throw new \InvalidArgumentException("Invalid date validation rule[{$rule}]");
307
                    }
308
309 4
                    if ($ruleContents && ((string) $value) !== '') {
310 4
                        $isValueOk = function ($format) use ($value) {
311 4
                            return (\DateTime::createFromFormat($format, $value) !== false
312 4
                                && !\DateTime::getLastErrors()["warning_count"]
313 4
                                && !\DateTime::getLastErrors()["error_count"]);
314 4
                        };
315
316
                        // Allow datetime with and without seconds
317 4
                        if (($rule === 'date' && !$isValueOk('Y-m-d'))
318 3
                            || ($rule === 'dateTime' && !$isValueOk('Y-m-d H:i')
319 4
                                && !$isValueOk('Y-m-d H:i:s'))
320
                        ) {
321 2
                            $errorMsg = $this->translator->trans(
322 2
                                ($rule === 'date') ? 'errorInvalidDate' : 'errorInvalidDateTime'
323
                            );
324
                        }
325
                    }
326
327 4
                    break;
328 3
                case 'custom':
329 2
                    if (!is_callable($ruleContents)) {
330 1
                        throw new \InvalidArgumentException("Invalid custom validation rule[{$rule}]");
331
                    }
332
333 1
                    $errorMsg = $ruleContents($value);
334
335 1
                    break;
336
                default:
337 1
                    throw new \InvalidArgumentException("Unknown validation rule[{$rule}]");
338
            }
339
340 11
            if ($errorMsg) {
341 11
                return $errorMsg;
342
            }
343
        }
344
345 6
        return null;
346
    }
347
348
    /**
349
     * @param string $key
350
     * @param mixed  $default
351
     * @return array|mixed
352
     */
353 1
    public function getValidatedData($key = null, $default = null)
354
    {
355 1
        if ($key !== null) {
356 1
            return isset($this->validatedData[$key]) ? $this->validatedData[$key] : $default;
357
        } else {
358 1
            return $this->validatedData;
359
        }
360
    }
361
362
    /**
363
     * @return array
364
     */
365 1
    public static function getValidRuleProperties()
366
    {
367 1
        return static::$validRuleProperties;
368
    }
369
}
370