Completed
Push — master ( 1f652c...46bb95 )
by Andrii
15:10
created

ActiveRecord   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 348
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 16.13%

Importance

Changes 6
Bugs 1 Features 1
Metric Value
wmc 49
lcom 1
cbo 7
dl 0
loc 348
ccs 20
cts 124
cp 0.1613
rs 8.5454
c 6
b 1
f 1

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getDb() 0 4 1
A find() 0 6 1
A isScenarioDefault() 0 4 1
A get() 0 18 3
A primaryKey() 0 4 1
B attributes() 0 18 5
A instantiate() 0 4 1
A from() 0 4 1
A modelName() 0 4 1
B insert() 0 25 5
A delete() 0 14 3
A update() 0 8 3
B updateInternal() 0 25 5
A performScenario() 0 6 1
A perform() 0 4 1
D getScenarioAction() 0 29 9
A scenarioCommands() 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
 * Tools to use API as ActiveRecord for Yii2
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\ArrayHelper;
18
use yii\helpers\Inflector;
19
use yii\helpers\StringHelper;
20
21
class ActiveRecord extends BaseActiveRecord
22
{
23
    /**
24
     * Returns the database connection used by this AR class.
25
     * By default, the "hiart" application component is used as the database connection.
26
     * You may override this method if you want to use a different database connection.
27
     *
28
     * @return AbstractConnection the database connection used by this AR class
29
     */
30
    public static function getDb()
31
    {
32
        return AbstractConnection::getDb();
33
    }
34
35
    /**
36
     * {@inheritdoc}
37
     * @return ActiveQuery the newly created [[ActiveQuery]] instance
38
     */
39 2
    public static function find()
40
    {
41 2
        $class = static::getDb()->activeQueryClass;
42
43 2
        return new $class(get_called_class());
44
    }
45
46
    public function isScenarioDefault()
47
    {
48
        return $this->scenario === static::SCENARIO_DEFAULT;
49
    }
50
51
    /**
52
     * Gets a record by its primary key.
53
     *
54
     * @param mixed $primaryKey the primaryKey value
55
     * @param array $options    options given in this parameter are passed to API
56
     *
57
     * @return null|static the record instance or null if it was not found
58
     */
59
    public static function get($primaryKey = null, $options = [])
60
    {
61
        if ($primaryKey === null) {
62
            return null;
63
        }
64
        $command = static::getDb()->createCommand();
65
        $result  = $command->get(static::from(), $primaryKey, $options);
0 ignored issues
show
Documentation Bug introduced by
The method get does not exist on object<hiqdev\hiart\Command>? 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...
66
67
        if ($result) {
68
            $model = static::instantiate($result);
69
            static::populateRecord($model, $result);
70
            $model->afterFind();
71
72
            return $model;
73
        }
74
75
        return null;
76
    }
77
78
    /**
79
     * This method defines the attribute that uniquely identifies a record.
80
     *
81
     * The primaryKey for HiArt objects is the `id` field by default. This field is not part of the
82
     * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]].
83
     *
84
     * You may override this method to define the primary key name.
85
     *
86
     * Note that HiArt only supports _one_ attribute to be the primary key. However to match the signature
87
     * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
88
     * single string.
89
     *
90
     * @return string[] array of primary key attributes. Only the first element of the array will be used.
91
     */
92
    public static function primaryKey()
93
    {
94
        return ['id'];
95
    }
96
97
    /**
98
     * Returns the list of attribute names.
99
     * By default, this method returns all attributes mentioned in rules.
100
     * You may override this method to change the default behavior.
101
     * @return string[] list of attribute names
102
     */
103
    public function attributes()
104
    {
105
        $attributes = [];
106
        foreach ($this->rules() as $rule) {
107
            $names = reset($rule);
108
            if (is_string($names)) {
109
                $names = [$names];
110
            }
111
            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...
112
                if (substr_compare($name, '!', 0, 1) === 0) {
113
                    $name = mb_substr($name, 1);
114
                }
115
                $attributes[$name] = $name;
116
            }
117
        }
118
119
        return array_values($attributes);
120
    }
121
122
    /**
123
     * Creates an active record instance.
124
     *
125
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
126
     * It is not meant to be used for creating new records directly.
127
     *
128
     * You may override this method if the instance being created
129
     * depends on the row data to be populated into the record.
130
     * For example, by creating a record based on the value of a column,
131
     * you may implement the so-called single-table inheritance mapping.
132
     *
133
     * @return static the newly created active record
134
     */
135
    public static function instantiate($row)
136
    {
137
        return new static();
138
    }
139
140
    /**
141
     * @return string the name of the entity of this record
142
     */
143
    public static function from()
144
    {
145
        return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
146
    }
147
148
    /**
149
     * Declares the name of the model associated with this class.
150
     * By default this method returns the class name by calling [[Inflector::camel2id()]].
151
     *
152
     * @return string the module name
153
     */
154
    public static function modelName()
155
    {
156
        return Inflector::camel2id(StringHelper::basename(get_called_class()));
157
    }
158
159
    public function insert($runValidation = true, $attributes = null, $options = [])
