ModelUtils   C
last analyzed

Complexity

Total Complexity 79

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 35
Bugs 8 Features 4
Metric Value
wmc 79
c 35
b 8
f 4
lcom 1
cbo 1
dl 0
loc 351
rs 5.442

10 Methods

Rating   Name   Duplication   Size   Complexity  
C setMaxMinInOptions() 0 24 12
C filterValidate() 0 59 11
C validateDoc() 0 31 7
A validateDocItem() 0 12 3
C checkMinMaxInOptions() 0 30 14
C fitDocToModel() 0 24 8
C setModelDefaults() 0 65 17
A setDefaultModelAttributes() 0 8 2
A sanitizeDocItem() 0 13 4
A getType() 0 15 1

How to fix   Complexity   

Complex Class

Complex classes like ModelUtils 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 ModelUtils, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ModelUtils: A simple PHP class for validating variable types, fixing, sanitising and setting default values for a
4
 * model definition encoded as an array .
5
 * *
6
 *  @TODO: A doc item can be array that has multiple values,
7
 *  implement validation and sanitization for the situations like this.
8
 *  @TODO: Detailed documentation is needed
9
 */
10
11
namespace ModelUtils;
12
13
use Crisu83\ShortId\ShortId;
14
15
class ModelUtils
16
{
17
    static protected $fieldAttributes = [
18
        '_type' => null,
19
        '_input_type' => null,
20
        '_min_length' => null,
21
        '_max_length' => null,
22
        '_in_options' => null,
23
        '_input_format' => null,
24
        '_required' => null,
25
        '_index' => null,
26
        '_ref' => null,
27
        '_has_many' => null,
28
        '_index_type' => null
29
    ];
30
    
31
    /**
32
     * Validate given documents
33
     *
34
     * @param array     $myModel
35
     * @param array     $myDoc
36
     * @param string    $myKey
37
     * @return array
38
     * @throws \Exception
39
     */
40
    public static function validateDoc($myModel, $myDoc, $myKey = null)
41
    {
42
        $myKeys = array_keys($myDoc);
43
        foreach ($myKeys as $key) {
44
            
45
            $myDoc_key_type = self::getType($myDoc[$key]);
46
            $vKey = $key;
47
            if ($myKey !== null) {
48
                $vKey = strval($myKey).".".strval($key);
49
            }
50
            // Does doc has a array that does not exist in model definition.
51
            if (!isset($myModel[$key])) {
52
                throw new \Exception("Error for key '".$vKey."' that does not exist in the model");
53
            } // Is the value of the array[key] again another array?
54
            elseif ($myDoc_key_type == "array") {
55
                // Validate this array too.
56
                $myDoc[$key] = self::validateDoc($myModel[$key], $myDoc[$key], $vKey);
57
                if (self::getType($myDoc[$key]) != "array") {
58
                    return $myDoc[$key];
59
                }
60
            } // Is the value of the array[key] have same variable type
61
                //that stated in the definition of the model array.
62
            elseif ($myDoc_key_type != $myModel[$key]['_type']) {
63
                throw new \Exception("Error for key '".$vKey."'".", ".$myDoc_key_type.
64
                    " given but it must be ".$myModel[$key]['_type']);
65
            } else {
66
                $myDoc[$key] = self::validateDocItem($myDoc[$key], $myModel[$key], $vKey);
67
            }
68
        }
69
        return $myDoc;
70
    }
71
    
72
    /**
73
     * @param mixed     $value
74
     * @param array     $myModel
75
     * @param string    $key
76
     *
77
     * @return mixed
78
     * @throws \Exception
79
     */
80
    private static function validateDocItem($value, $myModel, $key)
81
    {
82
        $myModel = self::setDefaultModelAttributes($myModel);
83
        if (self::getType($value) != $myModel['_type']) {
84
            return false;
85
        }
86
        if ($myModel['_input_type'] !== null) {
87
            self::filterValidate($myModel['_input_type'], $key, $value, $myModel['_input_format']);
88
        }
89
        self::checkMinMaxInOptions($myModel['_type'], $key, $value, $myModel['_min_length'], $myModel['_max_length'], $myModel['_in_options']);
90
        return $value;
91
    }
92
    
93
    private static function checkMinMaxInOptions($type, $key, $value, $minLength, $maxLength, $inOptions)
94
    {
95
        $error = '';
96
        switch ($type) {
97
            case 'integer':
98
            case 'float':
99
                if ($minLength !== null && ($value<$minLength)) {
100
                    $error = "validation: Must be bigger than ".$minLength;
101
                }
102
                if ($maxLength !== null && ($value>$maxLength)) {
103
                    $error = "validation: Must be smallerr than ".$maxLength;
104
                }
105
                break;
106
            default:
107
                if ($maxLength !== null && (strlen($value)>$maxLength)) {
108
                    $error = "validation: It's length must be smaller than ".$maxLength;
109
                }
110
                if ($minLength !== null && (strlen($value)<$minLength)) {
111
                    $error = "validation: It's length must be longer than ".$minLength;
112
                }
113
                break;
114
        }
115
        if ($inOptions !== null && (!in_array($value, $inOptions))) {
116
            $error = "It's value must be one of the these values: ".implode(", ", $inOptions);
117
        }
118
        if ($error != '') {
119
            throw new \Exception("Error for value '".$value."' for '".$key."' couldn't pass the ".
120
                "validation: ".$error);
121
        }
122
    }
123
    
124
    private static function filterValidate($inputType, $key, $value, $format)
125
    {
126
        $filter_check = null;
127
        $validation = null;
128
        switch ($inputType) {
129
            case 'mail':
130
                $filter_check = filter_var($value, FILTER_VALIDATE_EMAIL);
131
                $validation = 'INVALID_EMAIL_ADDRESS';
132
    
133
                break;
134
            case 'bool':
135
                $filter_check = filter_var($value, FILTER_VALIDATE_BOOLEAN);
136
                $validation = 'INVALID_BOOLEAN_VALUE';
137
                break;
138
            case 'url':
139
                $filter_check = filter_var($value, FILTER_VALIDATE_URL);
140
                $validation = 'INVALID_URL';
141
                break;
142
            case 'ip':
143
                $filter_check = filter_var($value, FILTER_VALIDATE_IP);
144
                $validation = 'INVALID_IP_ADDRESS';
145
                break;
146
            case 'mac_address':
147
                $filter_check = filter_var($value, FILTER_VALIDATE_MAC);
148
                $validation = 'INVALID_MAC_ADDRESS';
149
                break;
150
            case 'date':
151
                $regex = "/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/";
152
                $options = array("options"=>array("regexp"=> $regex));
153
                $filter_check = filter_var($value, FILTER_VALIDATE_REGEXP, $options);
154
                $validation = 'INVALID_DATE_FORMAT';
155
                break;
156
            case 'time':
157
                $regex = "/^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])$/";
158
                $options = array("options"=>array("regexp"=> $regex));
159
                $filter_check = filter_var($value, FILTER_VALIDATE_REGEXP, $options);
160
                $validation = 'INVALID_TIME_FORMAT';
161
                break;
162
            case 'datetime':
163
                $date_part = "[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";
164
                $time_part = "([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])";
165
                $regex = "/^".$date_part." ".$time_part."$/";
166
                $options = array("options"=>array("regexp"=> $regex));
167
                $filter_check = filter_var($value, FILTER_VALIDATE_REGEXP, $options);
168
                $validation = 'INVALID_DATETIME_FORMAT';
169
                break;
170
            case 'regex':
171
                $regex = "/^".$format."$/";
172
                $options = array("options"=>array("regexp"=> $regex));
173
                $filter_check = filter_var($value, FILTER_VALIDATE_REGEXP, $options);
174
                $validation = 'INVALID_FORMAT';
175
                break;
176
        }
177
        if ($filter_check === false) {
178
            throw new \Exception("Error for value '".$value."' for '".$key."' couldn't pass the ".
179
                "validation: ".$validation);
180
        }
181
        return $filter_check;
182
    }
183
    
184
    /**
185
     * Fit document to given Model
186
     *
187
     * @param array     $myModel
188
     * @param array     $myDoc
189
     * @return array
190
     */
191
    public static function fitDocToModel($myModel, $myDoc)
192
    {
193
        $myKeys = array_keys($myDoc);
194
        foreach ($myKeys as $key) {
195
            // If array has a key that is not presented in the model definition, unset it .
196
            if (!isset($myModel[$key])) {
197
                unset($myDoc[$key]);
198
            } // If array[$key] is again an array, recursively fit this array too .
199
            elseif (self::getType($myDoc[$key]) == "array" && !isset($myModel[$key]['_type'])) {
200
                $myDoc[$key] = self::fitDocToModel($myModel[$key], $myDoc[$key]);
201
                // If returned value is not an array, return it .
202
                if (self::getType($myDoc[$key]) != "array") {
203
                    return $myDoc[$key];
204
                }
205
            } elseif (self::getType($myDoc[$key]) == "array" && $myModel[$key]['_type'] != "array") {
206
                $myDoc[$key] = $myModel[$key]['_default'];
207
            } // If array[key] is not an array and not has same variable type that stated in the model definition .
208
            else {
209
                $myDoc[$key] = self::sanitizeDocItem($myDoc[$key], $myModel[$key]);
210
            }
211
        }
212
213
        return $myDoc;
214
    }
215
216
    /**
217
     * @param array     $myModel
218
     * @param array     $myDoc
219
     *
220
     * @return array
221
     */
222
    public static function setModelDefaults($myModel, $myDoc)
223
    {
224
        $myKeys = array_keys($myModel);
225
        $newDoc = [];
226
        foreach ($myKeys as $key) {
227
            $item_keys = array_keys($myModel[$key]);
228
            // If one of the keys of $myModel[$key] is _type this is a definition, not a defined key
229
            if (in_array("_type", $item_keys)) {
230
                // If array does not have this key, set the default value .
231
                if (!isset($myDoc[$key])) {
232
                    if (isset($myModel[$key]['_input_type'])) {
233
                        switch ($myModel[$key]['_input_type']) {
234
                            case 'uid':
235
                                    $shortid = ShortId::create();
236
                                    $newDoc[$key] = $shortid->generate();
237
                                break;
238
                            case 'date':
239
                                if ($myModel[$key]['_default'] == 'today') {
240
                                    $newDoc[$key] = date("Y-m-d");
241
                                } else {
242
                                    $newDoc[$key] = $myModel[$key]['_default'];
243
                                }
244
                                break;
245
                            case 'timestamp':
246
                                $model_default = $myModel[$key]['_default'];
247
                                $model_type = $myModel[$key]['_type'];
248
                                if (($model_default == "now") && ($model_type == "integer")) {
249
                                    $newDoc[$key] = time();
250
                                } elseif ($model_default == "now" && ($model_type == "string")) {
251
                                    $newDoc[$key] = date("Y-m-d H:i:s");
252
                                } else {
253
                                    $newDoc[$key] = $model_default;
254
                                }
255
                                break;
256
                    
257
                            default:
258
                                $newDoc[$key] = $myModel[$key]['_default'];
259
                        }
260
                    } else {
261
                        $newDoc[$key] = $myModel[$key]['_default'];
262
                    }
263
                } // If array has this key
264
                else {
265
                    // If model definition stated this key's default value is not Null
266
                    // and has a wrong variable type, fix it.
267
                    if ($myModel[$key]['_default'] !== null) {
268
                        $key_type = self::getType($myDoc[$key]);
269
                        if ($key_type != $myModel[$key]['_type'] && $key_type == "array") {
270
                            $myDoc[$key] = $myModel[$key]['_default'];
271
                        }
272
                        settype($myDoc[$key], $myModel[$key]['_type']);
273
                    }
274
                    $newDoc[$key] = $myDoc[$key];
275
                }
276
                $newDoc[$key] = self::sanitizeDocItem($newDoc[$key], $myModel[$key]);
277
            } // If one of the keys is not _type, this is a defined key, recursively get sub keys .
278
            else {
279
                if (!isset($myDoc[$key])) {
280
                    $myDoc[$key] = "";
281
                }
282
                $newDoc[$key] = self::setModelDefaults($myModel[$key], $myDoc[$key]);
283
            }
284
        }
285
        return $newDoc;
286
    }
287
288
    private static function setDefaultModelAttributes($myModel)
289
    {
290
        $source =static::$fieldAttributes;
291
        foreach ($myModel as $key => $value) {
292
            $source[$key]=$value;
293
        }
294
        return $source;
295
    }
296
    
297
    /**
298
     * @param mixed     $value
299
     * @param array     $myModel
300
     *
301
     * @return mixed
302
     */
303
    private static function sanitizeDocItem($value, $myModel)
304
    {
305
        $myModel = self::setDefaultModelAttributes($myModel);
306
        if($myModel['_input_type']!='html'){
307
            $value = filter_var($value, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
308
        }
309
        if (($myModel['_input_type'] == 'timestamp') && ($value == 'now')) {
310
            $value = time();
311
        }
312
        settype($value, $myModel['_type']);
313
        $value = self::setMaxMinInOptions($myModel['_type'], $value, $myModel['_min_length'], $myModel['_max_length'], $myModel['_in_options']);
314
        return $value;
315
    }
316
    
317
    private static function setMaxMinInOptions($type, $value, $minLength, $maxLength, $inOptions)
318
    {
319
        switch ($type) {
320
            case 'integer':
321
            case 'float':
322
                if ($minLength !== null && ($value<$minLength)) {
323
                    $value = $minLength;
324
                }
325
                if ($maxLength !== null && ($value>$maxLength)) {
326
                    $value = $maxLength;
327
                }
328
                break;
329
            case 'string':
330
                if ($maxLength !== null && strlen($value)>$maxLength) {
331
                    $value = substr($value, 0, $maxLength);
332
                }
333
                break;
334
335
        }
336
        if ($inOptions !== null && (!in_array($value, $inOptions))) {
337
            $value = $inOptions[0]; // First value of the in_options array is assumed to be the default value .
338
        }
339
        return $value;
340
    }
341
    /**
342
     * A Note:
343
     * Since the built-in php function gettype returns "double" variabe type, here is the workaround function
344
     * See http://php . net/manual/en/function . gettype . php => Possible values for the returned string are:
345
     * "double" (for historical reasons "double" is returned in case of a float, and not simply "float")
346
     *
347
     * @param mixed     $value
348
     * @return string
349
     */
350
    private static function getType($value)
351
    {
352
        return [
353
            'boolean' => 'boolean',
354
            'string' => 'string',
355
            'integer' => 'integer',
356
            'long' => 'integer',
357
            'double' => 'float',
358
            'float' => 'float',
359
            'array' => 'array',
360
            'object' => 'object',
361
            'resource' => 'resource',
362
            'null' => 'null'
363
        ][strtolower(gettype($value))];
364
    }
365
}
366