Completed
Push — master ( 118fa7...4fdbde )
by Oleg
04:18
created

Model::update()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 29
rs 6.7272
cc 7
eloc 16
nc 9
nop 1
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)->build());
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)->build());
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)->build()->listFields(static::$tableName) AS $field) {
0 ignored issues
show
Documentation Bug introduced by
The method listFields does not exist on object<Micro\Db\IConnection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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)->build());
166
167
                $sql->addWhere('`m`.`'.$relation['On'][1].'`=:'.$relation['On'][0]);
168
169
                if ($relation['Where']) {
170
                    $sql->addWhere($relation['Where']);
171
                }
172
                if ($relation['Params']) {
173
                    $sql->params = $relation['Params'];
174
                }
175
                if ($relation['Limit'] > 0) {
176
                    $sql->limit = $relation['Limit'];
177
                }
178
179
                $sql->params[$relation['On'][0]] = $this->{$relation['On'][0]};
180
181
                /** @noinspection PhpUndefinedMethodInspection */
182
                $this->cacheRelations[$name] = $relation['Model']::finder($sql, !$relation['IsMany']);
183
            }
184
185
            return $this->cacheRelations[$name];
186
        } elseif (isset($this->$name)) {
187
            return $this->$name;
188
        }
189
190
        return false;
191
    }
192
193
    /**
194
     * @inheritdoc
195
     */
196
    public function relations()
197
    {
198
        return new Relations;
199
    }
200
201
    /**
202
     * Save changes
203
     *
204
     * @access public
205
     *
206
     * @param bool $validate Validated data?
207
     *
208
     * @return boolean
209
     * @throws Exception
210
     */
211
    final public function save($validate = false)
212
    {
213
        if ($validate && !$this->validate()) {
214
            return false;
215
        }
216
217
        if ($this->isNewRecord()) {
218
            return $this->create();
219
        } else {
220
            if ($this->beforeSave() && $this->update()) {
221
                $this->afterSave();
222
223
                return true;
224
            }
225
        }
226
227
        return false;
228
    }
229
230
    /**
231
     * Is new record?
232
     *
233
     * @access public
234
     * @return boolean
235
     */
236
    public function isNewRecord()
237
    {
238
        return $this->_isNewRecord;
239
    }
240
241
    /**
242
     * Create changes
243
     *
244
     * @access public
245
     * @return boolean
246
     * @throws Exception
247
     */
248
    final public function create()
249
    {
250
        if (!$this->isNewRecord()) {
251
            return false;
252
        }
253
        if ($this->beforeCreate() && $this->beforeSave()) {
254
            $id = (new ConnectionInjector)->build()->insert(static::$tableName, $this->mergeAttributesDb());
0 ignored issues
show
Documentation Bug introduced by
The method insert does not exist on object<Micro\Db\IConnection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
255
            if (!$id) {
256
                return false;
257
            }
258
259
            $pKey = self::$primaryKey ?: 'id';
260
            if ($this->checkAttributeExists($pKey)) {
261
                $this->$pKey = $id;
262
            }
263
264
            $this->_isNewRecord = false;
265
266
            $this->afterCreate();
267
            $this->afterSave();
268
269
            return true;
270
        }
271
272
        return false;
273
    }
274
275
    /**
276
     * @inheritdoc
277
     */
278
    public function beforeCreate()
279
    {
280
        return true;
281
    }
282
283
    /**
284
     * @inheritdoc
285
     */
286
    public function beforeSave()
287
    {
288
        return true;
289
    }
290
291
    /**
292
     * Merge local attributes and db attributes
293
     *
294
     * @access protected
295
     *
296
     * @return array
297
     * @throws \Micro\base\Exception
298
     */
299
    protected function mergeAttributesDb()
300
    {
301
        $arr = Type::getVars($this);
302
303
        $buffer = [];
304
        foreach ((new ConnectionInjector)->build()->listFields(static::$tableName) AS $row) {
0 ignored issues
show
Documentation Bug introduced by
The method listFields does not exist on object<Micro\Db\IConnection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
305
            $buffer[] = $row['field'];
306
        }
307
308
        foreach ($arr AS $key => $val) {
309
            if (!in_array($key, $buffer, true)) {
310
                unset($arr[$key]);
311
            }
312
        }
313
314
        unset($arr['isNewRecord']);
315
316
        return $arr;
317
    }
