Completed
Push — dev-master ( 1bf013...29e920 )
by Vijay
03:09
created

Mapper::quote()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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 ('id' == $field)  {
142
                continue;
143
            }
144
145
            $validationRules[$field] = '';
146
            $rules   = [];
147
148
            if (empty($metadata['nullable']) || !empty($metadata['pkey'])) {
149
                // special case, id for internal use so we don't interfere with this
150
                $rules[] = 'required';
151
            }
152
153
            if (preg_match('/^(?<type>[^(]+)\(?(?<length>[^)]+)?/i', $metadata['type'], $matches)) {
154
                switch ($matches['type']) {
155
                    case 'char':
156
                    case 'varchar':
157
                        $rules[] = 'max_len,' . $matches['length'];
158
                        break;
159
160
                    case 'text':
161
                        $rules[] = 'max_len,65535';
162
                        break;
163
164
                    case 'int':
165
                        $rules[] = 'integer|min_numeric,0';
166
                        break;
167
168
                    case 'datetime':
169
                        $rules[] = 'date|min_len,0|max_len,19';
170
                        break;
171
172
                    default:
173
                        break;
174
                }
175
                $validationRules[$field] = empty($rules) ? '' : join('|', $rules);
176
            }
177
        }
178
179
        // set default validation rules
180
        foreach ($this->validationRules as $field => $rule) {
181
            if (empty($rule)) {
182
                continue;
183
            }
184
            $validationRules[$field] = empty($validationRules[$field]) ? $rule :
185
                                                $validationRules[$field] .  '|' . $rule;
186
        }
187
188
        // save default validation rules and filter rules in-case we add rules
189
        $this->validationRulesDefault = $this->validationRules = $validationRules;
190
        $this->filterRulesDefault = $this->filterRules;
191
    }
192
193
    /**
194
     * Initialise hooks for the mapper object actions
195
     */
196
    public function setupHooks()
197
    {
198
        // set original data when object loaded
199
        $this->onload(function($mapper){
200
            $mapper->originalData = $mapper->cast();
201
        });
202
203
        // filter data, set UUID and date created before insert
204
        $this->beforeinsert(function($mapper){
205
            $mapper->setUUID($mapper->uuidField);
206
            $mapper->copyFrom($mapper->filter());
207
            if (in_array('created', $mapper->fields()) && empty($mapper->created)) {
208
                $mapper->created = Helpers\Time::database();
209
            }
210
            return $mapper->validate();
211
        });
212
213
        // filter data, set updated field if present before update
214
        $this->beforeupdate(function($mapper){
215
            $mapper->copyFrom($mapper->filter());
216
            if (in_array('updated', $mapper->fields())) {
217
                $mapper->updated = Helpers\Time::database();
218
            }
219
            return $mapper->validate();
220
        });
221
222
        // write audit data after save
223
        $this->aftersave(function($mapper){
224
            if ('audit' == $mapper->mapperName) {
225
                return;
226
            }
227
            $data = array_merge([
228
                'event' => (empty($mapper->originalData) ? 'created-'  : 'updated-') . $mapper->mapperName,
229
                'old' => $mapper->originalData,
230
                'new' => $mapper->cast()
231
            ], $this->auditData);
232
            Models\Audit::instance()->write($data);
233
            $mapper->originalData = $data['new'];
234
            $mapper->auditData = [];
235
        });
236
237
        // write audit data after erase
238
        $this->aftererase(function($mapper){
239
            if ('audit' == $mapper->mapperName) {
240
                return;
241
            }
242
            Models\Audit::instance()->write(array_merge([
243
                'event' => 'deleted-' . $mapper->mapperName,
244
                'old' => $mapper->originalData,
245
                'new' => $mapper->cast()
246
            ], $this->auditData));
247
            $mapper->originalData = $mapper->auditData = [];
248
        });
249
    }
250
251
252
    /**
253
     * Quote a database fieldname/key
254
     *
255
     * @param string $key String to quote as a database key
256
     * @param string $key
257
     */
258
    public function quotekey(string $key): string
259
    {
260
        if (!in_array($key, $this->fields())) {
261
            throw new Exceptions\InvalidArgumentException('No such key ' . $key . ' exists for mapper ' . $this->mapperName);
262
        }
263
        return $this->db->quotekey($key);
264
    }
265
266
267
    /**
268
     * Quote a database value
269
     *
270
     * @param mixed $value Value to quote
271
     * @param mixed $value
272
     */
273
    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...
274
    {
275
        return $this->db->quote($value);
276
    }
277
278
279
    /**
280
     * return string representation of class - json of data
281
     *
282
     * @param string
283
     */
284
    public function __toString(): string
285
    {
286
        return json_encode($this->cast(), JSON_PRETTY_PRINT);
287
    }
288
289
290
    /**
291
     * return array representation of class - json of data
292
     *
293
     * @param array
294
     */
