Passed
Push — master ( 91edba...d01bf5 )
by Wilmer
02:58
created

ActiveQuery::average()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord\Redis;
6
7
use JsonException;
8
use ReflectionException;
9
use Yiisoft\ActiveRecord\ActiveQuery as BaseActiveQuery;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\InvalidParamException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
15
use function array_keys;
16
use function arsort;
17
use function asort;
18
use function count;
19
use function get_class;
20
use function in_array;
21
use function is_array;
22
use function is_numeric;
23
use function is_string;
24
use function key;
25
use function reset;
26
27
/**
28
 * ActiveQuery represents a query associated with an Active Record class.
29
 *
30
 * An ActiveQuery can be a normal query or be used in a relational context.
31
 *
32
 * ActiveQuery instances are usually created by {@see ActiveRecord::find()}.
33
 *
34
 * Relational queries are created by {@see ActiveRecord::hasOne()} and {@see ActiveRecord::hasMany()}.
35
 *
36
 * Normal Query
37
 * ------------
38
 *
39
 * ActiveQuery mainly provides the following methods to retrieve the query results:
40
 *
41
 * - {@see one()}: returns a single record populated with the first row of data.
42
 * - {@see all()}: returns all records based on the query results.
43
 * - {@see count()}: returns the number of records.
44
 * - {@see sum()}: returns the sum over the specified column.
45
 * - {@see average()}: returns the average over the specified column.
46
 * - {@see min()}: returns the min over the specified column.
47
 * - {@see max()}: returns the max over the specified column.
48
 * - {@see scalar()}: returns the value of the first column in the first row of the query result.
49
 * - {@see exists()}: returns a value indicating whether the query result has data or not.
50
 *
51
 * You can use query methods, such as {@see where()}, {@see limit()} and {@see orderBy()} to customize the query
52
 * options.
53
 *
54
 * ActiveQuery also provides the following additional query options:
55
 *
56
 * - {@see with()}: list of relations that this query should be performed with.
57
 * - {@see indexBy()}: the name of the column by which the query result should be indexed.
58
 * - {@see asArray()}: whether to return each record as an array.
59
 *
60
 * These options can be configured using methods of the same name. For example:
61
 *
62
 * ```php
63
 * $customers = Customer::find()->with('orders')->asArray()->all();
64
 * ```
65
 *
66
 * Relational query
67
 * ----------------
68
 *
69
 * In relational context ActiveQuery represents a relation between two Active Record classes.
70
 *
71
 * Relational ActiveQuery instances are usually created by calling {@see ActiveRecord::hasOne()} and
72
 * {@see ActiveRecord::hasMany()}. An Active Record class declares a relation by defining
73
 * a getter method which calls one of the above methods and returns the created ActiveQuery object.
74
 *
75
 * A relation is specified by {@see link} which represents the association between columns of different tables; and the
76
 * multiplicity of the relation is indicated by {@see multiple}.
77
 *
78
 * If a relation involves a junction table, it may be specified by {@see via()}.
79
 *
80
 * This methods may only be called in a relational context. Same is true for {@see inverseOf()}, which
81
 * marks a relation as inverse of another relation.
82
 */
