Passed
Push — master ( a47013...c85fa2 )
by Wilmer
11:14 queued 09:27
created

ActiveQuery   F

Complexity

Total Complexity 118

Size/Duplication

Total Lines 859
Duplicated Lines 0 %

Test Coverage

Coverage 90.17%

Importance

Changes 0
Metric Value
wmc 118
eloc 286
dl 0
loc 859
ccs 266
cts 295
cp 0.9017
rs 2
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getPrimaryTableName() 0 6 1
A getJoinWith() 0 3 1
A populate() 0 21 6
A orOnCondition() 0 11 2
A alias() 0 17 5
A all() 0 3 1
A getJoinType() 0 7 4
A getTableNameAndAlias() 0 22 5
F joinWithRelation() 0 79 18
A getTablesUsedInFrom() 0 7 2
B joinWithRelations() 0 50 10
A innerJoinWith() 0 3 1
A getModelClass() 0 3 1
A andOnCondition() 0 11 2
A one() 0 11 3
A joinWith() 0 35 6
A queryScalar() 0 17 2
A viaTable() 0 13 3
B buildJoinWith() 0 50 11
B removeDuplicatedModels() 0 52 11
A getOn() 0 3 1
A createCommand() 0 17 2
C prepare() 0 77 15
A getSql() 0 3 1
A onCondition() 0 6 1
A __construct() 0 5 1
A sql() 0 5 1
A on() 0 5 1

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
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use Yiisoft\Db\Command\Command;
8
use Yiisoft\Db\Exception\Exception;
9
use Yiisoft\Db\Exception\InvalidArgumentException;
10
use Yiisoft\Db\Exception\NotSupportedException;
11
use Yiisoft\Db\Query\Query;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
14
/**
15
 * ActiveQuery represents a DB query associated with an Active Record class.
16
 *
17
 * An ActiveQuery can be a normal query or be used in a relational context.
18
 *
19
 * ActiveQuery instances are usually created by {@see ActiveRecord::find()} and {@see ActiveRecord::findBySql()}.
20
 * Relational queries are created by {@see ActiveRecord::hasOne()} and {@see ActiveRecord::hasMany()}.
21
 *
22
 * Normal Query
23
 * ------------
24
 *
25
 * ActiveQuery mainly provides the following methods to retrieve the query results:
26
 *
27
 * - {@see one()}: returns a single record populated with the first row of data.
28
 * - {@see all()}: returns all records based on the query results.
29
 * - {@see count()}: returns the number of records.
30
 * - {@see sum()}: returns the sum over the specified column.
31
 * - {@see average()}: returns the average over the specified column.
32
 * - {@see min()}: returns the min over the specified column.
33
 * - {@see max()}: returns the max over the specified column.
34
 * - {@see scalar()}: returns the value of the first column in the first row of the query result.
35
 * - {@see column()}: returns the value of the first column in the query result.
36
 * - {@see exists()}: returns a value indicating whether the query result has data or not.
37
 *
38
 * Because ActiveQuery extends from {@see Query}, one can use query methods, such as {@see where()}, {@see orderBy()} to
39
 * customize the query options.
40
 *
41
 * ActiveQuery also provides the following additional query options:
42
 *
43
 * - {@see with()}: list of relations that this query should be performed with.
44
 * - {@see joinWith()}: reuse a relation query definition to add a join to a query.
45
 * - {@see indexBy()}: the name of the column by which the query result should be indexed.
46
 * - {@see asArray()}: whether to return each record as an array.
47
 *
48
 * These options can be configured using methods of the same name. For example:
49
 *
50
 * ```php
51
 * $customers = Customer::find()->with('orders')->asArray()->all();
52
 * ```
53
 *
54
 * Relational query
55
 * ----------------
56
 *
57
 * In relational context ActiveQuery represents a relation between two Active Record classes.
58
 *
59
 * Relational ActiveQuery instances are usually created by calling {@see ActiveRecord::hasOne()} and
60
 * {@see ActiveRecord::hasMany()}. An Active Record class declares a relation by defining a getter method which calls
61
 * one of the above methods and returns the created ActiveQuery object.
62
 *
63
 * A relation is specified by {@see link} which represents the association between columns of different tables; and the
64
 * multiplicity of the relation is indicated by {@see multiple}.
65
 *
66
 * If a relation involves a junction table, it may be specified by {@see via()} or {@see viaTable()} method.
67
 * These methods may only be called in a relational context. Same is true for {@see inverseOf()}, which marks a relation
68
 * as inverse of another relation and {@see onCondition()} which adds a condition that is to be added to relational
69
 * query join condition.
70
 */
