Completed
Push — master ( e2fbb8...975e5f )
by Andreas
02:12
created

Validator::__construct()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
ccs 11
cts 11
cp 1
rs 9.2
cc 4
eloc 10
nc 4
nop 2
crap 4
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 28
        } elseif ($translator instanceof ValidatorTranslatorInterface) {
72 2
            $this->translator = $translator;
73 28
        } elseif ($translator instanceof SymfonyTranslatorInterface) {
74 27
            $this->translator = new SymfonyTranslatorProxy($translator);
75 27
        } 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 1
                );
91 1
            } else {
92 3
                $this->fieldsRuleProperties[$fieldName] = $newRuleProperties;
93
            }
94 3
        }
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 3
                }
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 1
            } else {
151 2
                $this->validatedData[$fieldName] = $value;
152
            }
153 3
        }
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 1
        } 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 3
                    }
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 1
                    }
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 1
                        );
214 1
                    }
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
                                '%number%' => $ruleContents
228 1
                            ]
229 1
                        );
230 1
                    }
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 1
                        );
243 1
                    }
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 1
                        );
256 1
                    }
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 1
                        );
269 1
                    }
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 1
                        );
282
283 1
                        if (isset($ruleProperties['regexpExpl'])) {
284 1
                            $errorMsg .= $this->translator->trans(
285 1
                                'errorFieldValidCharactersAreX',
286 1
                                ['%characters%' => $ruleProperties['regexpExpl']]
287 1
                            );
288 1
                        }
289 1
                    }
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 1
                    }
300
301 1
                    break;
302 8
                case 'date':
303
                    // No break
304 8
                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 3
                                && !$isValueOk('Y-m-d H:i:s'))
320 4
                        ) {
321 2
                            $errorMsg = $this->translator->trans(
322 2
                                ($rule === 'date') ? 'errorInvalidDate' : 'errorInvalidDateTime'
323 2
                            );
324 2
                        }
325 4
                    }
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 1
                default:
337 1
                    throw new \InvalidArgumentException("Unknown validation rule[{$rule}]");
338 1
            }
339
340 11
            if ($errorMsg) {
341 7
                return $errorMsg;
342
            }
343 6
        }
344
345 6
        return null;
346
    }
347
348
    /**
349
     * @param string $key
350
     * @param mixed  $default
351
     * @return array
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