160
    {
161 2
        if ($runValidation && !$this->validate($attributes)) {
162
            return false;
163 2
        }
164 2
165 2
        if (!$this->beforeSave(true)) {
166 2
            return false;
167 2
        }
168 2
169 2
        $values = $this->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 159 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...
170 2
        $data   = array_merge($values, $options, ['id' => $this->getOldPrimaryKey()]);
171
        $result = $this->performScenario('insert', $data);
172
173 2
        $pk        = static::primaryKey()[0];
174 2
        $this->$pk = $result['id'];
175 2
        if ($pk !== 'id') {
176
            $values[$pk] = $result['id'];
177 2
        }
178
        $changedAttributes = array_fill_keys(array_keys($values), null);
179
        $this->setOldAttributes($values);
180
        $this->afterSave(true, $changedAttributes);
181
182
        return true;
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function delete($options = [])
189
    {
190
        if (!$this->beforeDelete()) {
191
            return false;
192
        }
193
194
        $data   = array_merge($options, ['id' => $this->getOldPrimaryKey()]);
195
        $result = $this->performScenario('delete', $data);
196
197
        $this->setOldAttributes(null);
198
        $this->afterDelete();
199
200
        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...
201
    }
202
203
    public function update($runValidation = true, $attributeNames = null, $options = [])
204
    {
205
        if ($runValidation && !$this->validate($attributeNames)) {
206
            return false;
207 2
        }
208
209 2
        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 209 which is incompatible with the return type of the parent method yii\db\BaseActiveRecord::update of type false|integer.
Loading history...
210
    }
211
212
    protected function updateInternal($attributes = null, $options = [])
213
    {
214
        if (!$this->beforeSave(false)) {
215 2
            return false;
216
        }
217 2
218
        $values = $this->getAttributes($attributes);
219
        if (empty($values)) {
220
            $this->afterSave(false, $values);
221
222
            return 0;
223
        }
224
225
        $result = $this->performScenario('update', $values, $options);
226
227
        $changedAttributes = [];
228
        foreach ($values as $name => $value) {
229
            $changedAttributes[$name] = $this->getOldAttribute($name);
230
            $this->setOldAttribute($name, $value);
231
        }
232
233
        $this->afterSave(false, $changedAttributes);
234
235
        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...
236
    }
237
238
    public function performScenario($defaultScenario, $data, array $options = [])
239
    {
240
        $action = $this->getScenarioAction($defaultScenario);
241
242
        return static::perform($action, $data, $options);
243
    }
244
245
    public static function perform($action, $data, array $options = [])
246
    {
247
        return static::getDb()->createCommand()->perform($action, static::from(), $data, $options);
248
    }
249
250
    /**
251
     * Converts scenario name to action.
252
     * @param string $default default action name
253
     * @throws InvalidConfigException
254
     * @throws NotSupportedException
255
     * @return string
256
     */
257
    public function getScenarioAction($default = '')
258
    {
259
        if ($this->isScenarioDefault()) {
260
            if ($default !== '') {
261
                $result = Inflector::id2camel($default);
262
            } else {
263
                throw new InvalidConfigException('Scenario not specified');
264
            }
265
        } else {
266
            $scenarioCommands = static::scenarioCommands();
267
            if ($action = $scenarioCommands[$this->scenario]) {
268
                if ($action === false) {
269
                    throw new NotSupportedException('The scenario can not be saved');
270
                }
271
272
                if (is_array($action) && $action[0] === null) {
273
                    $result = $action[1];
274
                } elseif (is_array($action)) {
275
                    $result = $action;
276
                } else {
277
                    $result = Inflector::id2camel($action);
278
                }
279
            } else {
280
                $result = Inflector::id2camel($this->scenario);
281
            }
282
        }
283
284
        return is_array($result) ? implode('', $result) : $result;
285
    }
286
287
    /**
288
     * Define an array of relations between scenario and API call action.
289
     *
290
     * Example:
291
     *
292
     * ```
293
     * [
294
     *      'update-name'                => 'set-name', /// ModuleSetName
295
     *      'update-related-name'        => [Action::formName(), 'SetName'], /// ActionSetName
296
     *      'update-self-case-sensetive' => [null, 'SomeSENSETIVE'] /// ModuleSomeSENSETIVE
297
     * ]
298
     * ~~
299
     *
300
     *  key string name of scenario
301
     *  value string|array
302
     *              string will be passed to [[Inflector::id2camel|id2camel]] inflator
303
     *              array - first attribute a module name, second - value
304
     *
305
     * Tricks: pass null as first argument of array to leave command's case unchanged (no inflator calling)
306
     *
307
     * @return array
308
     */
309
    public function scenarioCommands()
310
    {
311
        return [];
312
    }
313
314
    /**
315
     * @return bool
316
     */
317
    public function getIsNewRecord()
318
    {
319
        return !$this->getPrimaryKey();
320
    }
321
322
    /**
323
     * This method has no effect in HiArt ActiveRecord.
324
     */
325
    public function optimisticLock()
326
    {
327
        return null;
328
    }
329
330
    /**
331
     * Destroys the relationship in current model.
332
     *
333
     * This method is not supported by HiArt.
334
     */
335
    public function unlinkAll($name, $delete = false)
336
    {
337
        throw new NotSupportedException('unlinkAll() is not supported by HiArt, use unlink() instead.');
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     *
343
     * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
344
     *                                          and `$throwException` is false, null will be returned.
345
     */
346
    public function getRelation($name, $throwException = true)
347
    {
348
        return parent::getRelation($name, $throwException);
349
    }
350
351
    /**
352
     * {@inheritdoc}
353
     * @return ActiveQuery the relational query object
354
     */
355
    public function hasOne($class, $link)
356
    {
357
        return parent::hasOne($class, $link);
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     * @return ActiveQuery the relational query object
363
     */
364
    public function hasMany($class, $link)
365
    {
366
        return parent::hasMany($class, $link);
367
    }
368
}
369