Completed
Pull Request — master (#12)
by Andrii
02:03
created

ActiveRecord   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 15.74%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 46
c 1
b 1
f 0
lcom 1
cbo 8
dl 0
loc 331
ccs 17
cts 108
cp 0.1574
rs 8.3999

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getDb() 0 4 1
A find() 0 6 1
A isScenarioDefault() 0 4 1
A primaryKey() 0 4 1
A tableName() 0 4 1
A modelName() 0 4 1
A delete() 0 14 3
A update() 0 8 3
A instantiate() 0 4 1
B attributes() 0 18 5
B insert() 0 25 5
B updateInternal() 0 25 5
A batchQuery() 0 19 4
A query() 0 6 1
A batchPerform() 0 6 1
A perform() 0 4 1
A getScenarioAction() 0 14 4
A scenarioActions() 0 4 1
A getIsNewRecord() 0 4 1
A optimisticLock() 0 4 1
A unlinkAll() 0 4 1
A getRelation() 0 4 1
A hasOne() 0 4 1
A hasMany() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ActiveRecord 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 ActiveRecord, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ActiveRecord for API
4
 *
5
 * @link      https://github.com/hiqdev/yii2-hiart
6
 * @package   yii2-hiart
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2015-2017, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\hiart;
12
13
use yii\base\InvalidConfigException;
14
use yii\base\NotSupportedException;
15
use yii\db\ActiveQueryInterface;
16
use yii\db\BaseActiveRecord;
17
use yii\helpers\Inflector;
18
use yii\helpers\StringHelper;
19
20
class ActiveRecord extends BaseActiveRecord
21
{
22
    /**
23
     * Returns the database connection used by this AR class.
24
     * By default, the "hiart" application component is used as the database connection.
25
     * You may override this method if you want to use a different database connection.
26
     *
27
     * @return AbstractConnection the database connection used by this AR class
28
     */
29
    public static function getDb()
30
    {
31
        return AbstractConnection::getDb();
32
    }
33
34
    /**
35
     * {@inheritdoc}
36
     * @return ActiveQuery the newly created [[ActiveQuery]] instance
37
     */
38 2
    public static function find()
39
    {
40 2
        $class = static::getDb()->activeQueryClass;
41
42 2
        return new $class(get_called_class());
43
    }
44
45
    public function isScenarioDefault()
46
    {
47
        return $this->scenario === static::SCENARIO_DEFAULT;
48
    }
49
50
    /**
51
     * This method defines the attribute that uniquely identifies a record.
52
     *
53
     * The primaryKey for HiArt objects is the `id` field by default. This field is not part of the
54
     * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]].
55
     *
56
     * You may override this method to define the primary key name.
57
     *
58
     * Note that HiArt only supports _one_ attribute to be the primary key. However to match the signature
59
     * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
60
     * single string.
61
     *
62
     * @return string[] array of primary key attributes. Only the first element of the array will be used.
63
     */
64
    public static function primaryKey()
65
    {
66
        return ['id'];
67
    }
68
69
    /**
70
     * Returns the list of attribute names.
71
     * By default, this method returns all attributes mentioned in rules.
72
     * You may override this method to change the default behavior.
73
     * @return string[] list of attribute names
74
     */
75 2
    public function attributes()
76
    {
77 2
        $attributes = [];
78 2
        foreach ($this->rules() as $rule) {
79 2
            $names = reset($rule);
80 2
            if (is_string($names)) {
81 2
                $names = [$names];
82
            }
83 2
            foreach ($names as $name) {
0 ignored issues
show
Bug introduced by
The expression $names of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
84 2
                if (substr_compare($name, '!', 0, 1) === 0) {
85
                    $name = mb_substr($name, 1);
86
                }
87 2
                $attributes[$name] = $name;
88
            }
89
        }
90
91 2
        return array_values($attributes);
92
    }
93
94
    /**
95
     * Creates an active record instance.
96
     *
97
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
98
     * It is not meant to be used for creating new records directly.
99
     *
100
     * You may override this method if the instance being created
101
     * depends on the row data to be populated into the record.
102
     * For example, by creating a record based on the value of a column,
103
     * you may implement the so-called single-table inheritance mapping.
104
     *
105
     * @return static the newly created active record
106
     */
107 2
    public static function instantiate($row)
108
    {
109 2
        return new static();
110
    }
111
112
    /**
113
     * @return string the name of the entity of this record
114
     */
115 2
    public static function tableName()
116
    {
117 2
        return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
118
    }
119
120
    /**
121
     * Declares the name of the model associated with this class.
122
     * By default this method returns the class name by calling [[Inflector::camel2id()]].
123
     *
124
     * @return string the module name
125
     */
126
    public static function modelName()
127
    {
128
        return Inflector::camel2id(StringHelper::basename(get_called_class()));
129
    }
130
131
    public function insert($runValidation = true, $attributes = null, $options = [])
132
    {
133
        if ($runValidation && !$this->validate($attributes)) {
134
            return false;
135
        }
136
137
        if (!$this->beforeSave(true)) {
138
            return false;
139
        }
140
141
        $values = $this->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 131 can also be of type array; however, yii\db\BaseActiveRecord::getDirtyAttributes() does only seem to accept array<integer,string>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
142
        $data   = array_merge($values, $options, ['id' => $this->getOldPrimaryKey()]);
143
        $result = $this->query('insert', $data);
144
145
        $pk        = static::primaryKey()[0];
146
        $this->$pk = $result['id'];
147
        if ($pk !== 'id') {
148
            $values[$pk] = $result['id'];
149
        }
150
        $changedAttributes = array_fill_keys(array_keys($values), null);
151
        $this->setOldAttributes($values);
152
        $this->afterSave(true, $changedAttributes);
153
154
        return true;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function delete($options = [])
161
    {
162
        if (!$this->beforeDelete()) {
163
            return false;
164
        }
165
166
        $data   = array_merge($options, ['id' => $this->getOldPrimaryKey()]);
167
        $result = $this->query('delete', $data);
168
169
        $this->setOldAttributes(null);
170
        $this->afterDelete();
171
172
        return $result === false ? false : true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result === false ? false : true; (boolean) is incompatible with the return type of the parent method yii\db\BaseActiveRecord::delete of type integer|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
173
    }
174
175
    public function update($runValidation = true, $attributeNames = null, $options = [])
176
    {
177
        if ($runValidation && !$this->validate($attributeNames)) {
178
            return false;
179
        }
180
181
        return $this->updateInternal($attributeNames, $options);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->updateInternal($attributeNames, $options); of type integer|boolean adds the type boolean to the return on line 181 which is incompatible with the return type of the parent method yii\db\BaseActiveRecord::update of type false|integer.
Loading history...
182
    }
183
184
    protected function updateInternal($attributes = null, $options = [])
185
    {
186
        if (!$this->beforeSave(false)) {
187
            return false;
188
        }
189
190
        $values = $this->getAttributes($attributes);
191
        if (empty($values)) {
192
            $this->afterSave(false, $values);
193
194
            return 0;
195
        }
196
197
        $result = $this->query('update', $values, $options);
198
199
        $changedAttributes = [];
200
        foreach ($values as $name => $value) {
201
            $changedAttributes[$name] = $this->getOldAttribute($name);
202
            $this->setOldAttribute($name, $value);
203
        }
204
205
        $this->afterSave(false, $changedAttributes);
206
207
        return $result === false ? false : true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result === false ? false : true; (boolean) is incompatible with the return type of the parent method yii\db\BaseActiveRecord::updateInternal of type false|integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
208
    }
209
210
    /**
211
     * Perform batch query.
212
     * Attention: takes bulk data and returns bulk result.
213
     * @param string $defaultScenario
214
     * @param array $data bulk data
215
     * @param array $options
216
     * @return array bulk results
217
     */
218
    public function batchQuery($defaultScenario, $data = [], array $options = [])
219
    {
220
        $batch = isset($options['batch']) ? (bool)$options['batch'] : true;
221
        $options['batch'] = $batch;
222
223
        if (!$batch) {
224
            $val = reset($data);
225
            $key = key($data);
0 ignored issues
show
Unused Code introduced by
$key is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
226
            $data = $val;
227
        }
228
229
        $result = $this->query($defaultScenario, $data, $options);
230
231
        if (!$batch) {
232
            $result = [$key = $result];
233
        }
234
235
        return $result;
236
    }
237
238
    /**
239
     * Perform query.
240
     * @param string $defaultScenario
241
     * @param array $data data
242
     * @param array $options
243
     * @return array result
244
     */
245
    public function query($defaultScenario, $data = [], array $options = [])
246
    {
247
        $action = $this->getScenarioAction($defaultScenario);
248
249
        return static::perform($action, $data, $options);
250
    }
251
252
    public static function batchPerform($action, $data = [], array $options = [])
253
    {
254
        $options['batch'] = true;
255
256
        return static::perform($action, $data, $options);
257
    }
258
259
    public static function perform($action, $data = [], array $options = [])
260
    {
261
        return static::getDb()->createCommand()->perform($action, static::tableName(), $data, $options)->getData();
262
    }
263
264
    /**
265
     * Converts scenario name to action.
266
     * @param string $default default action name
267
     * @throws InvalidConfigException
268
     * @throws NotSupportedException
269
     * @return string
270
     */
271
    public function getScenarioAction($default = '')
272
    {
273
        if ($this->isScenarioDefault()) {
274
            if (empty($default)) {
275
                throw new InvalidConfigException('Scenario not specified');
276
            }
277
278
            return $default;
279
        } else {
280
            $actions = static::scenarioActions();
281
282
            return isset($actions[$this->scenario]) ? $actions[$this->scenario] : $this->scenario;
283
        }
284
    }
285
286
    /**
287
     * Provides a correspondance array: scenario -> API action.
288
     * E.g. ['update-name' => 'set-name'].
289
     * @return array
290
     */
291
    public function scenarioActions()
292
    {
293
        return [];
294
    }
295
296
    /**
297
     * @return bool
298
     */
299
    public function getIsNewRecord()
300
    {
301
        return !$this->getPrimaryKey();
302
    }
303
304
    /**
305
     * This method has no effect in HiArt ActiveRecord.
306
     */
307
    public function optimisticLock()
308
    {
309
        return null;
310
    }
311
312
    /**
313
     * Destroys the relationship in current model.
314
     *
315
     * This method is not supported by HiArt.
316
     */
317
    public function unlinkAll($name, $delete = false)
318
    {
319
        throw new NotSupportedException('unlinkAll() is not supported by HiArt, use unlink() instead.');
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     *
325
     * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
326
     *                                          and `$throwException` is false, null will be returned.
327
     */
328
    public function getRelation($name, $throwException = true)
329
    {
330
        return parent::getRelation($name, $throwException);
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     * @return ActiveQuery the relational query object
336
     */
337
    public function hasOne($class, $link)
338
    {
339
        return parent::hasOne($class, $link);
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     * @return ActiveQuery the relational query object
345
     */
346
    public function hasMany($class, $link)
347
    {
348
        return parent::hasMany($class, $link);
349
    }
350
}
351