ActiveQuery::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
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-2019, 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
use yii\db\BaseActiveRecord;
18
19
class ActiveQuery extends Query implements ActiveQueryInterface
20
{
21
    use ActiveQueryTrait;
22
    use ActiveRelationTrait;
23
24
    /**
25
     * @event Event an event that is triggered when the query is initialized via [[init()]].
26
     */
27
    const EVENT_INIT = 'init';
28
29
    /**
30
     * @var array|null a list of relations that this query should be joined with
31
     */
32
    public $joinWith = [];
33
34
    /**
35
     * Constructor.
36
     * @param string $modelClass the model class associated with this query
37
     * @param array $config configurations to be applied to the newly created query object
38
     */
39 2
    public function __construct($modelClass, $config = [])
40
    {
41 2
        $this->modelClass = $modelClass;
42
43 2
        parent::__construct($config);
44 2
    }
45
46
    /**
47
     * Initializes the object.
48
     * This method is called at the end of the constructor. The default implementation will trigger
49
     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
50
     * to ensure triggering of the event.
51
     */
52 2
    public function init()
53
    {
54 2
        parent::init();
55 2
        $this->trigger(self::EVENT_INIT);
56 2
    }
57
58
    /**
59
     * Creates a DB command that can be used to execute this query.
60
     * @param AbstractConnection $db the DB connection used to create the DB command.
61
     * If null, the DB connection returned by [[modelClass]] will be used.
62
     * @return Command the created DB command instance
63
     */
64 2
    public function createCommand($db = null)
65
    {
66 2
        if ($this->primaryModel !== null) {
67
            // lazy loading
68
            if (is_array($this->via)) {
69
                // via relation
70
                /** @var $viaQuery ActiveQuery */
71
                list($viaName, $viaQuery) = $this->via;
72
                if ($viaQuery->multiple) {
73
                    $viaModels = $viaQuery->all();
74
                    $this->primaryModel->populateRelation($viaName, $viaModels);
75
                } else {
76
                    $model = $viaQuery->one();
77
                    $this->primaryModel->populateRelation($viaName, $model);
78
                    $viaModels = $model === null ? [] : [$model];
79
                }
80
                $this->filterByModels($viaModels);
81
            } else {
82
                $this->filterByModels([$this->primaryModel]);
83
            }
84
        }
85
86
        /* @var $modelClass ActiveRecord */
87 2
        $modelClass = $this->modelClass;
88
89 2
        if ($db === null) {
90 2
            $db = $modelClass::getDb();
91
        }
92 2
        if ($this->from === null) {
93 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|null 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...
94
        }
95
96 2
        return parent::createCommand($db);
97
    }
98
99
    /**
100
     * Prepares query for use. See NOTE.
101
     * @param QueryBuilder $builder
102
     * @return static
103
     */
104 2
    public function prepare($builder = null)
105
    {
106
        // NOTE: because the same ActiveQuery may be used to build different SQL statements
107
        // (e.g. by ActiveDataProvider, one for count query, the other for row data query,
108
        // it is important to make sure the same ActiveQuery can be used to build SQL statements
109
        // multiple times.
110 2
        if (!empty($this->joinWith)) {
111
            $this->buildJoinWith();
112
            $this->joinWith = null;
113
        }
114
115 2
        return $this;
116
    }
117
118
    /**
119
     * @param $with
120
     * @return static
121
     */
122
    public function joinWith($with)
123
    {
124
        $this->joinWith[] = (array) $with;
125
126
        return $this;
127
    }
128
129
    private function buildJoinWith()
130
    {
131
        $join = $this->join;
132
        $this->join = [];
133
134
        $model = new $this->modelClass();
135
136
        foreach ($this->joinWith as $with) {
137
            $this->joinWithRelations($model, $with);
138
139
            foreach ($with as $name => $callback) {
140
                if (is_int($name)) {
141
                    $this->innerJoin([$callback]);
142
                } else {
143
                    $this->innerJoin([$name => $callback]);
144
                }
145
146
                unset($with[$name]);
147
            }
148
        }
149
150
        if (!empty($join)) {
151
            // append explicit join to joinWith()
152
            // https://github.com/yiisoft/yii2/issues/2880
153
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
154
        }
155
156
        if (empty($this->select) || true) {
157
            $this->addSelect(['*' => '*']);
158
            foreach ($this->joinWith as $join) {
159
                $keys = array_keys($join);
160
                $key = array_shift($keys);
161
                $closure = array_shift($join);
162
163
                $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
        $relations = [];
175
        foreach ($with as $name => $callback) {
176
            if (is_int($name)) {
177
                $name = $callback;
178
                $callback = null;
179
            }
180
181
            $primaryModel = $model;
182
            $parent = $this;
183
184
            if (!isset($relations[$name])) {
185
                $relations[$name] = $relation = $primaryModel->getRelation($name);
186
                if ($callback !== null) {
187
                    call_user_func($callback, $relation);
0 ignored issues
show
Bug introduced by
$callback of type null is incompatible with the type callable expected by parameter $callback of call_user_func(). ( Ignorable by Annotation )

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

187
                    call_user_func(/** @scrutinizer ignore-type */ $callback, $relation);
Loading history...
188
                }
189
                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...
190
                    $relation->buildJoinWith();
191
                }
192
                $this->joinWithRelation($parent, $relation);
193
            }
194
        }
195
    }
196
197
    /**
198
     * Joins a parent query with a child query.
199
     * The current query object will be modified accordingly.
200
     * @param ActiveQuery $parent
201
     * @param ActiveQuery $child
202
     */
203
    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

203
    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...
204
    {
205
        if (!empty($child->join)) {
206
            foreach ($child->join as $join) {
207
                $this->join[] = $join;
208
            }
209
        }
210
    }
211
212
    public function select($columns, $option = null)
213
    {
214
        $this->select = $columns;
215
216
        return $this;
217
    }
218
219
    /**
220
     * @param array|string $columns
221
     * @return $this
222
     */
223
    public function addSelect($columns)
224
    {
225
        if (!is_array($columns)) {
226
            $columns = (array) $columns;
227
        }
228
229
        if ($this->select === null) {
230
            $this->select = $columns;
231
        } else {
232
            $this->select = array_merge($this->select, $columns);
233
        }
234
235
        return $this;
236
    }
237
238
    /**
239
     * Executes query and returns a single row of result.
240
     *
241
     * @param AbstractConnection $db the DB connection used to create the DB command.
242
     * If null, the DB connection returned by [[modelClass]] will be used.
243
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
244
     * the query result may be either an array or an ActiveRecord object. Null will be returned
245
     * if the query results in nothing.
246
     */
247
    public function one($db = null)
248
    {
249
        if ($this->asArray) {
250
            return parent::one($db);
251
        }
252
253
        $row = $this->searchOne($db);
254
        if ($row === null) {
255
            return null;
256
        }
257
        $models = $this->populate([$row]);
258
259
        return reset($models);
260
    }
261
262
    /**
263
     * Executes query and returns all results as an array.
264
     * @param AbstractConnection $db the DB connection used to create the DB command.
265
     * If null, the DB connection returned by [[modelClass]] will be used.
266
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
267
     */
268 2
    public function all($db = null)
269
    {
270 2
        if ($this->asArray) {
271
            return parent::all($db);
272
        }
273
274 2
        $rows = $this->searchAll($db);
275
276 2
        return $this->populate($rows);
277
    }
278
279 2
    public function populate($rows)
280
    {
281 2
        if (empty($rows)) {
282
            return [];
283
        }
284
285 2
        $models = $this->createModels($rows);
286
287 2
        if (!empty($this->with)) {
288
            $this->findWith($this->with, $models);
289
        }
290
291 2
        foreach ($models as $model) {
292 2
            $model->afterFind();
293
        }
294
295 2
        return $models;
296
    }
297
298 2
    private function createModels($rows)
299
    {
300 2
        $models = [];
301 2
        $class = $this->modelClass;
302 2
        foreach ($rows as $row) {
303 2
            $model = $class::instantiate($row);
304 2
            $modelClass = get_class($model);
305 2
            $modelClass::populateRecord($model, $row);
306 2
            $this->populateJoinedRelations($model, $row);
307 2
            if ($this->indexBy) {
308
                if ($this->indexBy instanceof \Closure) {
309
                    $key = call_user_func($this->indexBy, $model);
310
                } else {
311
                    $key = $model->{$this->indexBy};
312
                }
313
                $models[$key] = $model;
314
            } else {
315 2
                $models[] = $model;
316
            }
317
        }
318
319 2
        return $models;
320
    }
321
322
    /**
323
     * Populates joined relations from [[join]] array.
324
     *
325
     * @param ActiveRecord $model
326
     * @param array $row
327
     */
328 2
    public function populateJoinedRelations($model, array $row)
329
    {
330 2
        foreach ($row as $key => $value) {
331 2
            if (empty($this->join) || !is_array($value) || $model->hasAttribute($key)) {
332 2
                continue;
333
            }
334
            foreach ($this->join as $join) {
335
                $keys = array_keys($join);
336
                $name = array_shift($keys);
337
                $closure = array_shift($join);
338
339
                if (is_int($name)) {
340
                    $name = $closure;
341
                    $closure = null;
342
                }
343
                if ($name !== $key) {
344
                    continue;
345
                }
346
                if ($model->isRelationPopulated($name)) {
347
                    continue 2;
348
                }
349
                $records = [];
350
                $relation = $model->getRelation($name);
351
                $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...
352
                if ($closure !== null) {
353
                    call_user_func($closure, $relation);
354
                }
355
                $relation->prepare();
356
357
                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...
358
                    foreach ($value as $item) {
359
                        $relatedModel = $relationClass::instantiate($item);
360
                        $relatedModelClass = get_class($relatedModel);
361
                        $relatedModelClass::populateRecord($relatedModel, $item);
362
                        $relation->populateJoinedRelations($relatedModel, $item);
363
                        $relation->addInverseRelation($relatedModel, $model);
364
                        $relatedModel->trigger(BaseActiveRecord::EVENT_AFTER_FIND);
365
                        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...
366
                            $index = is_string($relation->indexBy)
367
                                ? $relatedModel[$relation->indexBy]
368
                                : call_user_func($relation->indexBy, $relatedModel);
369
                            $records[$index] = $relatedModel;
370
                        } else {
371
                            $records[] = $relatedModel;
372
                        }
373
                    }
374
                } else {
375
                    $relatedModel = $relationClass::instantiate($value);
376
                    $relatedModelClass = get_class($relatedModel);
377
                    $relatedModelClass::populateRecord($relatedModel, $value);
378
                    $relation->populateJoinedRelations($relatedModel, $value);
379
                    $relation->addInverseRelation($relatedModel, $model);
380
                    $relatedModel->trigger(BaseActiveRecord::EVENT_AFTER_FIND);
381
                    $records = $relatedModel;
382
                }
383
384
                $model->populateRelation($name, $records);
385
            }
386
        }
387 2
    }
388
389
    /**
390
     * @param $relatedModel
391
     */
392
    private function addInverseRelation($relatedModel)
393
    {
394
        if ($this->inverseOf === null) {
395
            return;
396
        }
397
398
        $inverseRelation = $relatedModel->getRelation($this->inverseOf);
399
        $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
400
    }
401
}
402