71
class ActiveQuery extends Query implements ActiveQueryInterface
72
{
73
    use ActiveQueryTrait;
74
    use ActiveRelationTrait;
75
76
    private ?string $modelClass;
77
    private ?string $sql = null;
78
    private $on;
79
    private array $joinWith = [];
80
81 407
    public function __construct(?string $modelClass)
82
    {
83 407
        $this->modelClass = $modelClass;
84
85 407
        parent::__construct($modelClass::getConnection());
0 ignored issues
show
Bug introduced by
The method getConnection() does not exist on null. ( Ignorable by Annotation )

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

85
        parent::__construct($modelClass::/** @scrutinizer ignore-call */ getConnection());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
86 407
    }
87
88
    /**
89
     * Executes query and returns all results as an array.
90
     *
91
     * If null, the DB connection returned by {@see modelClass} will be used.
92
     *
93
     * @throws InvalidConfigException
94
     * @throws Exception
95
     * @throws InvalidArgumentException
96
     * @throws NotSupportedException
97
     *
98
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
99
     */
100 157
    public function all(): array
101
    {
102 157
        return parent::all();
103
    }
104
105 308
    public function prepare($builder): Query
106
    {
107
        /**
108
         * NOTE: because the same ActiveQuery may be used to build different SQL statements (e.g. by ActiveDataProvider,
109
         * one for count query, the other for row data query, it is important to make sure the same ActiveQuery can be
110
         * used to build SQL statements multiple times.
111
         */
112 308
        if (!empty($this->joinWith)) {
113 39
            $this->buildJoinWith();
114
            /** clean it up to avoid issue {@see https://github.com/yiisoft/yii2/issues/2687} */
115 39
            $this->joinWith = [];
116
        }
117
118 308
        if (empty($this->getFrom())) {
119 287
            $this->from = [$this->getPrimaryTableName()];
0 ignored issues
show
Bug Best Practice introduced by
The property from does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
120
        }
121
122 308
        if (empty($this->getSelect()) && !empty($this->getJoin())) {
123 36
            [, $alias] = $this->getTableNameAndAlias();
124
125 36
            $this->select(["$alias.*"]);
126
        }
127
128 308
        if ($this->primaryModel === null) {
129
            /** eager loading */
130 302
            $query = Query::create($this->modelClass::getConnection(), $this);
0 ignored issues
show
Bug introduced by
The method getConnection() does not exist on null. ( Ignorable by Annotation )

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

130
            $query = Query::create($this->modelClass::/** @scrutinizer ignore-call */ getConnection(), $this);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
131
        } else {
132
            // lazy loading of a relation
133 87
            $where = $this->getWhere();
134
135 87
            if ($this->via instanceof self) {
136
                /** via junction table */
137 15
                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
138
139 15
                $this->filterByModels($viaModels);
140 78
            } elseif (\is_array($this->via)) {
141
                /**
142
                 * via relation
143
                 *
144
                 * @var $viaQuery ActiveQuery
145
                 */
146 21
                [$viaName, $viaQuery, $viaCallableUsed] = $this->via;
147
148 21
                if ($viaQuery->getMultiple()) {
149 21
                    if ($viaCallableUsed) {
150 15
                        $viaModels = $viaQuery->all();
151 6
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
152
                        $viaModels = $this->primaryModel->$viaName;
153
                    } else {
154 6
                        $viaModels = $viaQuery->all();
155 21
                        $this->primaryModel->populateRelation($viaName, $viaModels);
156
                    }
157
                } else {
158
                    if ($viaCallableUsed) {
159
                        $model = $viaQuery->one();
160
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
161
                        $model = $this->primaryModel->$viaName;
162
                    } else {
163
                        $model = $viaQuery->one();
164
                        $this->primaryModel->populateRelation($viaName, $model);
165
                    }
166
                    $viaModels = $model === null ? [] : [$model];
167
                }
168 21
                $this->filterByModels($viaModels);
169
            } else {
170 78
                $this->filterByModels([$this->primaryModel]);
171
            }
172
173 87
            $query = Query::create($this->modelClass::getConnection(), $this);
174 87
            $this->where($where);
175
        }
176
177 308
        if (!empty($this->on)) {
178 18
            $query->andWhere($this->on);
179
        }
180
181 308
        return $query;
182
    }
183
184 248
    public function populate($rows): array
185
    {
186 248
        if (empty($rows)) {
187 52
            return [];
188
        }
189
190 239
        $models = $this->createModels($rows);
191
192 239
        if (!empty($this->join) && $this->getIndexBy() === null) {
193 27
            $models = $this->removeDuplicatedModels($models);
194
        }
195
196 239
        if (!empty($this->with)) {
197 75
            $this->findWith($this->with, $models);
198
        }
199
200 239
        if ($this->inverseOf !== null) {
201 12
            $this->addInverseRelations($models);
202
        }
203
204 239
        return parent::populate($models);
205
    }
206
207
    /**
208
     * Removes duplicated models by checking their primary key values.
209
     *
210
     * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
211
     *
212
     * @param array $models the models to be checked
213
     *
214
     * @throws Exception
215
     * @throws InvalidConfigException
216
     * @throws NotSupportedException
217
     *
218
     * @return array the distinctive models
219
     */
220 27
    private function removeDuplicatedModels($models): array
221
    {
222 27
        $hash = [];
223
224
        /** @var $class ActiveRecord */
225 27
        $class = $this->modelClass;
226
227 27
        $pks = $class::primaryKey();
228
229 27
        if (\count($pks) > 1) {
230
            /** composite primary key */
231 6
            foreach ($models as $i => $model) {
232 6
                $key = [];
233 6
                foreach ($pks as $pk) {
234 6
                    if (!isset($model[$pk])) {
235
                        /** do not continue if the primary key is not part of the result set */
236 3
                        break 2;
237
                    }
238 6
                    $key[] = $model[$pk];
239
                }
240
241 3
                $key = \serialize($key);
242
243 3
                if (isset($hash[$key])) {
244
                    unset($models[$i]);
245
                } else {
246 3
                    $hash[$key] = true;
247
                }
248
            }
249
        } elseif (empty($pks)) {
250
            throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
251
        } else {
252
            // single column primary key
253 24
            $pk = \reset($pks);
254
255 24
            foreach ($models as $i => $model) {
256 24
                if (!isset($model[$pk])) {
257
                    /** do not continue if the primary key is not part of the result set */
258 3
                    break;
259
                }
260
261 21
                $key = $model[$pk];
262
263 21
                if (isset($hash[$key])) {
264 12
                    unset($models[$i]);
265 21
                } elseif ($key !== null) {
266 21
                    $hash[$key] = true;
267
                }
268
            }
269
        }
270
271 27
        return \array_values($models);
272
    }
273
274
    /**
275
     * Executes query and returns a single row of result.
276
     *
277
     * @throws Exception
278
     * @throws InvalidArgumentException
279
     * @throws InvalidConfigException
280
     * @throws NotSupportedException
281
     *
282
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of {@see asArray}, the
283
     * query result may be either an array or an ActiveRecord object. `null` will be returned if the query results
284
     * in nothing.
285
     */
286 190
    public function one()
287
    {
288 190
        $row = parent::one();
289
290 190
        if ($row !== false) {
0 ignored issues
show
introduced by
The condition $row !== false is always true.
Loading history...
291 187
            $models = $this->populate([$row]);
292
293 187
            return \reset($models) ?: null;
294
        }
295
296 24
        return null;
297
    }
298
299
    /**
300
     * Creates a DB command that can be used to execute this query.
301
     *
302
     * @return Command the created DB command instance.
303
     */
304 272
    public function createCommand(): Command
305
    {
306
        /** @var $modelClass ActiveRecord */
307 272
        $modelClass = $this->modelClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $modelClass is dead and can be removed.
Loading history...
308
309 272
        if ($this->sql === null) {
310 269
            [$sql, $params] = $this->modelClass::getConnection()->getQueryBuilder()->build($this);
311
        } else {
312 3
            $sql = $this->sql;
313 3
            $params = $this->params;
314
        }
315
316 272
        $command = $this->modelClass::getConnection()->createCommand($sql, $params);
317
318 272
        $this->setCommandCache($command);
319
320 272
        return $command;
321
    }
322
323 46
    protected function queryScalar($selectExpression)
324
    {
325
        /* @var $modelClass ActiveRecord */
326 46
        $modelClass = $this->modelClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $modelClass is dead and can be removed.
Loading history...
327
328 46
        if ($this->sql === null) {
329 43
            return parent::queryScalar($selectExpression);
330
        }
331
332 3
        $command = (new Query($this->modelClass::getConnection()))->select([$selectExpression])
333 3
            ->from(['c' => "({$this->sql})"])
334 3
            ->params($this->params)
335 3
            ->createCommand();
336
337 3
        $this->setCommandCache($command);
338
339 3
        return $command->queryScalar();
340
    }
341
342
    /**
343
     * Joins with the specified relations.
344
     *
345
     * This method allows you to reuse existing relation definitions to perform JOIN queries. Based on the definition of
346
     * the specified relation(s), the method will append one or multiple JOIN statements to the current query.
347
     *
348
     * If the `$eagerLoading` parameter is true, the method will also perform eager loading for the specified relations,
349
     * which is equivalent to calling {@see with()} using the specified relations.
350
     *
351
     * Note that because a JOIN query will be performed, you are responsible to disambiguate column names.
352
     *
353
     * This method differs from {@see with()} in that it will build up and execute a JOIN SQL statement
354
     * for the primary table. And when `$eagerLoading` is true, it will call {@see with()} in addition with the
355
     * specified relations.
356
     *
357
     * @param string|array $with the relations to be joined. This can either be a string, representing a relation name
358
     * or an array with the following semantics:
359
     *
360
     * - Each array element represents a single relation.
361
     * - You may specify the relation name as the array key and provide an anonymous functions that can be used to
362
     * modify the relation queries on-the-fly as the array value.
363
     * - If a relation query does not need modification, you may use the relation name as the array value.
364
     *
365
     * The relation name may optionally contain an alias for the relation table (e.g. `books b`).
366
     *
367
     * Sub-relations can also be specified, see {@see with()} for the syntax.
368
     *
369
     * In the following you find some examples:
370
     *
371
     * ```php
372
     * // find all orders that contain books, and eager loading "books"
373
     * Order::find()->joinWith('books', true, 'INNER JOIN')->all();
374
     * // find all orders, eager loading "books", and sort the orders and books by the book names.
375
     * Order::find()->joinWith([
376
     *     'books' => function (\Yiisoft\ActiveRecord\ActiveQuery $query) {
377
     *         $query->orderBy('item.name');
378
     *     }
379
     * ])->all();
380
     * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table
381
     * Order::find()->joinWith(['books b'], true, 'INNER JOIN')->where(['b.category' => 'Science fiction'])->all();
382
     * ```
383
     *
384
     * @param bool|array $eagerLoading whether to eager load the relations specified in `$with`. When this is a boolean,
385
     * it applies to all relations specified in `$with`. Use an array to explicitly list which relations in `$with` need
386
     * to be eagerly loaded.  Note, that this does not mean, that the relations are populated from the query result. An
387
     * extra query will still be performed to bring in the related data. Defaults to `true`.
388
     * @param string|array $joinType the join type of the relations specified in `$with`.  When this is a string, it
389
     * applies to all relations specified in `$with`. Use an array in the format of `relationName => joinType` to
390
     * specify different join types for different relations.
391
     *
392
     * @return $this the query object itself.
393
     */
394 48
    public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN'): self
395
    {
396 48
        $relations = [];
397
398 48
        foreach ((array) $with as $name => $callback) {
399 48
            if (\is_int($name)) {
400 48
                $name = $callback;
401 48
                $callback = null;
402
            }
403
404 48
            if (\preg_match('/^(.*?)(?:\s+AS\s+|\s+)(\w+)$/i', $name, $matches)) {
405
                /** relation is defined with an alias, adjust callback to apply alias */
406 9
                [, $relation, $alias] = $matches;
407
408 9
                $name = $relation;
409
410
                $callback = static function ($query) use ($callback, $alias) {
411
                    /** @var $query ActiveQuery */
412 9
                    $query->alias($alias);
413 9
                    if ($callback !== null) {
414 9
                        $callback($query);
415
                    }
416 9
                };
417
            }
418
419 48
            if ($callback === null) {
420 48
                $relations[] = $name;
421
            } else {
422 15
                $relations[$name] = $callback;
423
            }
424
        }
425
426 48
        $this->joinWith[] = [$relations, $eagerLoading, $joinType];
427
428 48
        return $this;
429
    }
430
431 39
    private function buildJoinWith()
432
    {
433 39
        $join = $this->join;
434
435 39
        $this->join = [];
0 ignored issues
show
Bug Best Practice introduced by
The property join does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
436
437
        /** @var $modelClass ActiveRecordInterface */
438 39
        $modelClass = $this->modelClass;
439
440 39
        $model = $modelClass::instance();
0 ignored issues
show
Bug introduced by
The method instance() does not exist on Yiisoft\ActiveRecord\ActiveRecordInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\ActiveRecord\ActiveRecordInterface. ( Ignorable by Annotation )

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

440
        /** @scrutinizer ignore-call */ 
441
        $model = $modelClass::instance();
Loading history...
441
442 39
        foreach ($this->joinWith as [$with, $eagerLoading, $joinType]) {
443 39
            $this->joinWithRelations($model, $with, $joinType);
444
445 39
            if (\is_array($eagerLoading)) {
446
                foreach ($with as $name => $callback) {
447
                    if (\is_int($name)) {
448
                        if (!\in_array($callback, $eagerLoading, true)) {
449
                            unset($with[$name]);
450
                        }
451
                    } elseif (!\in_array($name, $eagerLoading, true)) {
452
                        unset($with[$name]);
453
                    }
454
                }
455 39
            } elseif (!$eagerLoading) {
456 12
                $with = [];
457
            }
458
459 39
            $this->with($with);
460
        }
461
462
        /**
463
         * remove duplicated joins added by joinWithRelations that may be added e.g. when joining a relation and a via
464
         * relation at the same time.
465
         */
466 39
        $uniqueJoins = [];
467
468 39
        foreach ($this->join as $j) {
469 39
            $uniqueJoins[\serialize($j)] = $j;
470
        }
471
472 39
        $this->join = \array_values($uniqueJoins);
473
474 39
        if (!empty($join)) {
475
            /**
476
             * append explicit join to joinWith()
477
             *
478
             * {@see https://github.com/yiisoft/yii2/issues/2880}
479
             */
480
            $this->join = empty($this->join) ? $join : \array_merge($this->join, $join);
481
        }
482 39
    }
483
484
    /**
485
     * Inner joins with the specified relations.
486
     *
487
     * This is a shortcut method to {@see joinWith()} with the join type set as "INNER JOIN". Please refer to
488
     * {@see joinWith()} for detailed usage of this method.
489
     *
490
     * @param string|array $with the relations to be joined with.
491
     * @param bool|array $eagerLoading whether to eager load the relations. Note, that this does not mean, that the
492
     * relations are populated from the query result. An extra query will still be performed to bring in the related
493
     * data.
494
     *
495
     * @return $this the query object itself.
496
     *
497
     * {@see joinWith()}
498
     */
499 12
    public function innerJoinWith($with, $eagerLoading = true): self
500
    {
501 12
        return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
502
    }
503
504
    /**
505
     * Modifies the current query by adding join fragments based on the given relations.
506
     *
507
     * @param ActiveRecord $model the primary model.
508
     * @param array $with the relations to be joined.
509
     * @param string|array $joinType the join type.
510
     *
511
     * @throws InvalidArgumentException
512
     * @throws \ReflectionException
513
     */
514 39
    private function joinWithRelations($model, $with, $joinType)
515
    {
516 39
        $relations = [];
517
518 39
        foreach ($with as $name => $callback) {
519 39
            if (\is_int($name)) {
520 39
                $name = $callback;
521 39
                $callback = null;
522
            }
523
524 39
            $primaryModel = $model;
525 39
            $parent = $this;
526 39
            $prefix = '';
527
528 39
            while (($pos = \strpos($name, '.')) !== false) {
529 6
                $childName = \substr($name, $pos + 1);
530 6
                $name = \substr($name, 0, $pos);
531 6
                $fullName = $prefix === '' ? $name : "$prefix.$name";
532
533 6
                if (!isset($relations[$fullName])) {
534
                    $relations[$fullName] = $relation = $primaryModel->getRelation($name);
535
                    $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
536
                } else {
537 6
                    $relation = $relations[$fullName];
538
                }
539
540
                /** @var $relationModelClass ActiveRecordInterface */
541 6
                $relationModelClass = $relation->modelClass;
0 ignored issues
show
Bug introduced by
Accessing modelClass on the interface Yiisoft\ActiveRecord\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
542
543 6
                $primaryModel = new $relationModelClass();
544
545 6
                $parent = $relation;
546 6
                $prefix = $fullName;
547 6
                $name = $childName;
548
            }
549
550 39
            $fullName = $prefix === '' ? $name : "$prefix.$name";
551
552 39
            if (!isset($relations[$fullName])) {
553 39
                $relations[$fullName] = $relation = $primaryModel->getRelation($name);
554
555 39
                if ($callback !== null) {
556 15
                    $callback($relation);
557
                }
558
559 39
                if (!empty($relation->joinWith)) {
0 ignored issues
show
Bug introduced by
Accessing joinWith on the interface Yiisoft\ActiveRecord\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
560 6
                    $relation->buildJoinWith();
0 ignored issues
show
Bug introduced by
The method buildJoinWith() does not exist on Yiisoft\ActiveRecord\ActiveQueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\ActiveRecord\ActiveQueryInterface. ( Ignorable by Annotation )

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

560
                    $relation->/** @scrutinizer ignore-call */ 
561
                               buildJoinWith();
Loading history...
561
                }
562
563 39
                $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
564
            }
565
        }
566 39
    }
567
568
    /**
569
     * Returns the join type based on the given join type parameter and the relation name.
570
     *
571
     * @param string|array $joinType the given join type(s).
572
     * @param string $name relation name.
573
     *
574
     * @return string the real join type.
575
     */
576 39
    private function getJoinType($joinType, $name): string
577
    {
578 39
        if (\is_array($joinType) && isset($joinType[$name])) {
579
            return $joinType[$name];
580
        }
581
582 39
        return \is_string($joinType) ? $joinType : 'INNER JOIN';
583
    }
584
585
    /**
586
     * Returns the table name and the table alias for {@see modelClass}.
587
     *
588
     * @return array the table name and the table alias.
589
     */
590 87
    private function getTableNameAndAlias(): array
591
    {
592 87
        if (empty($this->from)) {
593 81
            $tableName = $this->getPrimaryTableName();
594
        } else {
595 45
            $tableName = '';
596
597 45
            foreach ($this->from as $alias => $tableName) {
598 45
                if (\is_string($alias)) {
599 15
                    return [$tableName, $alias];
600
                }
601 39
                break;
602
            }
603
        }
604
605 84
        if (\preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
606 3
            $alias = $matches[2];
607
        } else {
608 84
            $alias = $tableName;
609
        }
610
611 84
        return [$tableName, $alias];
612
    }
613
614
    /**
615
     * Joins a parent query with a child query.
616
     *
617
     * The current query object will be modified accordingly.
618
     *
619
     * @param ActiveQuery $parent
620
     * @param ActiveQuery $child
621
     * @param string $joinType
622
     */
623 39
    private function joinWithRelation($parent, $child, $joinType)
624
    {
625 39
        $via = $child->via;
626 39
        $child->via = null;
627
628 39
        if ($via instanceof self) {
629
            /* via table */
630 9
            $this->joinWithRelation($parent, $via, $joinType);
631 9
            $this->joinWithRelation($via, $child, $joinType);
632
633 9
            return;
634
        }
635
636 39
        if (\is_array($via)) {
637
            /** via relation */
638 12
            $this->joinWithRelation($parent, $via[1], $joinType);
639 12
            $this->joinWithRelation($via[1], $child, $joinType);
640
641 12
            return;
642
        }
643
644 39
        [$parentTable, $parentAlias] = $parent->getTableNameAndAlias();
645 39
        [$childTable, $childAlias] = $child->getTableNameAndAlias();
646
647 39
        if (!empty($child->link)) {
648 39
            if (\strpos($parentAlias, '{{') === false) {
649 39
                $parentAlias = '{{' . $parentAlias . '}}';
650
            }
651
652 39
            if (\strpos($childAlias, '{{') === false) {
653 39
                $childAlias = '{{' . $childAlias . '}}';
654
            }
655
656 39
            $on = [];
657
658 39
            foreach ($child->link as $childColumn => $parentColumn) {
659 39
                $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
660
            }
661
662 39
            $on = \implode(' AND ', $on);
663
664 39
            if (!empty($child->on)) {
665 39
                $on = ['and', $on, $child->on];
666
            }
667
        } else {
668
            $on = $child->on;
669
        }
670
671 39
        $this->join($joinType, empty($child->getFrom()) ? $childTable : $child->getFrom(), $on);
672
673 39
        if (!empty($child->getWhere())) {
674 6
            $this->andWhere($child->getWhere());
675
        }
676
677 39
        if (!empty($child->getHaving())) {
678
            $this->andHaving($child->getHaving());
679
        }
680
681 39
        if (!empty($child->getOrderBy())) {
682 12
            $this->addOrderBy($child->getOrderBy());
683
        }
684
685 39
        if (!empty($child->getGroupBy())) {
686
            $this->addGroupBy($child->getGroupBy());
687
        }
688
689 39
        if (!empty($child->getParams())) {
690
            $this->addParams($child->getParams());
691
        }
692
693 39
        if (!empty($child->getJoin())) {
694 6
            foreach ($child->getJoin() as $join) {
695 6
                $this->join[] = $join;
0 ignored issues
show
Bug Best Practice introduced by
The property join does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
696
            }
697
        }
698
699 39
        if (!empty($child->getUnion())) {
700
            foreach ($child->getUnion() as $union) {
701
                $this->union[] = $union;
0 ignored issues
show
Bug Best Practice introduced by
The property union does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
702
            }
703
        }
704 39
    }
705
706
    /**
707
     * Sets the ON condition for a relational query.
708
     *
709
     * The condition will be used in the ON part when {@see ActiveQuery::joinWith()} is called.
710
     *
711
     * Otherwise, the condition will be used in the WHERE part of a query.
712
     *
713
     * Use this method to specify additional conditions when declaring a relation in the {@see ActiveRecord} class:
714
     *
715
     * ```php
716
     * public function getActiveUsers()
717
     * {
718
     *     return $this->hasMany(User::class, ['id' => 'user_id'])
719
     *                 ->onCondition(['active' => true]);
720
     * }
721
     * ```
722
     *
723
     * Note that this condition is applied in case of a join as well as when fetching the related records. This only
724
     * fields of the related table can be used in the condition. Trying to access fields of the primary record will
725
     * cause an error in a non-join-query.
726
     *
727
     * @param string|array $condition the ON condition. Please refer to {@see Query::where()} on how to specify this
728
     * parameter.
729
     * @param array $params the parameters (name => value) to be bound to the query.
730
     *
731
     * @return $this the query object itself
732
     */
733 21
    public function onCondition($condition, $params = []): self
734
    {
735 21
        $this->on = $condition;
736 21
        $this->addParams($params);
737
738 21
        return $this;
739
    }
740
741
    /**
742
     * Adds an additional ON condition to the existing one.
743
     *
744
     * The new condition and the existing one will be joined using the 'AND' operator.
745
     *
746
     * @param string|array $condition the new ON condition. Please refer to {@see where()} on how to specify this
747
     * parameter.
748
     * @param array $params the parameters (name => value) to be bound to the query.
749
     *
750
     * @return $this the query object itself.
751
     *
752
     * {@see onCondition()}
753
     * {@see orOnCondition()}
754
     */
755 6
    public function andOnCondition($condition, $params = []): self
756
    {
757 6
        if ($this->on === null) {
758 3
            $this->on = $condition;
759
        } else {
760 3
            $this->on = ['and', $this->on, $condition];
761
        }
762
763 6
        $this->addParams($params);
764
765 6
        return $this;
766
    }
767
768
    /**
769
     * Adds an additional ON condition to the existing one.
770
     *
771
     * The new condition and the existing one will be joined using the 'OR' operator.
772
     *
773
     * @param string|array $condition the new ON condition. Please refer to {@see where()} on how to specify this
774
     * parameter.
775
     * @param array $params the parameters (name => value) to be bound to the query.
776
     *
777
     * @return $this the query object itself.
778
     *
779
     * {@see onCondition()}
780
     * {@see andOnCondition()}
781
     */
782 6
    public function orOnCondition($condition, $params = []): self
783
    {
784 6
        if ($this->on === null) {
785 3
            $this->on = $condition;
786
        } else {
787 3
            $this->on = ['or', $this->on, $condition];
788
        }
789
790 6
        $this->addParams($params);
791
792 6
        return $this;
793
    }
794
795
    /**
796
     * Specifies the junction table for a relational query.
797
     *
798
     * Use this method to specify a junction table when declaring a relation in the {@see ActiveRecord} class:
799
     *
800
     * ```php
801
     * public function getItems()
802
     * {
803
     *     return $this->hasMany(Item::class, ['id' => 'item_id'])
804
     *                 ->viaTable('order_item', ['order_id' => 'id']);
805
     * }
806
     * ```
807
     *
808
     * @param string $tableName the name of the junction table.
809
     * @param array $link the link between the junction table and the table associated with {@see primaryModel}.
810
     * The keys of the array represent the columns in the junction table, and the values represent the columns
811
     * in the {@see primaryModel} table.
812
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
813
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
814
     *
815
     * @return $this the query object itself.
816
     *
817
     * {@see via()}
818
     */
819 24
    public function viaTable($tableName, $link, callable $callable = null): self
820
    {
821 24
        $modelClass = $this->primaryModel ? \get_class($this->primaryModel) : $this->modelClass;
822
823 24
        $relation = (new self($modelClass))->from([$tableName])->link($link)->multiple(true)->asArray(true);
824
825 24
        $this->via = $relation;
826
827 24
        if ($callable !== null) {
828 6
            $callable($relation);
829
        }
830
831 24
        return $this;
832
    }
833
834
    /**
835
     * Define an alias for the table defined in {@see modelClass}.
836
     *
837
     * This method will adjust {@see from} so that an already defined alias will be overwritten. If none was defined,
838
     * {@see from} will be populated with the given alias.
839
     *
840
     * @param string $alias the table alias.
841
     *
842
     * @return $this the query object itself.
843
     */
844 51
    public function alias($alias): self
845
    {
846 51
        if (empty($this->from) || \count($this->from) < 2) {
847 51
            [$tableName] = $this->getTableNameAndAlias();
848 51
            $this->from = [$alias => $tableName];
0 ignored issues
show
Bug Best Practice introduced by
The property from does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
849
        } else {
850 3
            $tableName = $this->getPrimaryTableName();
851
852 3
            foreach ($this->from as $key => $table) {
853 3
                if ($table === $tableName) {
854 3
                    unset($this->from[$key]);
855 3
                    $this->from[$alias] = $tableName;
856
                }
857
            }
858
        }
859
860 51
        return $this;
861
    }
862
863 120
    public function getTablesUsedInFrom(): array
864
    {
865 120
        if (empty($this->from)) {
866 69
            return $this->cleanUpTableNames([$this->getPrimaryTableName()]);
867
        }
868
869 51
        return parent::getTablesUsedInFrom();
870
    }
871
872 347
    protected function getPrimaryTableName(): string
873
    {
874
        /** @var $modelClass ActiveRecord */
875 347
        $modelClass = $this->modelClass;
876
877 347
        return $modelClass::tableName();
878
    }
879
880
    /**
881
     * @return string|array the join condition to be used when this query is used in a relational context.
882
     *
883
     * The condition will be used in the ON part when {@see ActiveQuery::joinWith()} is called. Otherwise, the condition
884
     * will be used in the WHERE part of a query.
885
     *
886
     * Please refer to {@see Query::where()} on how to specify this parameter.
887
     *
888
     * {@see onCondition()}
889
     */
890 33
    public function getOn()
891
    {
892 33
        return $this->on;
893
    }
894
895
    /**
896
     * @return array $value a list of relations that this query should be joined with
897
     */
898 117
    public function getJoinWith(): array
899
    {
900 117
        return $this->joinWith;
901
    }
902
903
    /**
904
     * @return string|null the SQL statement to be executed for retrieving AR records.
905
     *
906
     * This is set by {@see ActiveRecord::findBySql()}.
907
     */
908
    public function getSql(): ?string
909
    {
910
        return $this->sql;
911
    }
912
913 21
    public function getModelClass(): ?string
914
    {
915 21
        return $this->modelClass;
916
    }
917
918 9
    public function on($value): self
919
    {
920 9
        $this->on = $value;
921
922 9
        return $this;
923
    }
924
925 6
    public function sql(?string $value): self
926
    {
927 6
        $this->sql = $value;
928
929 6
        return $this;
930
    }
931
}
932