Completed
Push — master ( 25859b...314ac7 )
by Dmitry
03:40
created

ActiveQuery::populate()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 16
ccs 0
cts 14
cp 0
rs 9.2
cc 4
eloc 9
nc 5
nop 1
crap 20
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\NotSupportedException;
15
use yii\db\ActiveQueryInterface;
16
use yii\db\ActiveQueryTrait;
17
use yii\db\ActiveRelationTrait;
18
use yii\helpers\ArrayHelper;
19
20
class ActiveQuery extends Query implements ActiveQueryInterface
21
{
22
    use ActiveQueryTrait {
23
        createModels as defaultCreateModels;
24
    }
25
26
    use ActiveRelationTrait;
27
28
    /**
29
     * @event Event an event that is triggered when the query is initialized via [[init()]].
30
     */
31
    const EVENT_INIT = 'init';
32
33
    /**
34
     * @var array a list of relations that this query should be joined with
35
     */
36
    public $joinWith = [];
37
38
    /**
39
     * @var array options for search
40
     */
41
    public $options = [];
42
43
    /**
44
     * Constructor.
45
     *
46
     * @param array $modelClass the model class associated with this query
47
     * @param array $config configurations to be applied to the newly created query object
48
     */
49
    public function __construct($modelClass, $config = [])
50
    {
51
        $this->modelClass = $modelClass;
0 ignored issues
show
Documentation Bug introduced by
It seems like $modelClass of type array is incompatible with the declared type string of property $modelClass.

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...
52
53
        parent::__construct($config);
54
    }
55
56
    /**
57
     * Initializes the object.
58
     * This method is called at the end of the constructor. The default implementation will trigger
59
     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
60
     * to ensure triggering of the event.
61
     */
62
    public function init()
63
    {
64
        parent::init();
65
        $this->trigger(self::EVENT_INIT);
66
    }
67
68
    /**
69
     * Creates a DB command that can be used to execute this query.
70
     *
71
     * @param Connection $db the DB connection used to create the DB command.
72
     *                       If null, the DB connection returned by [[modelClass]] will be used.
73
     *
74
     * @return Command the created DB command instance.
75
     */
76
    public function createCommand($db = null)
77
    {
78
        if ($this->primaryModel !== null) {
79
            // lazy loading
80
            if (is_array($this->via)) {
81
                // via relation
82
                /* @var $viaQuery ActiveQuery */
83
                list($viaName, $viaQuery) = $this->via;
84
                if ($viaQuery->multiple) {
85
                    $viaModels = $viaQuery->all();
86
                    $this->primaryModel->populateRelation($viaName, $viaModels);
87
                } else {
88
                    $model = $viaQuery->one();
89
                    $this->primaryModel->populateRelation($viaName, $model);
90
                    $viaModels = $model === null ? [] : [$model];
91
                }
92
                $this->filterByModels($viaModels);
93
            } else {
94
                $this->filterByModels([$this->primaryModel]);
95
            }
96
        }
97
98
        /* @var $modelClass ActiveRecord */
99
        $modelClass = $this->modelClass;
100
        if ($db === null) {
101
            $db = $modelClass::getDb();
102
        }
103
104
        if ($this->type === null) {
105
            $this->type = $modelClass::type();
106
        }
107
        if ($this->index === null) {
108
            $this->index = $modelClass::index();
109
            $this->type = $modelClass::type();
110
        }
111
112
        $commandConfig = $db->getQueryBuilder()->build($this);
113
114
        return $db->createCommand($commandConfig);
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function prepare()
121
    {
122
        // NOTE: because the same ActiveQuery may be used to build different SQL statements
123
        // (e.g. by ActiveDataProvider, one for count query, the other for row data query,
124
        // it is important to make sure the same ActiveQuery can be used to build SQL statements
125
        // multiple times.
126
        if (!empty($this->joinWith)) {
127
            $this->buildJoinWith();
128
            $this->joinWith = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $joinWith.

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...
129
        }
130
    }
131
132
    public function joinWith($with)
133
    {
134
        $this->joinWith[] = [(array) $with, true];
135
136
        return $this;
137
    }
138
139
    private function buildJoinWith()
140
    {
141
        $join = $this->join;
142
143
        $this->join = [];
144
145
        foreach ($this->joinWith as $config) {
146
            list($with, $eagerLoading) = $config;
0 ignored issues
show
Unused Code introduced by
The assignment to $eagerLoading is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
147
148
            foreach ($with as $name => $callback) {
149
                if (is_int($name)) {
150
                    $this->join($callback);
151
                    unset($with[$name]);
152
                } else {
153
                    throw new NotSupportedException('joinWith() using query modification is not supported, use with() instead.');
154
                }
155
            }
156
        }
157
158
        // remove duplicated joins added by joinWithRelations that may be added
159
        // e.g. when joining a relation and a via relation at the same time
160
        $uniqueJoins = [];
161
        foreach ($this->join as $j) {
162
            $uniqueJoins[serialize($j)] = $j;
163
        }
164
        $this->join = array_values($uniqueJoins);
165
166
        if (!empty($join)) {
167
            // append explicit join to joinWith()
168
            // https://github.com/yiisoft/yii2/issues/2880
169
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
170
        }
171
172
        if (empty($this->select)) {
173
            $this->addSelect(['*' => '*']);
174
            foreach ($this->joinWith as $join) {
175
                $this->addSelect(reset($join));
176
            }
177
        }
178
    }
179
180
    public function select($columns)
181
    {
182
        $this->select = $columns;
183
184
        return $this;
185
    }
186
187
    public function addSelect($columns)
188
    {
189
        if ($this->select === null) {
190
            $this->select = $columns;
191
        } else {
192
            $this->select = array_merge($this->select, $columns);
193
        }
194
195
        return $this;
196
    }
197
198
    /**
199
     * Executes query and returns all results as an array.
200
     *
201
     * @param Connection $db the DB connection used to create the DB command.
202
     *                            If null, the DB connection returned by [[modelClass]] will be used.
203
     *
204
     * @return array the query results. If the query results in nothing, an empty array will be returned.
205
     */
206
    public function all($db = null)
207
    {
208
        if ($this->asArray) {
209
            // TODO implement with
210
            return parent::all($db);
0 ignored issues
show
Bug introduced by
It seems like $db defined by parameter $db on line 206 can also be of type object<hiqdev\hiart\Connection>; 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...
211
        }
212
213
        $rows = $this->createCommand($db)->search($this->options);
214
215
        return $this->populate($rows);
216
    }
217
218
    public function populate($rows)
219
    {
220
        if (empty($rows)) {
221
            return [];
222
        }
223
224
        $models = $this->createModels($rows);
225
        if (!empty($this->with)) {
226
            $this->findWith($this->with, $models);
227
        }
228
        foreach ($models as $model) {
229
            $model->afterFind();
230
        }
231
232
        return $models;
233
    }
234
235
    private function createModels($rows)
236
    {
237
        $models = [];
238
        if ($this->asArray) {
239
            if ($this->indexBy === null) {
240
                return $rows;
241
            }
242
            foreach ($rows as $row) {
243 View Code Duplication
                if (is_string($this->indexBy)) {
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...
244
                    $key = $row[$this->indexBy];
245
                } else {
246
                    $key = call_user_func($this->indexBy, $row);
247
                }
248
                $models[$key] = $row;
249
            }
250
        } else {
251
            /* @var $class ActiveRecord */
252
            $class = $this->modelClass;
253
            if ($this->indexBy === null) {
254
                foreach ($rows as $row) {
255
                    $model = $class::instantiate($row);
256
                    $modelClass = get_class($model);
257
                    $modelClass::populateRecord($model, $row);
258
                    $this->populateJoinedRelations($model, $row);
259
                    $models[] = $model;
260
                }
261
            } else {
262
                foreach ($rows as $row) {
263
                    $model = $class::instantiate($row);
264
                    $modelClass = get_class($model);
265
                    $modelClass::populateRecord($model, $row);
266 View Code Duplication
                    if (is_string($this->indexBy)) {
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...
267
                        $key = $model->{$this->indexBy};
268
                    } else {
269
                        $key = call_user_func($this->indexBy, $model);
270
                    }
271
                    $models[$key] = $model;
272
                }
273
            }
274
        }
275
276
        return $models;
277
    }
278
279
    /**
280
     * Populates joined relations from [[join]] array.
281
     *
282
     * @param ActiveRecord $model
283
     * @param array $row
284
     */
285
    public function populateJoinedRelations($model, array $row)
286
    {
287
        foreach ($row as $key => $value) {
288
            if (empty($this->join) || !is_array($value) || $model->hasAttribute($key)) {
289
                continue;
290
            }
291
            foreach ($this->join as $name) {
292
                if ($name !== $key) {
293
                    continue;
294
                }
295
                if ($model->isRelationPopulated($name)) {
296
                    continue 2;
297
                }
298
                $records = [];
299
                $relation = $model->getRelation($name);
300
                $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...
301
302
                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...
303
                    foreach ($value as $item) {
304
                        $relationModel = $relationClass::instantiate($item);
305
                        $relationModelClass = get_class($relationModel);
306
                        $relationModelClass::populateRecord($relationModel, $item);
307
                        $relation->populateJoinedRelations($relationModel, $item);
308
                        $records[] = $relationModel;
309
                    }
310
                } else {
311
                    $relationModel = $relationClass::instantiate($value);
312
                    $relationModelClass = get_class($relationModel);
313
                    $relationModelClass::populateRecord($relationModel, $value);
314
                    $relation->populateJoinedRelations($relationModel, $value);
315
                    $records = $relationModel;
316
                }
317
318
                $model->populateRelation($name, $records);
319
            }
320
        }
321
    }
322
323
    /**
324
     * Executes query and returns a single row of result.
325
     *
326
     * @param Connection $db the DB connection used to create the DB command.
327
     *                       If null, the DB connection returned by [[modelClass]] will be used.
328
     *
329
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
330
     *                                 the query result may be either an array or an ActiveRecord object. Null will be returned
331
     *                                 if the query results in nothing.
332
     */
333
    public function one($db = null)
334
    {
335
        //        $result = $this->createCommand($db)->get();
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% 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...
336
337
        $result = $this->createCommand($db)->search(ArrayHelper::merge(['limit' => 1], $this->options));
338
        if (empty($result)) {
339
            return null;
340
        }
341
        $result = reset($result);
342
343
        if ($this->asArray) {
344
            // TODO implement with()
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% 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...
345
//            /* @var $modelClass ActiveRecord */
346
//            $modelClass = $this->modelClass;
347
//            $model = $result['_source'];
348
//            $pk = $modelClass::primaryKey()[0];
349
//            if ($pk === '_id') {
350
//                $model['_id'] = $result['_id'];
351
//            }
352
//            $model['_score'] = $result['_score'];
353
//            if (!empty($this->with)) {
354
//                $models = [$model];
355
//                $this->findWith($this->with, $models);
356
//                $model = $models[0];
357
//            }
358
            return $result;
359
        }
360
361
        /* @var $class ActiveRecord */
362
        $class = $this->modelClass;
363
        $model = $class::instantiate($result);
364
        $class::populateRecord($model, $result);
365
        $this->populateJoinedRelations($model, $result);
366
        if (!empty($this->with)) {
367
            $models = [$model];
368
            $this->findWith($this->with, $models);
369
            $model = $models[0];
370
        }
371
        $model->afterFind();
372
373
        return $model;
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     */
379
    public function search($db = null, $options = [])
380
    {
381
        $result = $this->createCommand($db)->search($options);
382
        // TODO implement with() for asArray
383
        if (!empty($result) && !$this->asArray) {
384
            $models = $this->createModels($result);
385
            if (!empty($this->with)) {
386
                $this->findWith($this->with, $models);
387
            }
388
            foreach ($models as $model) {
389
                $model->afterFind();
390
            }
391
            $result = $models;
392
        }
393
394
        return $result;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    public function column($field, $db = null)
401
    {
402
        if ($field === '_id') {
403
            $command = $this->createCommand($db);
404
            $command->queryParts['fields'] = [];
405
            $command->queryParts['_source'] = false;
406
            $result = $command->search();
407
            if (empty($result['hits']['hits'])) {
408
                return [];
409
            }
410
            $column = [];
411
            foreach ($result['hits']['hits'] as $row) {
412
                $column[] = $row['_id'];
413
            }
414
415
            return $column;
416
        }
417
418
        return parent::column($field, $db);
419
    }
420
}
421