Completed
Push — master ( c3bf1d...4da758 )
by Andrii
02:28
created

ActiveQuery::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
crap 1
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 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 2
        }
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 2
        }
94
95 2
        $commandConfig = $db->getQueryBuilder()->build($this);
96
97 2
        return $db->createCommand($commandConfig);
98
    }
99
100
    /**
101
     * Prepares query for use. See NOTE.
102
     * @param QueryBuilder $builder
103
     * @return static
104
     */
105 2
    public function prepare($builder = null)
106
    {
107
        // NOTE: because the same ActiveQuery may be used to build different SQL statements
108
        // (e.g. by ActiveDataProvider, one for count query, the other for row data query,
109
        // it is important to make sure the same ActiveQuery can be used to build SQL statements
110
        // multiple times.
111 2
        if (!empty($this->joinWith)) {
112
            $this->buildJoinWith();
113
            $this->joinWith = null;
114
        }
115
116 2
        return $this;
117 1
    }
118
119
    /**
120
     * @param $with
121
     * @return static
122
     */
123
    public function joinWith($with)
124
    {
125
        $this->joinWith[] = (array) $with;
126
127
        return $this;
128
    }
129
130 1
    private function buildJoinWith()
131
    {
132
        $join = $this->join;
133
        $this->join = [];
134
135
        $model = new $this->modelClass();
136
137
        foreach ($this->joinWith as $with) {
0 ignored issues
show
Bug introduced by
The expression $this->joinWith of type array|null 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...
138
            $this->joinWithRelations($model, $with);
139
140
            foreach ($with as $name => $callback) {
141
                if (is_int($name)) {
142
                    $this->innerJoin([$callback]);
143
                } else {
144
                    $this->innerJoin([$name => $callback]);
145
                }
146
147
                unset($with[$name]);
148
            }
149
        }
150
151
        if (!empty($join)) {
152
            // append explicit join to joinWith()
153
            // https://github.com/yiisoft/yii2/issues/2880
154
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
155
        }
156
157
        if (empty($this->select) || true) {
158
            $this->addSelect(['*' => '*']);
159
            foreach ($this->joinWith as $join) {
0 ignored issues
show
Bug introduced by
The expression $this->joinWith of type array|null 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...
160
                $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.
Loading history...
161
                $closure = array_shift($join);
162
163 1
                $this->addSelect(is_int($key) ? $closure : $key);
164
            }
165
        }
166
    }
167
168
    /**
169
     * @param ActiveRecord $model
170
     * @param $with
171
     */
172
    protected function joinWithRelations($model, $with)
173
    {
174
        foreach ($with as $name => $callback) {
175
            if (is_int($name)) {
176
                $name = $callback;
177
                $callback = null;
178
            }
179
180
            $primaryModel = $model;
181
            $parent = $this;
182
183
            if (!isset($relations[$name])) {
184
                $relations[$name] = $relation = $primaryModel->getRelation($name);
0 ignored issues
show
Bug introduced by
The variable $relations does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
185
                if ($callback !== null) {
186
                    call_user_func($callback, $relation);
187
                }
188
                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?

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...
189
                    $relation->buildJoinWith();
190
                }
191
                $this->joinWithRelation($parent, $relation);
0 ignored issues
show
Compatibility introduced by
$relation of type object<yii\db\ActiveQueryInterface> is not a sub-type of object<hiqdev\hiart\ActiveQuery>. It seems like you assume a concrete implementation of the interface yii\db\ActiveQueryInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
192
            }
193
        }
194
    }
195
196
    /**
197
     * Joins a parent query with a child query.
198
     * The current query object will be modified accordingly.
199
     * @param ActiveQuery $parent
200
     * @param ActiveQuery $child
201
     */
202
    private function joinWithRelation($parent, $child)
0 ignored issues
show
Unused Code introduced by
The parameter $parent is not used and could be removed.

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

Loading history...
203
    {
204
        if (!empty($child->join)) {
205
            foreach ($child->join as $join) {
206
                $this->join[] = $join;
207
            }
208
        }
209
    }
210
211
    public function select($columns, $option = null)
212
    {
213
        $this->select = $columns;
214
215
        return $this;
216
    }
217
218
    /**
219
     * @param array|string $columns
220
     * @return $this
221
     */
222
    public function addSelect($columns)
223
    {
224
        if (!is_array($columns)) {
225
            $columns = (array) $columns;
226
        }
227
228
        if ($this->select === null) {
229
            $this->select = $columns;
230
        } else {
231
            $this->select = array_merge($this->select, $columns);
232
        }
233
234
        return $this;
235
    }
236
237
    /**
238
     * Executes query and returns a single row of result.
239
     *
240
     * @param AbstractConnection $db the DB connection used to create the DB command.
241
     * If null, the DB connection returned by [[modelClass]] will be used.
242
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
243
     * the query result may be either an array or an ActiveRecord object. Null will be returned
244
     * if the query results in nothing.
245
     */
246
    public function one($db = null)
