Completed
Push — master ( 16fe6b...26679d )
by Andrii
02:32
created

ActiveRecord   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 0%

Importance

Changes 19
Bugs 5 Features 3
Metric Value
wmc 55
c 19
b 5
f 3
lcom 1
cbo 10
dl 0
loc 464
ccs 0
cts 215
cp 0
rs 6.8

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getDb() 0 4 1
A findAll() 0 4 1
A isScenarioDefault() 0 4 1
A primaryKey() 0 4 1
A primaryValue() 0 4 1
A find() 0 8 1
A findOne() 0 9 2
A get() 0 18 3
A getPrimaryValue() 0 14 3
A attributes() 0 4 1
A index() 0 5 1
A joinIndex() 0 4 1
A instantiate() 0 4 1
A type() 0 4 1
A modelName() 0 4 1
B insert() 0 28 5
A delete() 0 16 3
A update() 0 8 3
B updateInternal() 0 29 5
A perform() 0 7 2
D getScenarioCommand() 0 33 10
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
/*
4
 * Tools to use API as ActiveRecord for Yii2
5
 *
6
 * @link      https://github.com/hiqdev/yii2-hiart
7
 * @package   yii2-hiart
8
 * @license   BSD-3-Clause
9
 * @copyright Copyright (c) 2015-2016, HiQDev (http://hiqdev.com/)
10
 */
