Completed
Push — master ( 579af5...b29473 )
by Oleg
07:53
created

Model   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 58
lcom 1
cbo 5
dl 0
loc 429
rs 6.3005
c 1
b 1
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A findByPk() 0 4 1
A findByAttributes() 0 10 2
A finder() 0 9 2
A find() 0 4 1
A getAttributes() 0 9 2
C __get() 0 32 7
A relations() 0 4 1
B save() 0 18 6
A isNewRecord() 0 4 1
C create() 0 26 7
A beforeCreate() 0 4 1
A beforeSave() 0 4 1
A mergeAttributesDb() 0 19 4
A checkAttributeExists() 0 16 4
A afterCreate() 0 3 1
A afterSave() 0 3 1
B update() 0 25 6
A beforeUpdate() 0 4 1
A afterUpdate() 0 3 1
B delete() 0 25 5
A beforeDelete() 0 4 1
A afterDelete() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

1
<?php /** MicroModel */
2
3
namespace Micro\Mvc\Models;
4
5
use Micro\Base\Exception;
6
use Micro\Base\IContainer;
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/lugnsk/micro
15
 * @copyright Copyright &copy; 2013 Oleg Lunegov
16
 * @license /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 boolean $_isNewRecord is new record? */
28
    protected $_isNewRecord = false;
29
    /** @var array $cacheRelations cached loads relations */
30
    protected $cacheRelations = [];
31
32
33
    /**
34
     * Constructor for model
35
     *
36
     * @access public
37
     *
38
     * @param IContainer $container
39
     * @param boolean $new is new model?
40
     *
41
     * @result void
42
     */
43
    public function __construct(IContainer $container, $new = true)
44
    {
45
        parent::__construct($container);
46
47
        $this->_isNewRecord = $new;
48
    }
49
50
    /**
51
     * Finder by primary key
52
     *
53
     * @access public
54
     *
55
     * @param int|string $value unique value
56
     * @param IContainer $container
57
     *
58
     * @return mixed
59
     * @throws \Micro\base\Exception
60
     * @static
61
     */
62
    public static function findByPk($value, IContainer $container)
63
    {
64
        return self::findByAttributes([self::$primaryKey => $value], true, $container);
65
    }
66
67
    /**
68
     * Find models by attributes
69
     *
70
     * @access public
71
     *
72
     * @param array $attributes attributes and data for search
73
     * @param bool $single single or more
74
     * @param IContainer $container
75
     *
76
     * @return mixed
77
     * @throws \Micro\base\Exception
78
     */
79
    public static function findByAttributes(array $attributes = [], $single = false, IContainer $container)
80
    {
81
        $query = new Query($container->db);
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
82
        foreach ($attributes AS $key => $val) {
83
            $query->addWhere($key . ' = :' . $key);
84
        }
85
        $query->params = $attributes;
86
87
        return self::finder($query, $single);
88
    }
89
90
    /**
91
     * Finder data in DB
92
     *
93
     * @access public
94
     *
95
     * @param IQuery $query query to search
96
     * @param boolean $single is single
97
     * @param IContainer $container
98
     *
99
     * @return mixed One or more data
100
     * @throws \Micro\base\Exception
101
     * @static
102
     */
103
    public static function finder(IQuery $query = null, $single = false, IContainer $container = null)
104
    {
105
        $query = ($query instanceof Query) ? $query : new Query($container->db);
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
106
        $query->table = static::tableName() . ' `m`';
107
        $query->objectName = get_called_class();
108
        $query->single = $single;
109
110
        return $query->run();
111
    }
112
113
    /**
114
     * Find by model attribute values
115
     *
116
     * @access public
117
     *
118
     * @param bool $single Is a single?
119
     *
120
     * @return mixed
121
     * @throws \Micro\base\Exception
122
     */
123
    public function find($single = false)
124
    {
125
        return self::findByAttributes(Type::getVars($this), $single, $this->container);
126
    }
