Completed
Push — master ( 0c7d27...704002 )
by Andreas
03:34 queued 11s
created

Validator   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 347
Duplicated Lines 19.88 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

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