11
12
namespace hiqdev\hiart;
13
14
use yii\base\InvalidConfigException;
15
use yii\base\NotSupportedException;
16
use yii\db\ActiveQueryInterface;
17
use yii\db\BaseActiveRecord;
18
use yii\helpers\ArrayHelper;
19
use yii\helpers\Inflector;
20
use yii\helpers\StringHelper;
21
22
class ActiveRecord extends BaseActiveRecord
23
{
24
    /**
25
     * Returns the database connection used by this AR class.
26
     * By default, the "hiart" application component is used as the database connection.
27
     * You may override this method if you want to use a different database connection.
28
     *
29
     * @return Connection the database connection used by this AR class.
30
     */
31
    public static function getDb()
32
    {
33
        return \Yii::$app->get('hiart');
34
    }
35
36
    /**
37
     * {@inheritdoc}
38
     *
39
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
40
     */
41
    public static function find($options = [])
42
    {
43
        $config = [
44
            'class'   => ActiveQuery::className(),
45
            'options' => $options,
46
        ];
47
        return \Yii::createObject($config, [get_called_class()]);
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public static function findOne($condition, $options = [])
54
    {
55
        $query = static::find($options);
56
        if (is_array($condition)) {
57
            return $query->andWhere($condition)->one();
0 ignored issues
show
Bug Compatibility introduced by
The expression $query->andWhere($condition)->one(); of type hiqdev\hiart\ActiveRecord|array|null adds the type array to the return on line 57 which is incompatible with the return type declared by the interface yii\db\ActiveRecordInterface::findOne of type yii\db\ActiveRecordInterface|null.
Loading history...
58
        } else {
59
            return static::get($condition);
0 ignored issues
show
Bug Compatibility introduced by
The expression static::get($condition); of type null|hiqdev\hiart\ActiveRecord adds the type hiqdev\hiart\ActiveRecord to the return on line 59 which is incompatible with the return type of the parent method yii\db\BaseActiveRecord::findOne of type array|boolean.
Loading history...
60
        }
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public static function findAll($condition, $options = [])
67
    {
68
        return static::find($options)->andWhere($condition)->all();
69
    }
70
71
    public function isScenarioDefault()
72
    {
73
        return $this->scenario === static::SCENARIO_DEFAULT;
74
    }
75
76
    /**
77
     * Gets a record by its primary key.
78
     *
79
     * @param mixed $primaryKey the primaryKey value
80
     * @param array $options    options given in this parameter are passed to API.
81
     *
82
     * @return null|static The record instance or null if it was not found.
83
     */
84
    public static function get($primaryKey = null, $options = [])
85
    {
86
        if ($primaryKey === null) {
87
            return null;
88
        }
89
        $command = static::getDb()->createCommand();
90
        $result  = $command->get(static::type(), $primaryKey, $options);
91
92
        if ($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
93
            $model = static::instantiate($result);
94
            static::populateRecord($model, $result);
95
            $model->afterFind();
96
97
            return $model;
98
        }
99
100
        return null;
101
    }
102
103
    /**
104
     * This method defines the attribute that uniquely identifies a record.
105
     *
106
     * The primaryKey for HiArt objects is the `id` field by default. This field is not part of the
107
     * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]].
108
     *
109
     * You may override this method to define the primary key name.
110
     *
111
     * Note that HiArt only supports _one_ attribute to be the primary key. However to match the signature
112
     * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
113
     * single string.
114
     *
115
     * @return string[] array of primary key attributes. Only the first element of the array will be used.
116
     */
117
    public static function primaryKey()
118
    {
119
        return ['id'];
120
    }
121
122
    /**
123
     * +     * The name of the main attribute
124
     * +     *
125
     * Examples:.
126
     *
127
     * This will directly reference to the attribute 'name'
128
     * ```
129
     *     return 'name';
130
     * ```
131
     *
132
     * This will concatenate listed attributes, separated with `delimiter` value.
133
     * If delimiter is not set, space is used by default.
134
     * ```
135
     *     return ['seller', 'client', 'delimiter' => '/'];
136
     * ```
137
     *
138
     * The callable method, that will get [[$model]] and should return value of name attribute
139
     * ```
140
     *     return function ($model) {
141
     *        return $model->someField ? $model->name : $model->otherName;
142
     *     };
143
     * ```
144
     *
145
     * @throws InvalidConfigException
146
     *
147
     * @return string|callable|array
148
     *
149
     * @author SilverFire
150
     */
151
    public function primaryValue()
152
    {
153
        return static::formName();
154
    }
155
156
    /**
157
     * Returns the value of the primary attribute.
158
     *
159
     * @throws InvalidConfigException
160
     *
161
     * @return mixed|null
162
     *
163
     * @see primaryValue()
164
     */
165
    public function getPrimaryValue()
166
    {
167
        $primaryValue = $this->primaryValue();
168
169
        if ($primaryValue instanceof \Closure) {
170
            return call_user_func($primaryValue, [$this]);
171
        } elseif (is_array($primaryValue)) {
172
            $delimiter = ArrayHelper::remove($primaryValue, 'delimiter', ' ');
173
174
            return implode($delimiter, $this->getAttributes($primaryValue));
175
        } else {
176
            return $this->getAttribute($primaryValue);
177
        }
178
    }
179
180
    /**
181
     * Returns the list of all attribute names of the model.
182
     *
183
     * This method must be overridden by child classes to define available attributes.
184
     *
185
     * Attributes are names of fields of the corresponding API object.
186
     * The primaryKey for HiArt documents is the `id` field by default which is not part of the attributes.
187
     *
188
     * @throws \yii\base\InvalidConfigException if not overridden in a child class.
189
     *
190
     * @return string[] list of attribute names.
191
     */
192
    public function attributes()
193
    {
194
        throw new InvalidConfigException('The attributes() method of HiArt ActiveRecord has to be implemented by child classes.');
195
    }
196
197
    /**
198
     * @return string the name of the index this record is stored in.
199
     */
200
    public static function index()
201
    {
202
        //        return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
203
        return mb_strtolower(StringHelper::basename(get_called_class()) . 's');
204
    }
205
206
    public static function joinIndex()
207
    {
208
        return static::index();
209
    }
210
211
    /**
212
     * Creates an active record instance.
213
     *
214
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
215
     * It is not meant to be used for creating new records directly.
216
     *
217
     * You may override this method if the instance being created
218
     * depends on the row data to be populated into the record.
219
     * For example, by creating a record based on the value of a column,
220
     * you may implement the so-called single-table inheritance mapping.
221
     *
222
     * @param array $row row data to be populated into the record.
223
     *                   This array consists of the following keys:
224
     *                   - `_source`: refers to the attributes of the record.
225
     *                   - `_type`: the type this record is stored in.
226
     *                   - `_index`: the index this record is stored in.
227
     *
228
     * @return static the newly created active record
229
     */
230
    public static function instantiate($row)
231
    {
232
        return new static();
233
    }
234
235
    /**
236
     * @return string the name of the type of this record.
237
     */
238
    public static function type()
239
    {
240
        return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
241
    }
242
243
    /**
244
     * Declares the name of the model associated with this class.
245
     * By default this method returns the class name by calling [[Inflector::camel2id()]].
246
     *
247
     * @return string the module name
248
     */
249
    public static function modelName()
250
    {
251
        return Inflector::camel2id(StringHelper::basename(get_called_class()));
252
    }
253
254
    public function insert($runValidation = true, $attributes = null, $options = [])
255
    {
256
        if ($runValidation && !$this->validate($attributes)) {
257
            return false;
258
        }
259
260
        if (!$this->beforeSave(true)) {
261
            return false;
262
        }
263
264
        $values = $this->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 254 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...
265
266
        $command = $this->getScenarioCommand('create');
267
        $data    = array_merge($values, $options, ['id' => $this->getOldPrimaryKey()]);
268
269
        $result = static::getDb()->createCommand()->perform($command, $data);
270
271
        $pk        = static::primaryKey()[0];
272
        $this->$pk = $result['id'];
273
        if ($pk !== 'id') {
274
            $values[$pk] = $result['id'];
275
        }
276
        $changedAttributes = array_fill_keys(array_keys($values), null);
277
        $this->setOldAttributes($values);
278
        $this->afterSave(true, $changedAttributes);
279
280
        return true;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function delete($options = [])
287
    {
288
        if (!$this->beforeDelete()) {
289
            return false;
290
        }
291
292
        $command = $this->getScenarioCommand('delete');
293
        $data    = array_merge($options, ['id' => $this->getOldPrimaryKey()]);
294
295
        $result = static::getDb()->createCommand()->perform($command, $data);
296
297
        $this->setOldAttributes(null);
298
        $this->afterDelete();
299
300
        return $result === false ? false : true;
301
    }
302
303
    public function update($runValidation = true, $attributeNames = null, $options = [])
304
    {
305
        if ($runValidation && !$this->validate($attributeNames)) {
306
            return false;
307
        }
308
309
        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 309 which is incompatible with the return type of the parent method yii\db\BaseActiveRecord::update of type false|integer.
Loading history...
310
    }
311
312
    protected function updateInternal($attributes = null, $options = [])
313
    {
314
        if (!$this->beforeSave(false)) {
315
            return false;
316
        }
317
318
        $values = $this->getAttributes($attributes);
319
//        $values = $this->attributes;
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
320
321
        if (empty($values)) {
322
            $this->afterSave(false, $values);
323
324
            return 0;
325
        }
326
327
        $command = $this->getScenarioCommand('update');
328
        $data    = array_merge($values, $options, ['id' => $this->getOldPrimaryKey()]);
329
330
        $result            = static::getDb()->createCommand()->perform($command, $data);
331
        $changedAttributes = [];
332
        foreach ($values as $name => $value) {
333
            $changedAttributes[$name] = $this->getOldAttribute($name);
334
            $this->setOldAttribute($name, $value);
335
        }
336
337
        $this->afterSave(false, $changedAttributes);
338
339
        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...
340
    }
341
342
    /**
343
     * Custom method for HiArt.
344
     *
345
     * @param $action
346
     * @param array $options
347
     * @param bool  $bulk
348
     *
349
     * @return array
350
     */
351
    public static function perform($action, $options = [], $bulk = false)
352
    {
353
        $action = ($bulk === true ? static::index() : static::type()) . $action;
354
        $result = static::getDb()->createCommand()->perform($action, $options);
355
356
        return $result;
357
    }
358
359
    /**
360
     * Creates the command name for the specified scenario name.
361
     *
362
     * @param string $default
363
     * @param bool   $bulk
364
     *
365
     * @throws InvalidConfigException
366
     * @throws NotSupportedException
367
     *
368
     * @return string
369
     */
370
    public function getScenarioCommand($default = '', $bulk = false)
371
    {
372
        if ($this->isScenarioDefault()) {
373
            if ($default !== '') {
374
                $result = Inflector::id2camel($default);
375
            } else {
376
                throw new InvalidConfigException('Scenario not specified');
377
            }
378
        } else {
379
            $scenarioCommands = static::scenarioCommands($bulk);
0 ignored issues
show
Unused Code introduced by
The call to ActiveRecord::scenarioCommands() has too many arguments starting with $bulk.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
380
            if ($command = $scenarioCommands[$this->scenario]) {
381
                if ($command === false) {
382
                    throw new NotSupportedException('The scenario can not be saved');
383
                }
384
385
                if (is_array($command) && $command[0] === null) {
386
                    $result = $command[1];
387
                } elseif (is_array($command)) {
388
                    $result = $command;
389
                } else {
390
                    $result = Inflector::id2camel($command);
391
                }
392
            } else {
393
                $result = Inflector::id2camel($this->scenario);
394
            }
395
        }
396
397
        if (is_array($result)) {
398
            return implode('', $result);
399
        } else {
400
            return static::type() . ($bulk ? 's' : '') . $result;
401
        }
402
    }
403
404
    /**
405
     * Define an array of relations between scenario and API call action.
406
     *
407
     * Example:
408
     *
409
     * ```
410
     * [
411
     *      'update-name'                => 'set-name', /// ModuleSetName
412
     *      'update-related-name'        => [Action::formName(), 'SetName'], /// ActionSetName
413
     *      'update-self-case-sensetive' => [null, 'SomeSENSETIVE'] /// ModuleSomeSENSETIVE
414
     * ]
415
     * ~~
416
     *
417
     *  key string name of scenario
418
     *  value string|array
419
     *              string will be passed to [[Inflector::id2camel|id2camel]] inflator
420
     *              array - first attribute a module name, second - value
421
     *
422
     * Tricks: pass null as first argument of array to leave command's case unchanged (no inflator calling)
423
     *
424
     * @return array
425
     */
426
    public function scenarioCommands()
427
    {
428
        return [];
429
    }
430
431
    /**
432
     * @return bool
433
     */
434
    public function getIsNewRecord()
435
    {
436
        return !$this->getPrimaryKey();
437
    }
438
439
    /**
440
     * This method has no effect in HiArt ActiveRecord.
441
     */
442
    public function optimisticLock()
443
    {
444
        return null;
445
    }
446
447
    /**
448
     * Destroys the relationship in current model.
449
     *
450
     * This method is not supported by HiArt.
451
     */
452
    public function unlinkAll($name, $delete = false)
453
    {
454
        throw new NotSupportedException('unlinkAll() is not supported by HiArt, use unlink() instead.');
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     *
460
     * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
461
     *                                          and `$throwException` is false, null will be returned.
462
     */
463
    public function getRelation($name, $throwException = true)
464
    {
465
        return parent::getRelation($name, $throwException);
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     * @return ActiveQuery the relational query object.
471
     */
472
    public function hasOne($class, $link)
473
    {
474
        return parent::hasOne($class, $link);
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     * @return ActiveQuery the relational query object.
480
     */
481
    public function hasMany($class, $link)
482
    {
483
        return parent::hasMany($class, $link);
484
    }
485
}
486