127
128
    /**
129
     * Get attributes defined into model
130
     *
131
     * @access public
132
     *
133
     * @return array
134
     */
135
    public function getAttributes()
136
    {
137
        $fields = [];
138
        foreach ($this->container->db->listFields(static::tableName()) AS $field) {
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
139
            $fields[] = $field['field'];
140
        }
141
142
        return $fields;
143
    }
144
145
    /**
146
     * Get relation data or magic properties
147
     *
148
     * @access public
149
     *
150
     * @param string $name
151
     *
152
     * @return mixed
153
     */
154
    public function __get($name)
155
    {
156
        /** @var array $relation */
157
        if ($relation = $this->relations()->get($name)) {
158
            if (empty($this->cacheRelations[$name])) {
159
                $sql = new Query($this->container->db);
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
160
161
                $sql->addWhere('`m`.`' . $relation['On'][1] . '`=:' . $relation['On'][0]);
162
163
                if ($relation['Where']) {
164
                    $sql->addWhere($relation['Where']);
165
                }
166
                if ($relation['Params']) {
167
                    $sql->params = $relation['Params'];
168
                }
169
                if ($relation['Limit'] > 0) {
170
                    $sql->limit = $relation['Limit'];
171
                }
172
173
                $sql->params[$relation['On'][0]] = $this->{$relation['On'][0]};
174
175
                /** @noinspection PhpUndefinedMethodInspection */
176
                $this->cacheRelations[$name] = $relation['Model']::finder($sql, !$relation['IsMany'], $this->container);
177
            }
178
179
            return $this->cacheRelations[$name];
180
        } elseif (isset($this->$name)) {
181
            return $this->$name;
182
        }
183
184
        return false;
185
    }
186
187
    /**
188
     * @inheritdoc
189
     */
190
    public function relations()
191
    {
192
        return  new Relations;
193
    }
194
195
    /**
196
     * Save changes
197
     *
198
     * @access public
199
     *
200
     * @param bool $validate Validated data?
201
     *
202
     * @return boolean
203
     * @throws Exception
204
     */
205
    final public function save($validate = false)
206
    {
207
        if ($validate && !$this->validate()) {
208
            return false;
209
        }
210
211
        if ($this->isNewRecord()) {
212
            return $this->create();
213
        } else {
214
            if ($this->beforeSave() && $this->update()) {
215
                $this->afterSave();
216
217
                return true;
218
            }
219
        }
220
221
        return false;
222
    }
223
224
    /**
225
     * Is new record?
226
     *
227
     * @access public
228
     * @return boolean
229
     */
230
    public function isNewRecord()
231
    {
232
        return $this->_isNewRecord;
233
    }
234
235
    /**
236
     * Create changes
237
     *
238
     * @access public
239
     * @return boolean
240
     * @throws Exception
241
     */
242
    final public function create()
243
    {
244
        if (!$this->isNewRecord()) {
245
            return false;
246
        }
247
        if ($this->beforeCreate() && $this->beforeSave()) {
248
            $id = $this->container->db->insert(static::tableName(), $this->mergeAttributesDb());
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
249
            if (!$id) {
250
                return false;
251
            }
252
253
            $pKey = self::$primaryKey ?: 'id';
254
            if ($this->checkAttributeExists($pKey)) {
255
                $this->$pKey = $id;
256
            }
257
258
            $this->_isNewRecord = false;
259
260
            $this->afterCreate();
261
            $this->afterSave();
262
263
            return true;
264
        }
265
266
        return false;
267
    }
268
269
    /**
270
     * @inheritdoc
271
     */
272
    public function beforeCreate()
273
    {
274
        return true;
275
    }
276
277
    /**
278
     * @inheritdoc
279
     */
280
    public function beforeSave()
281
    {
282
        return true;
283
    }