318
319
    /**
320
     * Check attribute exists into table
321
     *
322
     * @access public
323
     *
324
     * @param string $name Attribute name
325
     *
326
     * @return boolean
327
     * @throws Exception
328
     */
329
    public function checkAttributeExists($name)
330
    {
331
        if (isset($this->$name)) {
332
            return true;
333
        }
334
335
        $res = false;
336
        foreach ((new ConnectionInjector)->build()->listFields(static::$tableName) AS $row) {
0 ignored issues
show
Documentation Bug introduced by
The method listFields does not exist on object<Micro\Db\IConnection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
337
            if ($row['field'] === $name) {
338
                $res = true;
339
                break;
340
            }
341
        }
342
343
        return $res;
344
    }
345
346
    /**
347
     * @inheritdoc
348
     */
349
    public function afterCreate()
350
    {
351
    }
352
353
    /**
354
     * @inheritdoc
355
     */
356
    public function afterSave()
357
    {
358
    }
359
360
    /**
361
     * Update changes
362
     *
363
     * @access public
364
     *
365
     * @param string $where condition for search
366
     *
367
     * @throws Exception
368
     * @return boolean
369
     */
370
    final public function update($where = null)
371
    {
372
        if ($this->isNewRecord()) {
373
            return false;
374
        }
375
        if ($this->beforeUpdate()) {
376
            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...
377
                if (self::$primaryKey) {
378
                    if ((new ConnectionInjector)->build()->getDriverType() === 'pgsql') {
0 ignored issues
show
Documentation Bug introduced by
The method getDriverType does not exist on object<Micro\Db\IConnection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
379
                        $where .= '"' . self::$primaryKey . '" = :' . self::$primaryKey;
380
                    } else {
381
                        $where .= '`' . self::$primaryKey . '` = :' . self::$primaryKey;
382
                    }
383
384
                } else {
385
                    throw new Exception('In table '.static::$tableName.' option `'.self::$primaryKey.'` not defined/not use.'
386
                    );
387
                }
388
            }
389
390
            if ((new ConnectionInjector)->build()->update(static::$tableName, $this->mergeAttributesDb(), $where)) {
0 ignored issues
show
Documentation Bug introduced by
The method update does not exist on object<Micro\Db\IConnection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
391
                $this->afterUpdate();
392
393
                return true;
394
            }
395
        }
396
397
        return false;
398
    }
399
400
    /**
401
     * @inheritdoc
402
     */
403
    public function beforeUpdate()
404
    {
405
        return true;
406
    }
407
408
    /**
409
     * @inheritdoc
410
     */
411
    public function afterUpdate()
412
    {
413
    }
414
415
    /**
416
     * Delete changes
417
     *
418
     * @access public
419
     * @return boolean
420
     * @throws Exception
421
     */
422
    final public function delete()
423
    {
424
        if ($this->isNewRecord()) {
425
            return false;
426
        }
427
        if ($this->beforeDelete()) {
428
            if (!self::$primaryKey) {
429
                throw new Exception('In table '.static::$tableName.' option `'.self::$primaryKey.'` not defined/not use.');
430
            }
431
432
            if (
433
            (new ConnectionInjector)->build()->delete(
0 ignored issues
show
Documentation Bug introduced by
The method delete does not exist on object<Micro\Db\IConnection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
434
                static::$tableName,
435
                self::$primaryKey.'=:'.self::$primaryKey, [self::$primaryKey => $this->{self::$primaryKey}]
436
            )
437
            ) {
438
                $this->afterDelete();
439
                unset($this);
440
441
                return true;
442
            }
443
        }
444
445
        return false;
446
    }
447
448
    /**
449
     * @inheritdoc
450
     */
451
    public function beforeDelete()
452
    {
453
        return true;
454
    }
455
456
    /**
457
     * @inheritdoc
458
     */
459
    public function afterDelete()
460
    {
461
    }
462
}
463