Mapper::exportJson()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
eloc 2
nc 1
nop 2
1
<?php
2
3
namespace FFCMS\Mappers;
4
5
use FFMVC\Helpers;
6
use FFCMS\{Traits, Models, Exceptions};
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 int $id
25
 * @property string $key
26
 * @property string $value
27
 * @property string $created
28
 */
29
abstract class Mapper extends \DB\SQL\Mapper
30
{
31
    use Traits\Validation;
32
33
    /**
34
     * Fields and their visibility to clients, boolean or string of visible field name
35
     * all fields are visible except where 'false'
36
     *
37
     * @var array $fieldsVisible
38
     */
39
    protected $fieldsVisible = [];
40
41
    /**
42
     * Fields that are editable to clients, boolean or string of visible field name
43
     *
44
     * @var array $fieldsEditable
45
     */
46
    protected $fieldsEditable = [];
47
48
    /**
49
     * @var object database class
50
     */
51
    protected $db;
52
53
    /**
54
     * @var string $table for the mapper - this string gets automatically quoted
55
     */
56
    protected $table;
57
58
    /**
59
     * @var string $mapperName name for the mapper
60
     */
61
    protected $mapperName;
62
63
    /**
64
     * @var string $uuid the fieldname used for the uuid
65
     */
66
    protected $uuidField = 'uuid';
67
68
    /**
69
     * @var boolean $valid the data after validation is valid?
70
     */
71
    protected $valid = null;
72
73
    /**
74
     * @var array $originalData the original data when object created/loaded
75
     */
76
    protected $originalData = [];
77
78
    /**
79
     * @var array $auditData data to write to audit log
80
     */
81
    protected $auditData = [];
82
83
    /**
84
     * @var boolean $initValidation automatically append validation settings for fields $this->validationRules/Default?
85
     */
86
    protected $initValidation = true;
87
88
    /**
89
     * initialize with array of params
90
     *
91
     */
92
    public function __construct()
93
    {
94
        $f3 = \Base::instance();
95
96
        // guess the table name from the class name if not specified as a class member
97
        $class = \UTF::instance()->substr(strrchr(get_class($this), '\\'),1);
98
        $this->table = $this->mapperName = empty($this->table) ? $f3->snakecase($class) : $this->table;
99
100
        parent::__construct(\Registry::get('db'), $this->table);
101
102
        $this->initValidation(); // automatically create validation settings from reading tables
103
        $this->setupHooks(); // setup hooks for before/after data changes in mapper
104
    }
105
106
    /**
107
     * Load by internal integer ID or by UUID if no array passed or reload if null
108
     *
109
  	 * @param $filter string|array
110
 	 * @param $options array
111
 	 * @param $ttl int
112
     * @see DB\Cursor::load($filter = NULL, array $options = NULL, $ttl = 0)
113
     * @return array|FALSE
114
     */
115
    public function load($filter = NULL, array $options = null, $ttl = 0) {
116
        if (NULL === $filter && !empty($this->id)) {
117
            return $this->load($this->id);
118
        }
119
        if (!is_array($filter)) {
120
            if (is_int($filter)) {
121
                return parent::load(['id = ?', $filter], $options, $ttl);
122
            } else if (is_string($filter)) {
123
                return parent::load(['uuid = ?', $filter], $options, $ttl);
124
            }
125
        }
126
        return parent::load($filter, $options, $ttl);
127
    }
128
129
    /**
130
     * Initialise automatic validation settings for fields
131
     */
132
    public function initValidation()
133
    {
134
        if (!$this->initValidation) {
135
            return;
136
        }
137
138
        // work out default validation rules from schema and cache them
139
        $validationRules   = [];
140
        foreach ($this->schema() as $field => $metadata) {
141
            if (in_array($field, ['id', 'uuid', 'created', 'updated']))  {
142
                continue;
143
            }
144
145
            $validationRules[$field] = '';
146
            $rules   = [];
147
148
            if (preg_match('/^(?<type>[^(]+)\(?(?<length>[^)]+)?/i', $metadata['type'], $matches)) {
149
                switch ($matches['type']) {
150
                    case 'char':
151
                    case 'varchar':
152
                        $rules[] = 'max_len,' . $matches['length'];
153
                        break;
154
155
                    case 'text':
156
                        $rules[] = 'max_len,65535';
157
                        break;
158
159
                    case 'int':
160
                        $rules[] = 'integer|min_numeric,0';
161
                        break;
162
163
                    case 'datetime':
164
                        $rules[] = 'date|min_len,0|max_len,19';
165
                        break;
166
167
                    default:
168
                        break;
169
                }
170
171
                if (empty($metadata['default']) || 'datetime' !== $matches['type']) {
172
                    if (empty($metadata['nullable']) || !empty($metadata['pkey'])) {
173
                        // special case, id for internal use so we don't interfere with this
174
                        $rules[] = 'required';
175
                    }
176
                }
177
178
                $validationRules[$field] = empty($rules) ? '' : join('|', $rules);
179
            }
180
        }
181
182
        // set default validation rules
183
        foreach ($this->validationRules as $field => $rule) {
184
            if (empty($rule)) {
185
                continue;
186
            }
187
            $validationRules[$field] = empty($validationRules[$field]) ? $rule :
188
                                                $validationRules[$field] .  '|' . $rule;
189
        }
190
191
        // save default validation rules and filter rules in-case we add rules
192
        $this->validationRulesDefault = $this->validationRules = $validationRules;
193
        $this->filterRulesDefault = $this->filterRules;
194
    }
195
196
    /**
197
     * Initialise hooks for the mapper object actions
198
     */
199
    public function setupHooks()
200
    {
201
        // set original data when object loaded
202
        $this->onload(function($mapper){
203
            $mapper->originalData = $mapper->cast();
204
        });
205
206
        // filter data, set UUID and date created before insert
207
        $this->beforeinsert(function($mapper){
208
            $mapper->setUUID($mapper->uuidField);
209
            $mapper->copyFrom($mapper->filter());
210
            if (in_array('created', $mapper->fields()) && empty($mapper->created)) {
211
                $mapper->created = Helpers\Time::database();
212
            }
213
            return $mapper->validate();
214
        });
215
216
        // filter data, set updated field if present before update
217
        $this->beforeupdate(function($mapper){
218
            $mapper->copyFrom($mapper->filter());
219
            if (in_array('updated', $mapper->fields())) {
220
                $mapper->updated = Helpers\Time::database();
221
            }
222
            return $mapper->validate();
223
        });
224
225
        // write audit data after save
226
        $this->aftersave(function($mapper){
227
            if ('audit' == $mapper->mapperName) {
228
                return;
229
            }
230
            $data = array_merge([
231
                'event' => (empty($mapper->originalData) ? 'created-'  : 'updated-') . $mapper->mapperName,
232
                'old' => $mapper->originalData,
233
                'new' => $mapper->cast()
234
            ], $this->auditData);
235
            Models\Audit::instance()->write($data);
236
            $mapper->originalData = $data['new'];
237
            $mapper->auditData = [];
238
        });
239
240
        // write audit data after erase
241
        $this->aftererase(function($mapper){
242
            if ('audit' == $mapper->mapperName) {
243
                return;
244
            }
245
            Models\Audit::instance()->write(array_merge([
246
                'event' => 'deleted-' . $mapper->mapperName,
247
                'old' => $mapper->originalData,
248
                'new' => $mapper->cast()
249
            ], $this->auditData));
250
            $mapper->originalData = $mapper->auditData = [];
251
        });
252
    }
253
254
255
    /**
256
     * Quote a database fieldname/key
257
     *
258
     * @param string $key String to quote as a database key
259
     * @param string $key
260
     * @param return string
261
     */
262
    public function quotekey(string $key): string
263
    {
264
        if (!in_array($key, $this->fields())) {
265
            throw new Exceptions\InvalidArgumentException('No such key ' . $key . ' exists for mapper ' . $this->mapperName);
266
        }
267
        return $this->db->quotekey($key);
268
    }
269
270
271
    /**
272
     * Quote a database value
273
     *
274
     * @param mixed $value Value to quote
275
     * @param mixed $value
276
     * @param null|return string
277
     */
278
    public function quote($value)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
279
    {
280
        return $this->db->quote($value);
281
    }
282
283
284
    /**
285
     * return string representation of class - json of data
286
     *
287
     * @param string
288
     */
289
    public function __toString(): string
290
    {
291
        return json_encode($this->cast(), JSON_PRETTY_PRINT);
292
    }
293
294
295
    /**
296
     * return array representation of class - json of data
297
     *
298
     * @param array
299
     */
300
    public function __toArray(): array
301
    {
302
        return $this->cast();
303
    }
304
305
    /**
306
     * Cast the mapper data to an array using only provided fields
307
     *
308
     * @param mixed string|array fields to return in response
309
     * @param array optional data optional data to use instead of fields
310
     * @return array $data
311
     */
312
    public function castFields($fields = null, array $data = []): array
313
    {
314 View Code Duplication
        if (!empty($fields)) {
315
            if (is_string($fields)) {
316
                $fields = preg_split("/[\s,]+/", strtolower($fields));
317
            } else if (!is_array($fields)) {
318
                $fields = [];
319
            }
320
        }
321
322
        if (empty($data) || !is_array($data)) {
323
            $data = $this->cast();
324
        }
325
326
        if (empty($fields)) {
327
            $fields = array_keys($data);
328
        }
329
330
        // remove fields not in the list
331
        foreach ($data as $k => $v) {
332
            if (!in_array($k, $fields)) {
333
                unset($data[$k]);
334
            }
335
        }
336
337
        $data['object'] = $this->table;
338
339
        return $data;
340
    }
341
342
    /**
343
     * Cast the mapper data to an array and modify (for external clients typically)
344
     * using the visible fields and names for export, converting dates to unixtime
345
     * optionally pass in a comma (or space)-separated list of fields or an array of fields
346
     *
347
     * @param mixed string|array fields to return in response
348
     * @param array optional data optional data to use instead of fields
349
     * @return array $data
350
     */
351
    public function exportArray($fields = null, array $data = []): array
352
    {
353 View Code Duplication
        if (!empty($fields)) {
354
            if (is_string($fields)) {
355
                $fields = preg_split("/[\s,]+/", strtolower($fields));
356
            } else if (!is_array($fields)) {
357
                $fields = [];
358
            }
359
        }
360
361
        if (empty($data) || !is_array($data)) {
362
            $data = $this->cast();
363
        }
364
365
        foreach ($data as $k => $v) {
366
            if (array_key_exists($k, $this->fieldsVisible)) {
367
                unset($data[$k]);
368
                if (empty($this->fieldsVisible[$k])) {
369
                    continue;
370
                }
371
                // use the alias of the field
372
                $k = $this->fieldsVisible[$k];
373
                $data[$k] = $v;
374
            }
375
376
            // convert date to unix timestamp
377
            if ('updated' == $k || 'created' == $k || (
378
                    \UTF::instance()->strlen($v) == 19 &&
379
                    preg_match("/^[\d]{4}-[\d]{2}-[\d]{2}[\s]+[\d]{2}:[\d]{2}:[\d]{2}/", $v, $m))) {
380
                $time = strtotime($v);
381
                $data[$k] = ($time < 0) ? 0 : $time;
382
            }
383
            if (!empty($fields) && $k !== 'id' && $k !== 'object' && !in_array($k, $fields)) {
384
                unset($data[$k]);
385
            }
386
        }
387
388
        $data['object'] = $this->table;
389
390
        return $data;
391
    }
392
393
394
    /**
395
     * Convert the mapper object to format suitable for JSON
396
     *
397
     * @param boolean $unmodified cast as public (visible) data or raw db data?
398
     * @param mixed $fields optional string|array fields to include
399
     * @return string json-encoded data
400
     */
401
    public function exportJson(bool $unmodified = false, $fields = null): string
402
    {
403
        return json_encode(empty($unmodified) ? $this->castFields($fields) : $this->exportArray($fields), JSON_PRETTY_PRINT);
404
    }
405
406
407
    /**
408
     * Set a field (default named uuid) to a UUID value if one is not present.
409
     *
410
     * @param string $field the name of the field to check and set
411
     * @param int $len length of uuid to return
412
     * @return null|string $uuid the new uuid generated
413
     */
414
    public function setUUID(string $field = 'uuid', $len = 8)
415
    {
416
        // a proper uuid is actually 36 characters but we don't need so many
417
        if (in_array($field, $this->fields()) &&
418
            (empty($this->$field) || strlen($this->$field) < 36)) {
419
            $tmp = clone $this;
420
421
            do {
422
                $uuid = Helpers\Str::uuid($len);
423
            }
424
            while ($tmp->load([$this->quotekey($field) . ' = ?', $uuid]));
425
426
            unset($tmp);
427
            $this->$field = $uuid;
428
            return $uuid;
429
        }
430
        return empty($this->$field) ? null : $this->$field;
431
    }
432
433
434
    /**
435
     * Write data for audit logging
436
     *
437
     * @param $data array of data to audit log
438
     * @return array $this->auditData return the updated audit data for the mapper
439
     */
440
    public function audit(array $data = []): array
441
    {
442
        $this->auditData = array_merge($this->auditData, $data);
443
        return $this->auditData;
444
    }
445
446
}
447