Completed
Push — dev-master ( 2b6108...c85948 )
by Vijay
05:50
created

Mapper   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 382
Duplicated Lines 3.66 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
dl 14
loc 382
rs 5.7097
c 0
b 0
f 0
wmc 67
lcom 1
cbo 8

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 2
A load() 0 10 4
C initValidation() 0 60 16
B setupHooks() 0 54 7
A __toString() 0 4 1
A __toArray() 0 4 1
D castFields() 7 29 9
D exportArray() 7 41 18
A exportJson() 0 4 2
B setUUID() 0 19 6
A audit() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
namespace FFCMS\Mappers;
4
5
use FFMVC\Helpers;
6
use FFCMS\{Traits, Models};
7
8
/**
9
 * Base Database Mapper Class extends f3's DB\SQL\Mapper
10
 *
11
 * @author Vijay Mahrra <[email protected]>
12
 * @copyright (c) Copyright 2016 Vijay Mahrra
13
 * @license GPLv3 (http://www.gnu.org/licenses/gpl-3.0.html)
14
 *
15
 * @see https://fatfreeframework.com/cursor
16
 * @see https://fatfreeframework.com/sql-mapper
17
 * @see https://github.com/Wixel/GUMP
18
 */
19
20
// abstract class Magic implements ArrayAccess
21
// abstract class Cursor extends \Magic implements \IteratorAggregate
22
// class Mapper extends \DB\Cursor
23
/**
24
 * @property string $key
25
 * @property string $value
26
 * @property string $created
27
 */