284
285
    /**
286
     * Merge local attributes and db attributes
287
     *
288
     * @access protected
289
     *
290
     * @return array
291
     * @throws \Micro\base\Exception
292
     */
293
    protected function mergeAttributesDb()
294
    {
295
        $arr = Type::getVars($this);
296
297
        $buffer = [];
298
        foreach ($this->container->db->listFields(static::tableName()) AS $row) {
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
299
            $buffer[] = $row['field'];
300
        }
301
302
        foreach ($arr AS $key => $val) {
303
            if (!in_array($key, $buffer, true)) {
304
                unset($arr[$key]);
305
            }
306
        }
307
308
        unset($arr['isNewRecord']);
309
310
        return $arr;
311
    }
312
313
    /**
314
     * Check attribute exists into table
315
     *
316
     * @access public
317
     *
318
     * @param string $name Attribute name
319
     *
320
     * @return array
321
     */
322
    public function checkAttributeExists($name)
323
    {
324
        if (isset($this->$name)) {
325
            return true;
326
        }
327
328
        $res = false;
329
        foreach ($this->container->db->listFields(static::tableName()) AS $row) {
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
330
            if ($row['field'] === $name) {
331
                $res = true;
332
                break;
333
            }
334
        }
335
336
        return $res;
337
    }
338
339
    /**
340
     * @inheritdoc
341
     */
342
    public function afterCreate()
343
    {
344
    }
345
346
    /**
347
     * @inheritdoc
348
     */
349
    public function afterSave()
350
    {
351
    }
352
353
    /**
354
     * Update changes
355
     *
356
     * @access public
357
     *
358
     * @param string $where condition for search
359
     *
360
     * @throws Exception
361
     * @return boolean
362
     */
363
    final public function update($where = null)
364
    {
365
        if ($this->isNewRecord()) {
366
            return false;
367
        }
368
        if ($this->beforeUpdate()) {
369
            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...
370
                if (self::$primaryKey) {
371
                    $where .= '`' . self::$primaryKey . '` = :' . self::$primaryKey;
372
                } else {
373
                    throw new Exception ($this->container,
374
                        'In table ' . static::tableName() . ' option `id` not defined/not use.'
375
                    );
376
                }
377
            }
378
379
            if ($this->container->db->update(static::tableName(), $this->mergeAttributesDb(), $where)) {
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
380
                $this->afterUpdate();
381
382
                return true;
383
            }
384
        }
385
386
        return false;
387
    }
388
389
    /**
390
     * @inheritdoc
391
     */
392
    public function beforeUpdate()
393
    {
394
        return true;
395
    }
396
397
    /**
398
     * @inheritdoc
399
     */
400
    public function afterUpdate()
401
    {
402
    }
403
404
    /**
405
     * Delete changes
406
     *
407
     * @access public
408
     * @return boolean
409
     * @throws Exception
410
     */
411
    final public function delete()
412
    {
413
        if ($this->isNewRecord()) {
414
            return false;
415
        }
416
        if ($this->beforeDelete()) {
417
            if (!self::$primaryKey) {
418
                throw new Exception('In table ' . static::tableName() . ' option `id` not defined/not use.');
419
            }
420
421
            if (
422
            $this->container->db->delete(
0 ignored issues
show
Bug introduced by
Accessing db on the interface Micro\Base\IContainer suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
423
                static::tableName(),
424
                self::$primaryKey . '=:' . self::$primaryKey, [self::$primaryKey => $this->{self::$primaryKey}]
425
            )
426
            ) {
427
                $this->afterDelete();
428
                unset($this);
429
430
                return true;
431
            }
432
        }
433
434
        return false;
435
    }
436
437
    /**
438
     * @inheritdoc
439
     */
440
    public function beforeDelete()
441
    {
442
        return true;
443
    }
444
445
    /**
446
     * @inheritdoc
447
     */
448
    public function afterDelete()
449
    {
450
    }
451
}
452