295
    public function __toArray(): array
296
    {
297
        return $this->cast();
298
    }
299
300
    /**
301
     * Cast the mapper data to an array using only provided fields
302
     *
303
     * @param mixed string|array fields to return in response
304
     * @param array optional data optional data to use instead of fields
305
     * @return array $data
306
     */
307
    public function castFields($fields = null, array $data = []): array
308
    {
309 View Code Duplication
        if (!empty($fields)) {
310
            if (is_string($fields)) {
311
                $fields = preg_split("/[\s,]+/", strtolower($fields));
312
            } else if (!is_array($fields)) {
313
                $fields = [];
314
            }
315
        }
316
317
        if (empty($data) || !is_array($data)) {
318
            $data = $this->cast();
319
        }
320
321
        if (empty($fields)) {
322
            $fields = array_keys($data);
323
        }
324
325
        // remove fields not in the list
326
        foreach ($data as $k => $v) {
327
            if (!in_array($k, $fields)) {
328
                unset($data[$k]);
329
            }
330
        }
331
332
        $data['object'] = $this->table;
333
334
        return $data;
335
    }
336
337
    /**
338
     * Cast the mapper data to an array and modify (for external clients typically)
339
     * using the visible fields and names for export, converting dates to unixtime
340
     * optionally pass in a comma (or space)-separated list of fields or an array of fields
341
     *
342
     * @param mixed string|array fields to return in response
343
     * @param array optional data optional data to use instead of fields
344
     * @return array $data
345
     */
346
    public function exportArray($fields = null, array $data = []): array
347
    {
348 View Code Duplication
        if (!empty($fields)) {
349
            if (is_string($fields)) {
350
                $fields = preg_split("/[\s,]+/", strtolower($fields));
351
            } else if (!is_array($fields)) {
352
                $fields = [];
353
            }
354
        }
355
356
        if (empty($data) || !is_array($data)) {
357
            $data = $this->cast();
358
        }
359
360
        foreach ($data as $k => $v) {
361
            if (array_key_exists($k, $this->fieldsVisible)) {
362
                unset($data[$k]);
363
                if (empty($this->fieldsVisible[$k])) {
364
                    continue;
365
                }
366
                // use the alias of the field
367
                $k = $this->fieldsVisible[$k];
368
                $data[$k] = $v;
369
            }
370
371
            // convert date to unix timestamp
372
            if ('updated' == $k || 'created' == $k || (
373
                    \UTF::instance()->strlen($v) == 19 &&
374
                    preg_match("/^[\d]{4}-[\d]{2}-[\d]{2}[\s]+[\d]{2}:[\d]{2}:[\d]{2}/", $v, $m))) {
375
                $time = strtotime($v);
376
                $data[$k] = ($time < 0) ? 0 : $time;
377
            }
378
            if (!empty($fields) && $k !== 'id' && $k !== 'object' && !in_array($k, $fields)) {
379
                unset($data[$k]);
380
            }
381
        }
382
383
        $data['object'] = $this->table;
384
385
        return $data;
386
    }
387
388
389
    /**
390
     * Convert the mapper object to format suitable for JSON
391
     *
392
     * @param boolean $unmodified cast as public (visible) data or raw db data?
393
     * @param mixed $fields optional string|array fields to include
394
     * @return string json-encoded data
395
     */
396
    public function exportJson(bool $unmodified = false, $fields = null): string
397
    {
398
        return json_encode(empty($unmodified) ? $this->castFields($fields) : $this->exportArray($fields), JSON_PRETTY_PRINT);
399
    }
400
401
402
    /**
403
     * Set a field (default named uuid) to a UUID value if one is not present.
404
     *
405
     * @param string $field the name of the field to check and set
406
     * @param int $len length of uuid to return
407
     * @return null|string $uuid the new uuid generated
408
     */
409
    public function setUUID(string $field = 'uuid', $len = 8)
410
    {
411
        // a proper uuid is actually 36 characters but we don't need so many
412
        if (in_array($field, $this->fields()) &&
413
            (empty($this->$field) || strlen($this->$field) < 36)) {
414
            $tmp = clone $this;
415
416
            do {
417
                $uuid = Helpers\Str::uuid($len);
418
            }
419
            while ($tmp->load([$this->quotekey($field) . ' = ?', $uuid]));
420
421
            unset($tmp);
422
            $this->$field = $uuid;
423
            return $uuid;
424
        }
425
        return empty($this->$field) ? null : $this->$field;
426
    }
427
428
429
    /**
430
     * Write data for audit logging
431
     *
432
     * @param $data array of data to audit log
433
     * @return array $this->auditData return the updated audit data for the mapper
434
     */
435
    public function audit(array $data = []): array
436
    {
437
        $this->auditData = array_merge($this->auditData, $data);
438
        return $this->auditData;
439
    }
440
441
}
442