Completed
Push — master ( c6fd8d...59482e )
by Mehmet
03:03
created

ModelUtils::validateDoc()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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