28
abstract class Mapper extends \DB\SQL\Mapper
29
{
30
    use Traits\Validation;
31
32
    /**
33
     * Fields and their visibility to clients, boolean or string of visible field name
34
     * all fields are visible except where 'false'
35
     *
36
     * @var array $fieldsVisible
37
     */
38
    protected $fieldsVisible = [];
39
40
    /**
41
     * Fields that are editable to clients, boolean or string of visible field name
42
     *
43
     * @var array $fieldsEditable
44
     */
45
    protected $fieldsEditable = [];
46
47
    /**
48
     * @var object database class
49
     */
50
    protected $db;
51
52
    /**
53
     * @var string $table for the mapper - this string gets automatically quoted
54
     */
55
    protected $table;
56
57
    /**
58
     * @var string $mapperName name for the mapper
59
     */
60
    protected $mapperName;
61
62
    /**
63
     * @var string $uuid the fieldname used for the uuid
64
     */
65
    protected $uuidField = 'uuid';
66
67
    /**
68
     * @var boolean $valid the data after validation is valid?
69
     */
70
    protected $valid = null;
71
72
    /**
73
     * @var array $originalData the original data when object created/loaded
74
     */
75
    protected $originalData = [];
76
77
    /**
78
     * @var array $auditData data to write to audit log
79
     */
80
    protected $auditData = [];
81
82
    /**
83
     * @var boolean $initValidation automatically append validation settings for fields $this->validationRules/Default?
84
     */
85
    protected $initValidation = true;
86
87
    /**
88
     * initialize with array of params
89
     *
90
     */
91
    public function __construct()
92
    {
93
        $f3 = \Base::instance();
94
95
        // guess the table name from the class name if not specified as a class member
96
        $class = \UTF::instance()->substr(strrchr(get_class($this), '\\'),1);
97
        $this->table = $this->mapperName = empty($this->table) ? $f3->snakecase($class) : $this->table;
98
99
        parent::__construct(\Registry::get('db'), $this->table);
100
101
        $this->initValidation(); // automatically create validation settings from reading tables
102
        $this->setupHooks(); // setup hooks for before/after data changes in mapper
103
    }
104
105
    /**
106
     * Load by internal integer ID or by UUID if no array passed
107
     *
108
  	 * @param $filter string|array
109
 	 * @param $options array
110
 	 * @param $ttl int
111
     * @see DB\Cursor::load($filter = NULL, array $options = NULL, $ttl = 0)
112
     * @return array|FALSE
113
     */
114
    public function load($filter = NULL, array $options = null, $ttl = 0) {
115
        if (!is_array($filter)) {
116
            if (is_int($filter)) {
117
                return parent::load(['id = ?', $filter], $options, $ttl);
118
            } else if (is_string($filter)) {
119
                return parent::load(['uuid = ?', $filter], $options, $ttl);
120
            }
121
        }
122
        return parent::load($filter, $options, $ttl);
123
    }
124
125
    /**
126
     * Initialise automatic validation settings for fields
127
     */
128
    public function initValidation()
129
    {
130
        if (!$this->initValidation) {
131
            return;
132
        }
133
134
        // work out default validation rules from schema and cache them
135
        $validationRules   = [];
136
        foreach ($this->schema() as $field => $metadata) {
137
            if ('id' == $field)  {
138
                continue;
139
            }
140
141
            $validationRules[$field] = '';
142
            $rules   = [];
143
144
            if (empty($metadata['nullable']) || !empty($metadata['pkey'])) {
145
                // special case, id for internal use so we don't interfere with this
146
                $rules[] = 'required';
147
            }
148
149
            if (preg_match('/^(?<type>[^(]+)\(?(?<length>[^)]+)?/i', $metadata['type'], $matches)) {
150
                switch ($matches['type']) {
151
                    case 'char':
152
                    case 'varchar':
153
                        $rules[] = 'max_len,' . $matches['length'];
154
                        break;
155
156
                    case 'text':
157
                        $rules[] = 'max_len,65535';
158
                        break;
159
160
                    case 'int':
161
                        $rules[] = 'integer|min_numeric,0';
162
                        break;
163
164
                    case 'datetime':
165
                        $rules[] = 'date|min_len,0|max_len,19';
166
                        break;
167
168
                    default:
169
                        break;
170
                }
171
                $validationRules[$field] = empty($rules) ? '' : join('|', $rules);
172
            }
173
        }
174
175
        // set default validation rules
176
        foreach ($this->validationRules as $field => $rule) {
177
            if (empty($rule)) {
178
                continue;
179
            }
180
            $validationRules[$field] = empty($validationRules[$field]) ? $rule :
181
                                                $validationRules[$field] .  '|' . $rule;
182
        }
183
184
        // save default validation rules and filter rules in-case we add rules
185
        $this->validationRulesDefault = $this->validationRules = $validationRules;
186
        $this->filterRulesDefault = $this->filterRules;
187
    }
188
189
    /**
190
     * Initialise hooks for the mapper object actions
191
     */
192
    public function setupHooks()
193
    {
194
        // set original data when object loaded
195
        $this->onload(function($mapper){
196
            $mapper->originalData = $mapper->cast();
197
        });
198
199
        // filter data, set UUID and date created before insert
200
        $this->beforeinsert(function($mapper){
201
            $mapper->setUUID($mapper->uuidField);
202
            $mapper->copyFrom($mapper->filter());
203
            if (in_array('created', $mapper->fields()) && empty($mapper->created)) {
204
                $mapper->created = Helpers\Time::database();
205
            }
206
            return $mapper->validate();
207
        });
208
209
        // filter data, set updated field if present before update
210
        $this->beforeupdate(function($mapper){
211
            $mapper->copyFrom($mapper->filter());
212
            if (in_array('updated', $mapper->fields())) {
213
                $mapper->updated = Helpers\Time::database();
214
            }
215
            return $mapper->validate();
216
        });
217
218
        // write audit data after save
219
        $this->aftersave(function($mapper){
220
            if ('audit' == $mapper->mapperName) {
221
                return;
222
            }
223
            $data = array_merge([
224
                'event' => (empty($mapper->originalData) ? 'created-'  : 'updated-') . $mapper->mapperName,
225
                'old' => $mapper->originalData,
226
                'new' => $mapper->cast()
227
            ], $this->auditData);
228
            Models\Audit::instance()->write($data);
229
            $mapper->originalData = $data['new'];
230
            $mapper->auditData = [];
231
        });
232
233
        // write audit data after erase
234
        $this->aftererase(function($mapper){
235
            if ('audit' == $mapper->mapperName) {
236
                return;
237
            }
238
            Models\Audit::instance()->write(array_merge([
239
                'event' => 'deleted-' . $mapper->mapperName,
240
                'old' => $mapper->originalData,
241
                'new' => $mapper->cast()
242
            ], $this->auditData));
243
            $mapper->originalData = $mapper->auditData = [];
244
        });
245
    }
246
247
    /**
248
     * return string representation of class - json of data
249
     *
250
     * @param string
251
     */
252
    public function __toString(): string
253
    {
254
        return json_encode($this->cast(), JSON_PRETTY_PRINT);
255
    }
256
257
258
    /**
259
     * return array representation of class - json of data
260
     *
261
     * @param array
262
     */
263
    public function __toArray(): array
264
    {
265
        return $this->cast();
266
    }
267
268
    /**
269
     * Cast the mapper data to an array using only provided fields
270
     *
271
     * @param mixed string|array fields to return in response
272
     * @param array optional data optional data to use instead of fields
273
     * @return array $data
274
     */
275
    public function castFields($fields = null, array $data = []): array
276
    {
277 View Code Duplication
        if (!empty($fields)) {
278
            if (is_string($fields)) {
279
                $fields = preg_split("/[\s,]+/", strtolower($fields));
280
            } else if (!is_array($fields)) {
281
                $fields = [];
282
            }
283
        }
284
285
        if (empty($data) || !is_array($data)) {
286
            $data = $this->cast();
287
        }
288
289
        if (empty($fields)) {
290
            $fields = array_keys($data);
291
        }
292
293
        // remove fields not in the list
294
        foreach ($data as $k => $v) {
295
            if (!in_array($k, $fields)) {
296
                unset($data[$k]);
297
            }
298
        }
299
300
        $data['object'] = $this->table;
301
302
        return $data;
303
    }
304
305
    /**
306
     * Cast the mapper data to an array and modify (for external clients typically)
307
     * using the visible fields and names for export, converting dates to unixtime
308
     * optionally pass in a comma (or space)-separated list of fields or an array of fields
309
     *
310
     * @param mixed string|array fields to return in response
311
     * @param array optional data optional data to use instead of fields
312
     * @return array $data
313
     */
314
    public function exportArray($fields = null, array $data = []): array
315
    {
316 View Code Duplication
        if (!empty($fields)) {
317
            if (is_string($fields)) {
318
                $fields = preg_split("/[\s,]+/", strtolower($fields));
319
            } else if (!is_array($fields)) {
320
                $fields = [];
321
            }
322
        }
323
324
        if (empty($data) || !is_array($data)) {
325
            $data = $this->cast();
326
        }
327
328
        foreach ($data as $k => $v) {
329
            if (array_key_exists($k, $this->fieldsVisible)) {
330
                unset($data[$k]);
331
                if (empty($this->fieldsVisible[$k])) {
332
                    continue;
333
                }
334
                // use the alias of the field
335
                $k = $this->fieldsVisible[$k];
336
                $data[$k] = $v;
337
            }
338
339
            // convert date to unix timestamp
340
            if ('updated' == $k || 'created' == $k || (
341
                    \UTF::instance()->strlen($v) == 19 &&
342
                    preg_match("/^[\d]{4}-[\d]{2}-[\d]{2}[\s]+[\d]{2}:[\d]{2}:[\d]{2}/", $v, $m))) {
343
                $time = strtotime($v);
344
                $data[$k] = ($time < 0) ? 0 : $time;
345
            }
346
            if (!empty($fields) && $k !== 'id' && $k !== 'object' && !in_array($k, $fields)) {
347
                unset($data[$k]);
348
            }
349
        }
350
351
        $data['object'] = $this->table;
352
353
        return $data;
354
    }
355
356
357
    /**
358
     * Convert the mapper object to format suitable for JSON
359
     *
360
     * @param boolean $unmodified cast as public (visible) data or raw db data?
361
     * @param mixed $fields optional string|array fields to include
362
     * @return string json-encoded data
363
     */
364
    public function exportJson(bool $unmodified = false, $fields = null): string
365
    {
366
        return json_encode(empty($unmodified) ? $this->castFields($fields) : $this->exportArray($fields), JSON_PRETTY_PRINT);
367
    }
368
369
370
    /**
371
     * Set a field (default named uuid) to a UUID value if one is not present.
372
     *
373
     * @param string $field the name of the field to check and set
374
     * @param int $len length of uuid to return
375
     * @return null|string $uuid the new uuid generated
376
     */
377
    public function setUUID(string $field = 'uuid', $len = 8)
378
    {
379
        $db = \Registry::get('db');
380
        // a proper uuid is actually 36 characters but we don't need so many
381
        if (in_array($field, $this->fields()) &&
382
            (empty($this->$field) || strlen($this->$field) < 36)) {
383
            $tmp = clone $this;
384
385
            do {
386
                $uuid = Helpers\Str::uuid($len);
387
            }
388
            while ($tmp->load([$db->quotekey($field) . ' = ?', $uuid]));
389
390
            unset($tmp);
391
            $this->$field = $uuid;
392
            return $uuid;
393
        }
394
        return empty($this->$field) ? null : $this->$field;
395
    }
396
397
398
    /**
399
     * Write data for audit logging
400
     *
401
     * @param $data array of data to audit log
402
     * @return array $this->auditData return the updated audit data for the mapper
403
     */
404
    public function audit(array $data = []): array
405
    {
406
        $this->auditData = array_merge($this->auditData, $data);
407
        return $this->auditData;
408
    }
409
}
410