Passed
Push — master ( 489f9e...c62eb9 )
by Dmitry
02:07
created

src/ActiveQuery.php (1 issue)

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();
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
                $key = array_shift(array_keys($join));
0 ignored issues
show
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

159
                $key = array_shift(/** @scrutinizer ignore-type */ array_keys($join));
Loading history...
160
                $closure = array_shift($join);
161
162
                $this->addSelect(is_int($key) ? $closure : $key);
163
            }
164
        }
165
    }
166
167
    /**
168
     * @param ActiveRecord $model
169
     * @param $with
170
     */
171
    protected function joinWithRelations($model, $with)
172
    {
173
        foreach ($with as $name => $callback) {
174
            if (is_int($name)) {
175
                $name = $callback;
176
                $callback = null;
177
            }
178
179
            $primaryModel = $model;
180
            $parent = $this;
181
182
            if (!isset($relations[$name])) {
183
                $relations[$name] = $relation = $primaryModel->getRelation($name);
184
                if ($callback !== null) {
185
                    call_user_func($callback, $relation);
186
                }
187
                if (!empty($relation->joinWith)) {
188
                    $relation->buildJoinWith();
189
                }
190
                $this->joinWithRelation($parent, $relation);
191
            }
192
        }
193
    }
194
195
    /**
196
     * Joins a parent query with a child query.
197
     * The current query object will be modified accordingly.
198
     * @param ActiveQuery $parent
199
     * @param ActiveQuery $child
200
     */
201
    private function joinWithRelation($parent, $child)
202
    {
203
        if (!empty($child->join)) {
204
            foreach ($child->join as $join) {
205
                $this->join[] = $join;
206
            }
207
        }
208
    }
209
210
    public function select($columns, $option = null)
211
    {
212
        $this->select = $columns;
213
214
        return $this;
215
    }
216
217
    /**
218
     * @param array|string $columns
219
     * @return $this
220
     */
221
    public function addSelect($columns)
222
    {
223
        if (!is_array($columns)) {
224
            $columns = (array) $columns;
225
        }
226
227
        if ($this->select === null) {
228
            $this->select = $columns;
229
        } else {
230
            $this->select = array_merge($this->select, $columns);
231
        }
232
233
        return $this;
234
    }
235
236
    /**
237
     * Executes query and returns a single row of result.
238
     *
239
     * @param AbstractConnection $db the DB connection used to create the DB command.
240
     * If null, the DB connection returned by [[modelClass]] will be used.
241
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
242
     * the query result may be either an array or an ActiveRecord object. Null will be returned
243
     * if the query results in nothing.
244
     */
245
    public function one($db = null)
246
    {
247
        if ($this->asArray) {
248
            return parent::one($db);
249
        }
250
251
        $row = $this->searchOne($db);
252
        if ($row === null) {
253
            return null;
254
        }
255
        $models = $this->populate([$row]);
256
257
        return reset($models);
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);
270
        }
271
272 2
        $rows = $this->searchAll($db);
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
        }
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
                if ($this->indexBy instanceof \Closure) {
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
        }
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
                $keys = array_keys($join);
334
                $name = array_shift($keys);
335
                $closure = array_shift($join);
336
337
                if (is_int($name)) {
338
                    $name = $closure;
339
                    $closure = null;
340
                }
341
                if ($name !== $key) {
342
                    continue;
343
                }
344
                if ($model->isRelationPopulated($name)) {
345
                    continue 2;
346
                }
347
                $records = [];
348
                $relation = $model->getRelation($name);
349
                $relationClass = $relation->modelClass;
350
                if ($closure !== null) {
351
                    call_user_func($closure, $relation);
352
                }
353
                $relation->prepare();
354
355
                if ($relation->multiple) {
356
                    foreach ($value as $item) {
357
                        $relatedModel = $relationClass::instantiate($item);
358
                        $relatedModelClass = get_class($relatedModel);
359
                        $relatedModelClass::populateRecord($relatedModel, $item);
360
                        $relation->populateJoinedRelations($relatedModel, $item);
361
                        $relation->addInverseRelation($relatedModel, $model);
362
                        $relatedModel->trigger(BaseActiveRecord::EVENT_AFTER_FIND);
363
                        if ($relation->indexBy !== null) {
364
                            $index = is_string($relation->indexBy)
365
                                ? $relatedModel[$relation->indexBy]
366
                                : call_user_func($relation->indexBy, $relatedModel);
367
                            $records[$index] = $relatedModel;
368
                        } else {
369
                            $records[] = $relatedModel;
370
                        }
371
                    }
372
                } else {
373
                    $relatedModel = $relationClass::instantiate($value);
374
                    $relatedModelClass = get_class($relatedModel);
375
                    $relatedModelClass::populateRecord($relatedModel, $value);
376
                    $relation->populateJoinedRelations($relatedModel, $value);
377
                    $relation->addInverseRelation($relatedModel, $model);
378
                    $relatedModel->trigger(BaseActiveRecord::EVENT_AFTER_FIND);
379
                    $records = $relatedModel;
380
                }
381
382
                $model->populateRelation($name, $records);
383
            }
384
        }
385 2
    }
386
387
    /**
388
     * @param $relatedModel
389
     */
390
    private function addInverseRelation($relatedModel)
391
    {
392
        if ($this->inverseOf === null) {
393
            return;
394
        }
395
396
        $inverseRelation = $relatedModel->getRelation($this->inverseOf);
397
        $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
398
    }
399
}
400