83
class ActiveQuery extends BaseActiveQuery
84
{
85
    private string $attribute;
86
    private ?LuaScriptBuilder $luaScriptBuilder = null;
87
88
    /**
89
     * Executes the query and returns all results as an array.
90
     *
91
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
92
     * @throws NotSupportedException
93
     *
94
     * @return array the query results. If the query results in nothing, an empty array will be returned.
95
     */
96 43
    public function all(): array
97
    {
98 43
        $indexBy = $this->getIndexBy();
99
100 43
        if ($this->shouldEmulateExecution()) {
101 2
            return [];
102
        }
103
104
        /** support for orderBy */
105 42
        $data = $this->executeScript('All');
106
107 42
        if (empty($data)) {
108 12
            return [];
109
        }
110
111 39
        $rows = [];
112
113 39
        foreach ($data as $dataRow) {
114 39
            $row = [];
115 39
            $c = count($dataRow);
116 39
            for ($i = 0; $i < $c;) {
117 39
                $row[$dataRow[$i++]] = $dataRow[$i++];
118
            }
119
120 39
            $rows[] = $row;
121
        }
122
123 39
        if (empty($rows)) {
124
            return [];
125
        }
126
127 39
        $models = $this->createModels($rows);
128
129 39
        if (!empty($this->getWith())) {
130 7
            $this->findWith($this->getWith(), $models);
131
        }
132
133 39
        if ($indexBy !== null) {
134 3
            $indexedModels = [];
135 3
            if (is_string($indexBy)) {
136 3
                foreach ($models as $model) {
137 3
                    $key = $model[$indexBy];
138 3
                    $indexedModels[$key] = $model;
139
                }
140
            } else {
141
                foreach ($models as $model) {
142
                    $key = $this->indexBy($model);
143
                    $indexedModels[$key] = $model;
144
                }
145
            }
146 3
            $models = $indexedModels;
147
        }
148
149 39
        return $models;
150
    }
151
152
    /**
153
     * Executes the query and returns a single row of result.
154
     *
155
     * Null will be returned, if the query results in nothing.
156
     *
157
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
158
     * @throws NotSupportedException
159
     *
160
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of {@see asArray}, the
161
     * query result may be either an array or an ActiveRecord object.
162
     */
163 48
    public function one()
164
    {
165 48
        if ($this->shouldEmulateExecution()) {
166 1
            return null;
167
        }
168
169
        /** add support for orderBy */
170 47
        $data = $this->executeScript('One');
171
172 47
        if (empty($data)) {
173 15
            return null;
174
        }
175
176 41
        $row = [];
177
178 41
        $c = count($data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type boolean and string; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

178
        $c = count(/** @scrutinizer ignore-type */ $data);
Loading history...
179
180 41
        for ($i = 0; $i < $c;) {
181 41
            $row[$data[$i++]] = $data[$i++];
182
        }
183
184 41
        if ($this->isAsArray()) {
185 2
            $model = $row;
186
        } else {
187
            /** @var $class ActiveRecord */
188 40
            $class = $this->modelClass;
189
190 40
            $model = $class::instantiate($row);
191
192 40
            $class = get_class($model);
193
194 40
            $class::populateRecord($model, $row);
195
        }
196
197 41
        if (!empty($this->getWith())) {
198 3
            $models = [$model];
199
200 3
            $this->findWith($this->getWith(), $models);
201
202 3
            $model = $models[0];
203
        }
204
205 41
        return $model;
206
    }
207
208
    /**
209
     * Returns the number of records.
210
     *
211
     * @param string $q the COUNT expression. This parameter is ignored by this implementation.
212
     *
213
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
214
     * @throws NotSupportedException
215
     *
216
     * @return int number of records.
217
     */
218 16
    public function count(string $q = '*'): int
219
    {
220 16
        if ($this->shouldEmulateExecution()) {
221 1
            return 0;
222
        }
223
224 15
        if ($this->getWhere() === null) {
225
            /* @var $modelClass ActiveRecord */
226 13
            $modelClass = $this->modelClass;
227
228 13
            return (int) $this->modelClass::getConnection()->executeCommand('LLEN', [$modelClass::keyPrefix()]);
229
        }
230
231 5
        return (int) $this->executeScript('Count');
232
    }
233
234
    /**
235
     * Returns a value indicating whether the query result contains any row of data.
236
     *
237
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
238
     * @throws NotSupportedException
239
     *
240
     * @return bool whether the query result contains any row of data.
241
     */
242 3
    public function exists(): bool
243
    {
244 3
        if ($this->shouldEmulateExecution()) {
245 1
            return false;
246
        }
247
248 2
        return $this->one() !== null;
249
    }
250
251
    /**
252
     * Returns the number of records.
253
     *
254
     * @param string $column the column to sum up. If this parameter is not given, the `db` application component will
255
     * be used.
256
     *
257
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
258
     * @throws NotSupportedException
259
     *
260
     * @return int number of records.
261
     */
262 3
    public function sum(string $column): int
263
    {
264 3
        if ($this->shouldEmulateExecution()) {
265 1
            return 0;
266
        }
267
268 2
        return (int) $this->executeScript('Sum', !empty($column) ? $column : $this->attribute);
269
    }
270
271
    /**
272
     * Returns the average of the specified column values.
273
     *
274
     * @param string $column the column name or expression. Make sure you properly quote column names in the expression.
275
     *
276
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
277
     * @throws NotSupportedException
278
     *
279
     * @return int the average of the specified column values.
280
     */
281 3
    public function average(string $column): int
282
    {
283 3
        if ($this->shouldEmulateExecution()) {
284 1
            return 0;
285
        }
286
287 2
        return (int) $this->executeScript('Average', !empty($column) ? $column : $this->attribute);
288
    }
289
290
    /**
291
     * Returns the minimum of the specified column values.
292
     *
293
     * @param string $column the column name or expression. Make sure you properly quote column names in the expression.
294
     *
295
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
296
     * @throws NotSupportedException
297
     *
298
     * @return int the minimum of the specified column values.
299
     */
300 3
    public function min(string $column): ?int
301
    {
302 3
        if ($this->shouldEmulateExecution()) {
303 1
            return null;
304
        }
305
306 2
        return (int) $this->executeScript('Min', !empty($column) ? $column : $this->attribute);
307
    }
308
309
    /**
310
     * Returns the maximum of the specified column values.
311
     *
312
     * @param string $column the column name or expression. Make sure you properly quote column names in the expression.
313
     *
314
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
315
     * @throws NotSupportedException
316
     *
317
     * @return int the maximum of the specified column values.
318
     */
319 3
    public function max(string $column): ?int
320
    {
321 3
        if ($this->shouldEmulateExecution()) {
322 1
            return null;
323
        }
324
325 2
        return (int) $this->executeScript('Max', !empty($column) ? $column : $this->attribute);
326
    }
327
328
    /**
329
     * Executes a script created by {@see LuaScriptBuilder}.
330
     *
331
     * @param string $type the type of the script to generate
332
     * @param string|null $columnName
333
     *
334
     * @throws Exception|JsonException|InvalidConfigException|InvalidParamException|ReflectionException
335
     * @throws NotSupportedException
336
     *
337
     * @return array|bool|null|string
338
     */
339 68
    protected function executeScript(string $type, ?string $columnName = null)
340
    {
341 68
        if ($this->getPrimaryModel() !== null) {
342
            /** lazy loading */
343 15
            if ($this->getVia() instanceof self) {
344
                /** via junction table */
345
                $viaModels = $this->getVia()->findJunctionRows([$this->getPrimaryModel()]);
346
                $this->filterByModels($viaModels);
347 15
            } elseif (is_array($this->getVia())) {
348
                /**
349
                 * via relation
350
                 * @var $viaQuery ActiveQuery
351
                 */
352 9
                [$viaName, $viaQuery] = $this->getVia();
353
354 9
                if ($viaQuery->getMultiple()) {
355 9
                    $viaModels = $viaQuery->all();
356 9
                    $this->getPrimaryModel()->populateRelation($viaName, $viaModels);
357
                } else {
358
                    $model = $viaQuery->one();
359
                    $this->getPrimaryModel()->populateRelation($viaName, $model);
360
                    $viaModels = $model === null ? [] : [$model];
361
                }
362
363 9
                $this->filterByModels($viaModels);
364
            } else {
365 15
                $this->filterByModels([$this->getPrimaryModel()]);
366
            }
367
        }
368
369
        /** find by primary key if possible. This is much faster than scanning all records */
370
        if (
371 68
            is_array($this->getWhere()) &&
372
            (
373 62
                (!isset($this->getWhere()[0]) && $this->modelClass::isPrimaryKey(array_keys($this->getWhere()))) ||
374
                (
375 44
                    isset($this->getWhere()[0]) && $this->getWhere()[0] === 'in' &&
376 68
                    $this->modelClass::isPrimaryKey((array) $this->getWhere()[1])
377
                )
378
            )
379
        ) {
380 49
            return $this->findByPk($type, $columnName);
381
        }
382
383 50
        $method = 'build' . $type;
384
385 50
        $script = $this->getLuaScriptBuilder()->$method($this, $columnName);
386
387 50
        return $this->modelClass::getConnection()->executeCommand('EVAL', [$script, 0]);
388
    }
389
390
    /**
391
     * Fetch by pk if possible as this is much faster.
392
     *
393
     * @param string $type the type of the script to generate.
394
     * @param string|null $columnName
395
     *
396
     * @throws InvalidParamException|JsonException|NotSupportedException
397
     *
398
     * @return array|bool|null|string
399
     */
400 49
    private function findByPk(string $type, ?string $columnName = null)
401
    {
402 49
        $limit = $this->getLimit();
403 49
        $offset = $this->getOffset();
404 49
        $orderBy = $this->getOrderBy();
405 49
        $needSort = !empty($orderBy) && in_array($type, ['All', 'One', 'Column']);
406 49
        $where = $this->getWhere();
407
408 49
        if ($needSort) {
409 9
            if (!is_array($orderBy) || count($orderBy) > 1) {
410
                throw new NotSupportedException(
411
                    'orderBy by multiple columns is not currently supported by redis ActiveRecord.'
412
                );
413
            }
414
415 9
            $k = key($orderBy);
416 9
            $v = $orderBy[$k];
417
418 9
            if (is_numeric($k)) {
419
                $orderColumn = $v;
420
                $orderType = SORT_ASC;
421
            } else {
422 9
                $orderColumn = $k;
423 9
                $orderType = $v;
424
            }
425
        }
426
427 49
        if (isset($where[0]) && $where[0] === 'in') {
428 18
            $pks = (array) $where[2];
429 40
        } elseif (count($where) == 1) {
430 37
            $pks = (array) reset($where);
431
        } else {
432 5
            foreach ($where as $values) {
433 5
                if (is_array($values)) {
434
                    /** support composite IN for composite PK */
435
                    throw new NotSupportedException('Find by composite PK is not supported by redis ActiveRecord.');
436
                }
437
            }
438 5
            $pks = [$where];
439
        }
440
441
        /** @var $modelClass ActiveRecord */
442 49
        $modelClass = $this->modelClass;
443
444 49
        if ($type === 'Count') {
445 2
            $start = 0;
446 2
            $limit = null;
447
        } else {
448 47
            $start = ($offset === null || $offset < 0) ? 0 : $offset;
449 47
            $limit = ($limit < 0) ? null : $limit;
450
        }
451
452 49
        $i = 0;
453 49
        $data = [];
454 49
        $orderArray = [];
455
456 49
        foreach ($pks as $pk) {
457 48
            if (++$i > $start && ($limit === null || $i <= $start + $limit)) {
458 48
                $key = $modelClass::keyPrefix() . ':a:' . $modelClass::buildKey($pk);
459 48
                $result = $this->modelClass::getConnection()->executeCommand('HGETALL', [$key]);
460 48
                if (!empty($result)) {
461 47
                    $data[] = $result;
462 47
                    if ($needSort) {
463 9
                        $orderArray[] = $this->modelClass::getConnection()->executeCommand(
464 9
                            'HGET',
465 9
                            [$key, $orderColumn]
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $orderColumn does not seem to be defined for all execution paths leading up to this point.
Loading history...
466
                        );
467
                    }
468 47
                    if ($type === 'One' && $orderBy === null) {
469
                        break;
470
                    }
471
                }
472
            }
473
        }
474
475 49
        if ($needSort) {
476 9
            $resultData = [];
477
478 9
            if ($orderType === SORT_ASC) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $orderType does not seem to be defined for all execution paths leading up to this point.
Loading history...
479 9
                asort($orderArray, SORT_NATURAL);
480
            } else {
481
                arsort($orderArray, SORT_NATURAL);
482
            }
483
484 9
            foreach ($orderArray as $orderKey => $orderItem) {
485 9
                $resultData[] = $data[$orderKey];
486
            }
487
488 9
            $data = $resultData;
489
        }
490
491
        switch ($type) {
492 49
            case 'All':
493 25
                return $data;
494 41
            case 'One':
495 39
                return reset($data);
496 2
            case 'Count':
497 2
                return count($data);
498
            case 'Column':
499
                $column = [];
500
                foreach ($data as $dataRow) {
501
                    $row = [];
502
                    $c = count($dataRow);
503
                    for ($i = 0; $i < $c;) {
504
                        $row[$dataRow[$i++]] = $dataRow[$i++];
505
                    }
506
                    $column[] = $row[$columnName];
507
                }
508
509
                return $column;
510
            case 'Sum':
511
                $sum = 0;
512
                foreach ($data as $dataRow) {
513
                    $c = count($dataRow);
514
                    for ($i = 0; $i < $c;) {
515
                        if ($dataRow[$i++] == $columnName) {
516
                            $sum += $dataRow[$i];
517
                            break;
518
                        }
519
                    }
520
                }
521
522
                return $sum;
523
            case 'Average':
524
                $sum = 0;
525
                $count = 0;
526
                foreach ($data as $dataRow) {
527
                    $count++;
528
                    $c = count($dataRow);
529
                    for ($i = 0; $i < $c;) {
530
                        if ($dataRow[$i++] == $columnName) {
531
                            $sum += $dataRow[$i];
532
                            break;
533
                        }
534
                    }
535
                }
536
537
                return $sum / $count;
538
            case 'Min':
539
                $min = null;
540
                foreach ($data as $dataRow) {
541
                    $c = count($dataRow);
542
                    for ($i = 0; $i < $c;) {
543
                        if ($dataRow[$i++] == $columnName && ($min == null || $dataRow[$i] < $min)) {
544
                            $min = $dataRow[$i];
545
                            break;
546
                        }
547
                    }
548
                }
549
550
                return $min;
551
            case 'Max':
552
                $max = null;
553
                foreach ($data as $dataRow) {
554
                    $c = count($dataRow);
555
                    for ($i = 0; $i < $c;) {
556
                        if ($dataRow[$i++] == $columnName && ($max == null || $dataRow[$i] > $max)) {
557
                            $max = $dataRow[$i];
558
                            break;
559
                        }
560
                    }
561
                }
562
563
                return $max;
564
        }
565
566
        throw new InvalidParamException('Unknown fetch type: ' . $type);
567
    }
568
569
    /**
570
     * Executes the query and returns the first column of the result.
571
     *
572
     * @throws Exception|InvalidConfigException|InvalidParamException|ReflectionException|NotSupportedException
573
     *
574
     * @return array the first column of the query result. An empty array is returned if the query results in nothing.
575
     */
576 2
    public function column(): array
577
    {
578 2
        if ($this->shouldEmulateExecution()) {
579 1
            return [];
580
        }
581
582
        /** TODO add support for orderBy */
583 1
        return $this->executeScript('Column', $this->attribute);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->executeScr...umn', $this->attribute) could return the type boolean|null|string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
584
    }
585
586
    /**
587
     * Returns the query result as a scalar value.
588
     *
589
     * The value returned will be the specified attribute in the first record of the query results.
590
     *
591
     * @throws Exception|InvalidConfigException|InvalidParamException|ReflectionException|NotSupportedException
592
     *
593
     * @return string|null the value of the specified attribute in the first record of the query result. Null is
594
     * returned if the query result is empty.
595
     */
596 3
    public function scalar(): ?string
597
    {
598 3
        if ($this->shouldEmulateExecution()) {
599 1
            return null;
600
        }
601
602 2
        $record = $this->one();
603
604 2
        if ($record !== null) {
605 2
            return $record->hasAttribute($this->attribute) ? $record->getAttribute($this->attribute) : null;
606
        }
607
608
        return null;
609
    }
610
611 5
    public function withAttribute(string $value): self
612
    {
613 5
        $this->attribute = $value;
614
615 5
        return $this;
616
    }
617
618 50
    public function getLuaScriptBuilder(): LuaScriptBuilder
619
    {
620 50
        if ($this->luaScriptBuilder === null) {
621 50
            $this->luaScriptBuilder = new LuaScriptBuilder();
622
        }
623
624 50
        return $this->luaScriptBuilder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->luaScriptBuilder could return the type null which is incompatible with the type-hinted return Yiisoft\ActiveRecord\Redis\LuaScriptBuilder. Consider adding an additional type-check to rule them out.
Loading history...
625
    }
626
}
627