Completed
Push — master ( aeb75f...c6fd8d )
by Mehmet
02:54
created

ModelUtils::setDefaultModelAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
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
            // Does doc has a array that does not exist in model definition.
44
            if (!isset($my_model[$key])) {
45
                if ($my_key !== null) {
46
                    $v_key = strval($my_key).".".strval($key);
47
                }
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
                if ($my_key !== null) {
52
                    $v_key = strval($my_key).".".strval($key);
53
                }
54
                // Validate this array too.
55
                $my_doc[$key] = self::validateDoc($my_model[$key], $my_doc[$key], $v_key);
56
                if (self::getType($my_doc[$key]) != "array") {
57
                    return $my_doc[$key];
58
                }
59
            } // Is the value of the array[key] have same variable type
60
              //that stated in the definition of the model array.
61
            elseif ($my_doc_key_type != $my_model[$key]['_type']) {
62
                if ($my_key !== null) {
63
                    $v_key = $my_key." . ".$key;
64
                }
65
                throw new \Exception("Error for key '".$v_key."'".", ".$my_doc_key_type.
66
                    " given but it must be ".$my_model[$key]['_type']);
67
            } else {
68
                if ($my_key !== null) {
69
                    $v_key = $my_key." . ".$key;
70
                }
71
                $my_doc[$key] = self::validateDocItem($my_doc[$key], $my_model[$key], $v_key);
72
            }
73
        }
74
        return $my_doc;
75
    }
76
    
77
    /**
78
     * @param mixed     $value
79
     * @param array     $my_model
80
     * @param string    $key
81
     *
82
     * @return mixed
83
     * @throws \Exception
84
     */
85
    private static function validateDocItem($value, $my_model, $key)
86
    {
87
        $my_model = self::setDefaultModelAttributes($my_model);
88
        if (self::getType($value) != $my_model['_type']) {
89
            return false;
90
        }
91
        if ($my_model['_input_type'] !== null) {
92
            self::filterValidate($my_model['_input_type'], $key, $value, $my_model['_input_format']);
93
        }
94
        self::checkMinMaxInOptions($my_model['_type'], $key, $value, $my_model['_min_length'], $my_model['_max_length'], $my_model['_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
    private static function setDefaultModelAttributes($my_model){
295
        
296
        return array_merge(static::$field_attributes, $my_model);
297
    }
298
    
299
    /**
300
     * @param mixed     $value
301
     * @param array     $my_model
302
     *
303
     * @return mixed
304
     */
305
    private static function sanitizeDocItem($value, $my_model)
306
    {
307
        $my_model = self::setDefaultModelAttributes($my_model);
308
        $value = filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
309
        if (($my_model['_input_type'] == 'timestamp') && ($value == 'now')) {
310
            $value = time();
311
        }
312
        settype($value, $my_model['_type']);
313
        $value = self::setMaxMinInOptions($my_model['_type'], $value, $my_model['_min_length'], $my_model['_max_length'], $my_model['_in_options']);
314
        return $value;
315
    }
316
    
317
    private static function setMaxMinInOptions($type, $value, $min_length, $max_length, $in_options)
318
    {
319
        switch ($type) {
320
            case 'integer':
321
            case 'float':
322
                if ($min_length !== null && ($value<$min_length)) {
323
                    $value = $min_length;
324
                }
325
                if ($max_length !== null && ($value>$max_length)) {
326
                    $value = $max_length;
327
                }
328
                break;
329
            case 'string':
330
                if ($max_length !== null && strlen($value)>$max_length) {
331
                    $value = substr($value, 0, $max_length);
332
                }
333
                break;
334
335
        }
336
        if ($in_options !== null && (!in_array($value, $in_options))) {
337
            $value = $in_options[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