Completed
Push — master ( 8cf557...bdfdd6 )
by Mehmet
03:12
created

ModelUtils::sanitizeDocItem()   D

Complexity

Conditions 9
Paths 128

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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