247
    {
248
        if ($this->asArray) {
249
            return parent::one($db);
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::one($db); of type array|boolean adds the type boolean to the return on line 249 which is incompatible with the return type declared by the interface yii\db\ActiveQueryInterface::one of type yii\db\ActiveRecordInterface|array|null.
Loading history...
Bug introduced by
It seems like $db defined by parameter $db on line 246 can also be of type object<hiqdev\hiart\AbstractConnection>; however, hiqdev\hiart\Query::one() does only seem to accept object<yii\db\Connection>|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...
250
        }
251
252
        $row = $this->searchOne($db);
253
        if ($row === null) {
254
            return null;
255
        }
256
257
        return reset($this->populate([$row]));
0 ignored issues
show
Bug introduced by
$this->populate(array($row)) cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
258
    }
259
260
    /**
261
     * Executes query and returns all results as an array.
262
     * @param AbstractConnection $db the DB connection used to create the DB command.
263
     * If null, the DB connection returned by [[modelClass]] will be used.
264
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
265
     */
266 2
    public function all($db = null)
267
    {
268 2
        if ($this->asArray) {
269
            return parent::all($db);
0 ignored issues
show
Bug introduced by
It seems like $db defined by parameter $db on line 266 can also be of type object<hiqdev\hiart\AbstractConnection>; however, hiqdev\hiart\Query::all() does only seem to accept object<yii\db\Connection>|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...
270
        }
271
272 2
        $rows = $this->searchAll();
273
274 2
        return $this->populate($rows);
275
    }
276
277 2
    public function populate($rows)
278
    {
279 2
        if (empty($rows)) {
280
            return [];
281
        }
282
283 2
        $models = $this->createModels($rows);
284
285 2
        if (!empty($this->with)) {
286
            $this->findWith($this->with, $models);
287
        }
288
289 2
        foreach ($models as $model) {
290 2
            $model->afterFind();
291 2
        }
292
293 2
        return $models;
294
    }
295
296 2
    private function createModels($rows)
297
    {
298 2
        $models = [];
299 2
        $class = $this->modelClass;
300 2
        foreach ($rows as $row) {
301 2
            $model = $class::instantiate($row);
302 2
            $modelClass = get_class($model);
303 2
            $modelClass::populateRecord($model, $row);
304 2
            $this->populateJoinedRelations($model, $row);
305 2
            if ($this->indexBy) {
306 View Code Duplication
                if ($this->indexBy instanceof \Closure) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
307
                    $key = call_user_func($this->indexBy, $model);
308
                } else {
309
                    $key = $model->{$this->indexBy};
310
                }
311
                $models[$key] = $model;
312
            } else {
313 2
                $models[] = $model;
314
            }
315 2
        }
316
317 2
        return $models;
318
    }
319
320
    /**
321
     * Populates joined relations from [[join]] array.
322
     *
323
     * @param ActiveRecord $model
324
     * @param array $row
325
     */
326 2
    public function populateJoinedRelations($model, array $row)
327
    {
328 2
        foreach ($row as $key => $value) {
329 2
            if (empty($this->join) || !is_array($value) || $model->hasAttribute($key)) {
330 2
                continue;
331
            }
332
            foreach ($this->join as $join) {
333
                $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.
Loading history...
334
                $closure = array_shift($join);
335
336
                if (is_int($name)) {
337
                    $name = $closure;
338
                    $closure = null;
339
                }
340
                if ($name !== $key) {
341
                    continue;
342
                }
343
                if ($model->isRelationPopulated($name)) {
344
                    continue 2;
345
                }
346
                $records = [];
347
                $relation = $model->getRelation($name);
348
                $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?

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...
349
                if ($closure !== null) {
350
                    call_user_func($closure, $relation);
351
                }
352
                $relation->prepare();
353
354
                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?

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...
355
                    foreach ($value as $item) {
356
                        $relatedModel = $relationClass::instantiate($item);
357
                        $relatedModelClass = get_class($relatedModel);
358
                        $relatedModelClass::populateRecord($relatedModel, $item);
359
                        $relation->populateJoinedRelations($relatedModel, $item);
360
                        $relation->addInverseRelation($relatedModel, $model);
361
                        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?

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...
362
                            $index = is_string($relation->indexBy)
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?

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...
363
                                ? $relatedModel[$relation->indexBy]
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?

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...
364
                                : call_user_func($relation->indexBy, $relatedModel);
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?

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...
365
                            $records[$index] = $relatedModel;
366
                        } else {
367
                            $records[] = $relatedModel;
368
                        }
369
                    }
370
                } else {
371
                    $relatedModel = $relationClass::instantiate($value);
372
                    $relatedModelClass = get_class($relatedModel);
373
                    $relatedModelClass::populateRecord($relatedModel, $value);
374
                    $relation->populateJoinedRelations($relatedModel, $value);
375
                    $relation->addInverseRelation($relatedModel, $model);
376
                    $records = $relatedModel;
377
                }
378
379
                $model->populateRelation($name, $records);
380
            }
381 2
        }
382 2
    }
383
384
    /**
385
     * @param $relatedModel
386
     */
387
    private function addInverseRelation($relatedModel)
388
    {
389
        if ($this->inverseOf === null) {
390
            return;
391
        }
392
393
        $inverseRelation = $relatedModel->getRelation($this->inverseOf);
394
        $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
395
    }
396
}
397