Completed
Push — dev-master ( c85948...388867 )
by Vijay
03:35
created

Mapper::load()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 9
nc 5
nop 3
dl 0
loc 13
rs 8.8571
c 0
b 0
f 0
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 or reload if null
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 (NULL === $filter && !empty($this->id)) {
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<FFCMS\Mappers\Mapper>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
116
            return $this->load($this->id);
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<FFCMS\Mappers\Mapper>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

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