Completed
Push — master ( 66cefc...1126a8 )
by Andrii
02:48
created

ActiveQuery   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 376
Duplicated Lines 0 %

Test Coverage

Coverage 27.54%

Importance

Changes 0
Metric Value
dl 0
loc 376
ccs 46
cts 167
cp 0.2754
rs 3.2
c 0
b 0
f 0
wmc 65

16 Methods

Rating   Name   Duplication   Size   Complexity  
A joinWithRelation() 0 5 3
A joinWith() 0 5 1
A __construct() 0 5 1
A init() 0 4 1
A select() 0 5 1
B joinWithRelations() 0 20 6
B createCommand() 0 33 7
A prepare() 0 12 2
A addSelect() 0 13 3
B buildJoinWith() 0 34 10
C populateJoinedRelations() 0 54 14
A addInverseRelation() 0 8 3
A populate() 0 17 4
A all() 0 9 2
A one() 0 13 3
A createModels() 0 22 4

How to fix   Complexity   

Complex Class

Complex classes like ActiveQuery 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.

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 ActiveQuery, 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-2018, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\hiart;
12
13
use hiqdev\hiart\rest\QueryBuilder;
14
use yii\db\ActiveQueryInterface;
15
use yii\db\ActiveQueryTrait;
16
use yii\db\ActiveRelationTrait;
17
18
class ActiveQuery extends Query implements ActiveQueryInterface
19
{
20
    use ActiveQueryTrait;
21
    use ActiveRelationTrait;
22
23
    /**
24
     * @event Event an event that is triggered when the query is initialized via [[init()]].
25
     */
26
    const EVENT_INIT = 'init';
27
28
    /**
29
     * @var array|null a list of relations that this query should be joined with
30
     */
31
    public $joinWith = [];
32
33
    /**
34
     * Constructor.
35
     * @param string $modelClass the model class associated with this query
36
     * @param array $config configurations to be applied to the newly created query object
37
     */
38 2
    public function __construct($modelClass, $config = [])
39
    {
40 2
        $this->modelClass = $modelClass;
41
42 2
        parent::__construct($config);
43 2
    }
44
45
    /**
46
     * Initializes the object.
47
     * This method is called at the end of the constructor. The default implementation will trigger
48
     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
49
     * to ensure triggering of the event.
50
     */
51 2
    public function init()
52
    {
53 2
        parent::init();
54 2
        $this->trigger(self::EVENT_INIT);
55 2
    }
56
57
    /**
58
     * Creates a DB command that can be used to execute this query.
59
     * @param AbstractConnection $db the DB connection used to create the DB command.
60
     * If null, the DB connection returned by [[modelClass]] will be used.
61
     * @return Command the created DB command instance
62
     */
63 2
    public function createCommand($db = null)
64
    {
65 2
        if ($this->primaryModel !== null) {
66
            // lazy loading
67
            if (is_array($this->via)) {
68
                // via relation
69
                /** @var $viaQuery ActiveQuery */
70
                list($viaName, $viaQuery) = $this->via;
71
                if ($viaQuery->multiple) {
72
                    $viaModels = $viaQuery->all();
73
                    $this->primaryModel->populateRelation($viaName, $viaModels);
74
                } else {
75
                    $model = $viaQuery->one();
76
                    $this->primaryModel->populateRelation($viaName, $model);
77
                    $viaModels = $model === null ? [] : [$model];
78
                }
79
                $this->filterByModels($viaModels);
80
            } else {
81
                $this->filterByModels([$this->primaryModel]);
82
            }
83
        }
84
85
        /* @var $modelClass ActiveRecord */
86 2
        $modelClass = $this->modelClass;
87
88 2
        if ($db === null) {
89 2
            $db = $modelClass::getDb();
90
        }
91 2
        if ($this->from === null) {
92 2
            $this->from = $modelClass::tableName();
0 ignored issues
show
Documentation Bug introduced by
It seems like $modelClass::tableName() of type string is incompatible with the declared type array of property $from.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
93
        }
94
95 2
        return parent::createCommand($db);
96
    }
97
98
    /**
99
     * Prepares query for use. See NOTE.
100
     * @param QueryBuilder $builder
101
     * @return static
102
     */
103 2
    public function prepare($builder = null)
104
    {
105
        // NOTE: because the same ActiveQuery may be used to build different SQL statements
106
        // (e.g. by ActiveDataProvider, one for count query, the other for row data query,
107
        // it is important to make sure the same ActiveQuery can be used to build SQL statements
108
        // multiple times.
109 2
        if (!empty($this->joinWith)) {
110
            $this->buildJoinWith();
111
            $this->joinWith = null;
112
        }
113
114 2
        return $this;
115
    }
116
117
    /**
118
     * @param $with
119
     * @return static
120
     */
121
    public function joinWith($with)
122
    {
123
        $this->joinWith[] = (array) $with;
124
125
        return $this;
126
    }
127
128
    private function buildJoinWith()
129
    {
130
        $join = $this->join;
131
        $this->join = [];
132
133
        $model = new $this->modelClass();
134
135
        foreach ($this->joinWith as $with) {
136
            $this->joinWithRelations($model, $with);
137
138
            foreach ($with as $name => $callback) {
139
                if (is_int($name)) {
140
                    $this->innerJoin([$callback]);
141
                } else {
142
                    $this->innerJoin([$name => $callback]);
143
                }
144
145
                unset($with[$name]);
146
            }
147
        }
148
149
        if (!empty($join)) {
150
            // append explicit join to joinWith()
151
            // https://github.com/yiisoft/yii2/issues/2880
152
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
153
        }
154
155
        if (empty($this->select) || true) {
156
            $this->addSelect(['*' => '*']);
157
            foreach ($this->joinWith as $join) {
158
                $key = array_shift(array_keys($join));
0 ignored issues
show
Bug introduced by
array_keys($join) cannot be passed to array_shift() as the parameter $array expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
                $key = array_shift(/** @scrutinizer ignore-type */ array_keys($join));
Loading history...
159
                $closure = array_shift($join);
160
161
                $this->addSelect(is_int($key) ? $closure : $key);
162
            }
163
        }
164
    }
165
166
    /**
167
     * @param ActiveRecord $model
168
     * @param $with
169
     */
170
    protected function joinWithRelations($model, $with)
171
    {
172
        foreach ($with as $name => $callback) {
173
            if (is_int($name)) {
174
                $name = $callback;
175
                $callback = null;
176
            }
177
178
            $primaryModel = $model;
179
            $parent = $this;
180
181
            if (!isset($relations[$name])) {
182
                $relations[$name] = $relation = $primaryModel->getRelation($name);
183
                if ($callback !== null) {
184
                    call_user_func($callback, $relation);
185
                }
186
                if (!empty($relation->joinWith)) {
0 ignored issues
show
Bug introduced by
Accessing joinWith on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
187
                    $relation->buildJoinWith();
188
                }
189
                $this->joinWithRelation($parent, $relation);
190
            }
191
        }
192
    }
193
194
    /**
195
     * Joins a parent query with a child query.
196
     * The current query object will be modified accordingly.
197
     * @param ActiveQuery $parent
198
     * @param ActiveQuery $child
199
     */
200
    private function joinWithRelation($parent, $child)
0 ignored issues
show
Unused Code introduced by
The parameter $parent is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

200
    private function joinWithRelation(/** @scrutinizer ignore-unused */ $parent, $child)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
201
    {
202
        if (!empty($child->join)) {
203
            foreach ($child->join as $join) {
204
                $this->join[] = $join;
205
            }
206
        }
207
    }
208
209
    public function select($columns, $option = null)
210
    {
211
        $this->select = $columns;
212
213
        return $this;
214
    }
215
216
    /**
217
     * @param array|string $columns
218
     * @return $this
219
     */
220
    public function addSelect($columns)
221
    {
222
        if (!is_array($columns)) {
223
            $columns = (array) $columns;
224
        }
225
226
        if ($this->select === null) {
227
            $this->select = $columns;
228
        } else {
229
            $this->select = array_merge($this->select, $columns);
230
        }
231
232
        return $this;
233
    }
234
235
    /**
236
     * Executes query and returns a single row of result.
237
     *
238
     * @param AbstractConnection $db the DB connection used to create the DB command.
239
     * If null, the DB connection returned by [[modelClass]] will be used.
240
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
241
     * the query result may be either an array or an ActiveRecord object. Null will be returned
242
     * if the query results in nothing.
243
     */
244
    public function one($db = null)
245
    {
246
        if ($this->asArray) {
247
            return parent::one($db);
0 ignored issues
show
Unused Code introduced by
The call to yii\db\ActiveRelationTrait::one() has too many arguments starting with $db. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

247
            return parent::/** @scrutinizer ignore-call */ one($db);

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. Please note the @ignore annotation hint above.

Loading history...
Bug Best Practice introduced by
The expression return parent::one($db) returns the type yii\db\ActiveRecordInterface which is incompatible with the return type mandated by yii\db\QueryInterface::one() of boolean|array.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
248
        }
249
250
        $row = $this->searchOne($db);
251
        if ($row === null) {
252
            return null;
253
        }
254
        $models = $this->populate([$row]);
255
256
        return reset($models);
257
    }
258
259
    /**
260
     * Executes query and returns all results as an array.
261
     * @param AbstractConnection $db the DB connection used to create the DB command.
262
     * If null, the DB connection returned by [[modelClass]] will be used.
263
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
264
     */
265 2
    public function all($db = null)
266
    {
267 2
        if ($this->asArray) {
268
            return parent::all($db);
0 ignored issues
show
Unused Code introduced by
The call to yii\db\ActiveRelationTrait::all() has too many arguments starting with $db. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

268
            return parent::/** @scrutinizer ignore-call */ all($db);

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. Please note the @ignore annotation hint above.

Loading history...
269
        }
270
271 2
        $rows = $this->searchAll($db);
272
273 2
        return $this->populate($rows);
274
    }
275
276 2
    public function populate($rows)
277
    {
278 2
        if (empty($rows)) {
279
            return [];
280
        }
281
282 2
        $models = $this->createModels($rows);
283
284 2
        if (!empty($this->with)) {
285
            $this->findWith($this->with, $models);
286
        }
287
288 2
        foreach ($models as $model) {
289 2
            $model->afterFind();
290
        }
291
292 2
        return $models;
293
    }
294
295 2
    private function createModels($rows)
296
    {
297 2
        $models = [];
298 2
        $class = $this->modelClass;
299 2
        foreach ($rows as $row) {
300 2
            $model = $class::instantiate($row);
301 2
            $modelClass = get_class($model);
302 2
            $modelClass::populateRecord($model, $row);
303 2
            $this->populateJoinedRelations($model, $row);
304 2
            if ($this->indexBy) {
305
                if ($this->indexBy instanceof \Closure) {
306
                    $key = call_user_func($this->indexBy, $model);
307
                } else {
308
                    $key = $model->{$this->indexBy};
309
                }
310
                $models[$key] = $model;
311
            } else {
312 2
                $models[] = $model;
313
            }
314
        }
315
316 2
        return $models;
317
    }
318
319
    /**
320
     * Populates joined relations from [[join]] array.
321
     *
322
     * @param ActiveRecord $model
323
     * @param array $row
324
     */
325 2
    public function populateJoinedRelations($model, array $row)
326
    {
327 2
        foreach ($row as $key => $value) {
328 2
            if (empty($this->join) || !is_array($value) || $model->hasAttribute($key)) {
329 2
                continue;
330
            }
331
            foreach ($this->join as $join) {
332
                $name = array_shift(array_keys($join));
0 ignored issues
show
Bug introduced by
array_keys($join) cannot be passed to array_shift() as the parameter $array expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

332
                $name = array_shift(/** @scrutinizer ignore-type */ array_keys($join));
Loading history...
333
                $closure = array_shift($join);
334
335
                if (is_int($name)) {
336
                    $name = $closure;
337
                    $closure = null;
338
                }
339
                if ($name !== $key) {
340
                    continue;
341
                }
342
                if ($model->isRelationPopulated($name)) {
343
                    continue 2;
344
                }
345
                $records = [];
346
                $relation = $model->getRelation($name);
347
                $relationClass = $relation->modelClass;
0 ignored issues
show
Bug introduced by
Accessing modelClass on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
348
                if ($closure !== null) {
349
                    call_user_func($closure, $relation);
350
                }
351
                $relation->prepare();
352
353
                if ($relation->multiple) {
0 ignored issues
show
Bug introduced by
Accessing multiple on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
354
                    foreach ($value as $item) {
355
                        $relatedModel = $relationClass::instantiate($item);
356
                        $relatedModelClass = get_class($relatedModel);
357
                        $relatedModelClass::populateRecord($relatedModel, $item);
358
                        $relation->populateJoinedRelations($relatedModel, $item);
359
                        $relation->addInverseRelation($relatedModel, $model);
360
                        if ($relation->indexBy !== null) {
0 ignored issues
show
Bug introduced by
Accessing indexBy on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
361
                            $index = is_string($relation->indexBy)
362
                                ? $relatedModel[$relation->indexBy]
363
                                : call_user_func($relation->indexBy, $relatedModel);
364
                            $records[$index] = $relatedModel;
365
                        } else {
366
                            $records[] = $relatedModel;
367
                        }
368
                    }
369
                } else {
370
                    $relatedModel = $relationClass::instantiate($value);
371
                    $relatedModelClass = get_class($relatedModel);
372
                    $relatedModelClass::populateRecord($relatedModel, $value);
373
                    $relation->populateJoinedRelations($relatedModel, $value);
374
                    $relation->addInverseRelation($relatedModel, $model);
375
                    $records = $relatedModel;
376
                }
377
378
                $model->populateRelation($name, $records);
379
            }
380
        }
381 2
    }
382
383
    /**
384
     * @param $relatedModel
385
     */
386
    private function addInverseRelation($relatedModel)
387
    {
388
        if ($this->inverseOf === null) {
389
            return;
390
        }
391
392
        $inverseRelation = $relatedModel->getRelation($this->inverseOf);
393
        $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
394
    }
395
}
396