Completed
Push — master ( 9f0ecb...4a849c )
by Oleg
06:13
created

Model::create()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
rs 6.7272
cc 7
eloc 16
nc 7
nop 0
1
<?php /** MicroModel */
2
3
namespace Micro\Mvc\Models;
4
5
use Micro\Base\Exception;
6
use Micro\Db\ConnectionInjector;
7
use Micro\File\Type;
8
use Micro\Form\FormModel;
9
10
/**
11
 * Model class file.
12
 *
13
 * @author Oleg Lunegov <[email protected]>
14
 * @link https://github.com/linpax/microphp-framework
15
 * @copyright Copyright (c) 2013 Oleg Lunegov
16
 * @license https://github.com/linpax/microphp-framework/blob/master/LICENSE
17
 * @package Micro
18
 * @subpackage Mvc\Models
19
 * @version 1.0
20
 * @since 1.0
21
 * @abstract
22
 */
23
abstract class Model extends FormModel implements IModel
24
{
25
    /** @var string $primaryKey Primary key on table */
26
    public static $primaryKey = 'id';
27
    /** @var string $tableName Table name */
28
    public static $tableName;
29
30
    /** @var boolean $_isNewRecord Is new record? */
31
    protected $_isNewRecord = false;
32
    /** @var array $cacheRelations Cached loads relations */
33
    protected $cacheRelations = [];
34
35
36
    /**
37
     * Constructor for model
38
     *
39
     * @access public
40
     *
41
     * @param boolean $new is new model?
42
     *
43
     * @result void
44
     * @throws Exception
45
     */
46
    public function __construct($new = true)
47
    {
48
        parent::__construct();
49
50
        if (!static::$tableName) {
51
            throw new Exception('Table name not set in `'.__CLASS__.'`` model.');
52
        }
53
54
        $this->_isNewRecord = $new;
55
    }
56
57
    /**
58
     * Finder by primary key
59
     *
60
     * @access public
61
     *
62
     * @param int|string $value unique value
63
     *
64
     * @return mixed
65
     * @throws \Micro\base\Exception
66
     * @static
67
     */
68
    public static function findByPk($value)
69
    {
70
        return self::findByAttributes([self::$primaryKey => $value], true);
71
    }
72
73
    /**
74
     * Find models by attributes
75
     *
76
     * @access public
77
     *
78
     * @param array $attributes attributes and data for search
79
     * @param bool $single single or more
80
     *
81
     * @return mixed
82
     * @throws \Micro\base\Exception
83
     */
84
    public static function findByAttributes(array $attributes = [], $single = false)
85
    {
86
        $query = new Query((new ConnectionInjector)->getDriver());
87
        foreach ($attributes AS $key => $val) {
88
            $query->addWhere($key.' = :'.$key);
89
        }
90
        $query->params = $attributes;
91
92
        return self::finder($query, $single);
93
    }
94
95
    /**
96
     * Finder data in DB
97
     *
98
     * @access public
99
     *
100
     * @param IQuery $query query to search
101
     * @param boolean $single is single
102
     *
103
     * @return mixed One or more data
104
     * @throws \Micro\base\Exception
105
     * @static
106
     */
107
    public static function finder(IQuery $query = null, $single = false)
108
    {
109
        $query = ($query instanceof Query) ? $query : new Query((new ConnectionInjector)->getDriver());
110
        $query->table = static::$tableName . ' m';
111
        $query->objectName = get_called_class();
112
        $query->single = $single;
113
114
        return $query->run();
115
    }
116
117
    /**
118
     * Find by model attribute values
119
     *
120
     * @access public
121
     *
122
     * @param bool $single Is a single?
123
     *
124
     * @return mixed
125
     * @throws \Micro\base\Exception
126
     */
127
    public function find($single = false)
128
    {
129
        return self::findByAttributes(Type::getVars($this), $single);
130
    }
131
132
    /**
133
     * Get attributes defined into model
134
     *
135
     * @access public
136
     *
137
     * @return array
138
     * @throws Exception
139
     */
140
    public function getAttributes()
141
    {
142
        $fields = [];
143
        foreach ((new ConnectionInjector)->getDriver()->listFields(static::$tableName) AS $field) {
144
            $fields[] = $field['field'];
145
        }
146
147
        return $fields;
148
    }
149
150
    /**
151
     * Get relation data or magic properties
152
     *
153
     * @access public
154
     *
155
     * @param string $name
156
     *
157
     * @return mixed
158
     * @throws Exception
159
     */
160
    public function __get($name)
161
    {
162
        /** @var array $relation */
163
        if ($relation = $this->relations()->get($name)) {
164
            if (empty($this->cacheRelations[$name])) {
165
                $sql = new Query((new ConnectionInjector)->getDriver());
166
167
                if ((new ConnectionInjector)->getDriver()->getDriverType() === 'pgsql') {
168
                    $sql->addWhere('"m"."' . $relation['On'][1] . '"=:' . $relation['On'][0]);
169
                } else {
170
                    $sql->addWhere('`m`.`' . $relation['On'][1] . '`=:' . $relation['On'][0]);
171
                }
172
173
                if ($relation['Where']) {
174
                    $sql->addWhere($relation['Where']);
175
                }
176
                if ($relation['Params']) {
177
                    $sql->params = $relation['Params'];
178
                }
179
                if ($relation['Limit'] > 0) {
180
                    $sql->limit = $relation['Limit'];
181
                }
182
183
                $sql->params[$relation['On'][0]] = $this->{$relation['On'][0]};
184
185
                /** @noinspection PhpUndefinedMethodInspection */
186
                $this->cacheRelations[$name] = $relation['Model']::finder($sql, !$relation['IsMany']);
187
            }
188
189
            return $this->cacheRelations[$name];
190
        } elseif (isset($this->$name)) {
191
            return $this->$name;
192
        }
193
194
        return false;
195
    }
196
197
    /**
198
     * @param string $name
199
     * @param mixed $value
200
     */
201
    public function __set($name, $value)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
202
    {
203
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208
    public function relations()
209
    {
210
        return new Relations;
211
    }
212
213
    /**
214
     * @param string $name
215
     * @return boolean
216
     */
217
    public function __isset($name)
218
    {
219
        return (bool)$this->$name;
220
    }
221
222
    /**
223
     * Save changes
224
     *
225
     * @access public
226
     *
227
     * @param bool $validate Validated data?
228
     *
229
     * @return boolean
230
     * @throws Exception
231
     */
232
    final public function save($validate = false)
233
    {
234
        if ($validate && !$this->validate()) {
235
            return false;
236
        }
237
238
        if ($this->isNewRecord()) {
239
            return $this->create();
240
        } else {
241
            if ($this->beforeSave() && $this->update()) {
242
                $this->afterSave();
243
244
                return true;
245
            }
246
        }
247
248
        return false;
249
    }
250
251
    /**
252
     * Is new record?
253
     *
254
     * @access public
255
     * @return boolean
256
     */
257
    public function isNewRecord()
258
    {
259
        return $this->_isNewRecord;
260
    }
261
262
    /**
263
     * Create changes
264
     *
265
     * @access public
266
     * @return boolean
267
     * @throws Exception
268
     */
269
    final public function create()
270
    {
271
        if (!$this->isNewRecord()) {
272
            return false;
273
        }
274
        if ($this->beforeCreate() && $this->beforeSave()) {
275
            $id = (new ConnectionInjector)->getDriver()->insert(static::$tableName,
276
                $this->mergeAttributesDb());
277
            if (!$id) {
278
                return false;
279
            }
280
281
            $pKey = self::$primaryKey ?: 'id';
282
            if ($this->checkAttributeExists($pKey)) {
283
                $this->$pKey = $id;
284
            }
285
286
            $this->_isNewRecord = false;
287
288
            $this->afterCreate();
289
            $this->afterSave();
290
291
            return true;
292
        }
293
294
        return false;
295
    }
296
297
    /**
298
     * @inheritdoc
299
     */
300
    public function beforeCreate()
301
    {
302
        return true;
303
    }
304
305
    /**
306
     * @inheritdoc
307
     */
308
    public function beforeSave()
309
    {
310
        return true;
311
    }
312
313
    /**
314
     * Merge local attributes and db attributes
315
     *
316
     * @access protected
317
     *
318
     * @return array
319
     * @throws \Micro\base\Exception
320
     */
321
    protected function mergeAttributesDb()
322
    {
323
        $arr = Type::getVars($this);
324
325
        $buffer = [];
326
        foreach ((new ConnectionInjector)->getDriver()->listFields(static::$tableName) AS $row) {
327
            $buffer[] = $row['field'];
328
        }
329
330
        foreach ($arr AS $key => $val) {
331
            if (!in_array($key, $buffer, true)) {
332
                unset($arr[$key]);
333
            }
334
        }
335
336
        unset($arr['isNewRecord']);
337
338
        return $arr;
339
    }
340
341
    /**
342
     * Check attribute exists into table
343
     *
344
     * @access public
345
     *
346
     * @param string $name Attribute name
347
     *
348
     * @return boolean
349
     * @throws Exception
350
     */
351
    public function checkAttributeExists($name)
352
    {
353
        if (isset($this->$name)) {
354
            return true;
355
        }
356
357
        $res = false;
358
        foreach ((new ConnectionInjector)->getDriver()->listFields(static::$tableName) AS $row) {
359
            if ($row['field'] === $name) {
360
                $res = true;
361
                break;
362
            }
363
        }
364
365
        return $res;
366
    }
367
368
    /**
369
     * @inheritdoc
370
     */
371
    public function afterCreate()
372
    {
373
    }
374
375
    /**
376
     * @inheritdoc
377
     */
378
    public function afterSave()
379
    {
380
    }
381
382
    /**
383
     * Update changes
384
     *
385
     * @access public
386
     *
387
     * @param string $where condition for search
388
     *
389
     * @throws Exception
390
     * @return boolean
391
     */
392
    final public function update($where = null)
393
    {
394
        if ($this->isNewRecord()) {
395
            return false;
396
        }
397
        if ($this->beforeUpdate()) {
398
            if (!$where) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $where of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
399
                if (self::$primaryKey) {
400
                    if ((new ConnectionInjector)->getDriver()->getDriverType() === 'pgsql') {
401
                        $where .= '"' . self::$primaryKey . '" = :' . self::$primaryKey;
402
                    } else {
403
                        $where .= '`' . self::$primaryKey . '` = :' . self::$primaryKey;
404
                    }
405
406
                } else {
407
                    throw new Exception('In table '.static::$tableName.' option `'.self::$primaryKey.'` not defined/not use.'
408
                    );
409
                }
410
            }
411
412
            if ((new ConnectionInjector)->getDriver()->update(static::$tableName, $this->mergeAttributesDb(), $where)) {
413
                $this->afterUpdate();
414
415
                return true;
416
            }
417
        }
418
419
        return false;
420
    }
421
422
    /**
423
     * @inheritdoc
424
     */
425
    public function beforeUpdate()
426
    {
427
        return true;
428
    }
429
430
    /**
431
     * @inheritdoc
432
     */
433
    public function afterUpdate()
434
    {
435
    }
436
437
    /**
438
     * Delete changes
439
     *
440
     * @access public
441
     * @return boolean
442
     * @throws Exception
443
     */
444
    final public function delete()
445
    {
446
        if ($this->isNewRecord()) {
447
            return false;
448
        }
449
        if ($this->beforeDelete()) {
450
            if (!self::$primaryKey) {
451
                throw new Exception('In table '.static::$tableName.' option `'.self::$primaryKey.'` not defined/not use.');
452
            }
453
454
            if (
455
            (new ConnectionInjector)->getDriver()->delete(
456
                static::$tableName,
457
                self::$primaryKey.'=:'.self::$primaryKey, [self::$primaryKey => $this->{self::$primaryKey}]
458
            )
459
            ) {
460
                $this->afterDelete();
461
                unset($this);
462
463
                return true;
464
            }
465
        }
466
467
        return false;
468
    }
469
470
    /**
471
     * @inheritdoc
472
     */
473
    public function beforeDelete()
474
    {
475
        return true;
476
    }
477
478
    /**
479
     * @inheritdoc
480
     */
481
    public function afterDelete()
482
    {
483
    }
484
}
485