Completed
Branch feature/pre-split (b025f2)
by Anton
03:30
created

AbstractSelect   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 451
Duplicated Lines 15.3 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
dl 69
loc 451
rs 9.8
c 0
b 0
f 0
wmc 31
lcom 2
cbo 9

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getParameters() 13 13 1
A distinct() 0 6 1
A having() 10 10 1
A andHaving() 10 10 1
A orHaving() 10 10 1
A orderBy() 0 14 3
A groupBy() 0 6 1
A cache() 0 12 1
B run() 0 36 4
B runChunks() 0 24 3
A count() 0 12 1
A __call() 0 18 4
A getIterator() 0 4 1
A jsonSerialize() 0 4 1
C havingWrapper() 26 26 7

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Database\Builders\Prototypes;
10
11
use Spiral\Cache\StoreInterface;
12
use Spiral\Database\Builders\QueryBuilder;
13
use Spiral\Database\Builders\Traits\JoinsTrait;
14
use Spiral\Database\Entities\QueryCompiler;
15
use Spiral\Database\Exceptions\BuilderException;
16
use Spiral\Database\Exceptions\QueryException;
17
use Spiral\Database\Injections\ExpressionInterface;
18
use Spiral\Database\Injections\FragmentInterface;
19
use Spiral\Database\Injections\Parameter;
20
use Spiral\Database\Injections\ParameterInterface;
21
use Spiral\Database\Entities\Query\CachedResult;
22
use Spiral\Database\Entities\Query\PDOResult;
23
use Spiral\Pagination\PaginatorAwareInterface;
24
use Spiral\Pagination\Traits\LimitsTrait;
25
use Spiral\Pagination\Traits\PaginatorTrait;
26
27
/**
28
 * Prototype for select queries, include ability to cache, paginate or chunk results. Support WHERE,
29
 * JOIN, HAVING, ORDER BY, GROUP BY, UNION and DISTINCT statements. In addition only desired set
30
 * of columns can be selected. In addition select.
31
 *
32
 * @see AbstractWhere
33
 *
34
 * @method int avg($identifier) Perform aggregation (AVG) based on column or expression value.
35
 * @method int min($identifier) Perform aggregation (MIN) based on column or expression value.
36
 * @method int max($identifier) Perform aggregation (MAX) based on column or expression value.
37
 * @method int sum($identifier) Perform aggregation (SUM) based on column or expression value.
38
 */
39
abstract class AbstractSelect extends AbstractWhere implements
40
    \IteratorAggregate,
41
    \JsonSerializable,
42
    PaginatorAwareInterface
43
{
44
    use JoinsTrait, LimitsTrait, PaginatorTrait;
45
46
    /**
47
     * Query type.
48
     */
49
    const QUERY_TYPE = QueryCompiler::SELECT_QUERY;
50
51
    /**
52
     * Sort directions.
53
     */
54
    const SORT_ASC  = 'ASC';
55
    const SORT_DESC = 'DESC';
56
57
    /**
58
     * Query must return only unique rows.
59
     *
60
     * @var bool|string
61
     */
62
    protected $distinct = false;
63
64
    /**
65
     * Columns or expressions to be fetched from database, can include aliases (AS).
66
     *
67
     * @var array
68
     */
69
    protected $columns = ['*'];
70
71
    /**
72
     * Set of generated having tokens, format must be supported by QueryCompilers.
73
     *
74
     * @see AbstractWhere
75
     *
76
     * @var array
77
     */
78
    protected $havingTokens = [];
79
80
    /**
81
     * Parameters collected while generating HAVING tokens, must be in a same order as parameters
82
     * in resulted query.
83
     *
84
     * @see AbstractWhere
85
     *
86
     * @var array
87
     */
88
    protected $havingParameters = [];
89
90
    /**
91
     * Columns/expression associated with their sort direction (ASK|DESC).
92
     *
93
     * @var array
94
     */
95
    protected $ordering = [];
96
97
    /**
98
     * Columns/expressions to group by.
99
     *
100
     * @var array
101
     */
102
    protected $grouping = [];
103
104
    /**
105
     * Associated cache store.
106
     *
107
     * @var StoreInterface
108
     */
109
    protected $cacheStore = null;
110
111
    /**
112
     * Cache lifetime in seconds.
113
     *
114
     * @var int
115
     */
116
    protected $cacheLifetime = 0;
117
118
    /**
119
     * User specified cache key (optional).
120
     *
121
     * @var string
122
     */
123
    protected $cacheKey = '';
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 View Code Duplication
    public function getParameters(QueryCompiler $compiler = null): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
129
    {
130
        $compiler = $compiler ?? $this->compiler;
131
132
        return $this->flattenParameters(
133
            $compiler->orderParameters(
134
                self::QUERY_TYPE,
135
                $this->whereParameters,
136
                $this->onParameters,
137
                $this->havingParameters
138
            )
139
        );
140
    }
141
142
    /**
143
     * Mark query to return only distinct results.
144
     *
145
     * @param bool|string $distinct You are only allowed to use string value for Postgres databases.
146
     *
147
     * @return self|$this
148
     */
149
    public function distinct($distinct = true): AbstractSelect
150
    {
151
        $this->distinct = $distinct;
152
153
        return $this;
154
    }
155
156
    /**
157
     * Simple HAVING condition with various set of arguments.
158
     *
159
     * @see AbstractWhere
160
     *
161
     * @param string|mixed $identifier Column or expression.
162
     * @param mixed        $variousA   Operator or value.
163
     * @param mixed        $variousB   Value, if operator specified.
164
     * @param mixed        $variousC   Required only in between statements.
165
     *
166
     * @return self|$this
167
     *
168
     * @throws BuilderException
169
     */
170 View Code Duplication
    public function having(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
171
        $identifier,
172
        $variousA = null,
173
        $variousB = null,
174
        $variousC = null
175
    ): AbstractSelect {
176
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
177
178
        return $this;
179
    }
180
181
    /**
182
     * Simple AND HAVING condition with various set of arguments.
183
     *
184
     * @see AbstractWhere
185
     *
186
     * @param string|mixed $identifier Column or expression.
187
     * @param mixed        $variousA   Operator or value.
188
     * @param mixed        $variousB   Value, if operator specified.
189
     * @param mixed        $variousC   Required only in between statements.
190
     *
191
     * @return self|$this
192
     *
193
     * @throws BuilderException
194
     */
195 View Code Duplication
    public function andHaving(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
196
        $identifier,
197
        $variousA = null,
198
        $variousB = null,
199
        $variousC = null
200
    ): AbstractSelect {
201
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
202
203
        return $this;
204
    }
205
206
    /**
207
     * Simple OR HAVING condition with various set of arguments.
208
     *
209
     * @see AbstractWhere
210
     *
211
     * @param string|mixed $identifier Column or expression.
212
     * @param mixed        $variousA   Operator or value.
213
     * @param mixed        $variousB   Value, if operator specified.
214
     * @param mixed        $variousC   Required only in between statements.
215
     *
216
     * @return self|$this
217
     *
218
     * @throws BuilderException
219
     */
220 View Code Duplication
    public function orHaving(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221
        $identifier,
222
        $variousA = [],
223
        $variousB = null,
224
        $variousC = null
225
    ): AbstractSelect {
226
        $this->whereToken('OR', func_get_args(), $this->havingTokens, $this->havingWrapper());
227
228
        return $this;
229
    }
230
231
    /**
232
     * Sort result by column/expression. You can apply multiple sortings to query via calling method
233
     * few times or by specifying values using array of sort parameters:.
234
     *
235
     * $select->orderBy([
236
     *      'id'   => SelectQuery::SORT_DESC,
237
     *      'name' => SelectQuery::SORT_ASC
238
     * ]);
239
     *
240
     * @param string|array $expression
241
     * @param string       $direction Sorting direction, ASC|DESC.
242
     *
243
     * @return self|$this
244
     */
245
    public function orderBy($expression, $direction = self::SORT_ASC): AbstractSelect
246
    {
247
        if (!is_array($expression)) {
248
            $this->ordering[] = [$expression, $direction];
249
250
            return $this;
251
        }
252
253
        foreach ($expression as $nested => $direction) {
254
            $this->ordering[] = [$nested, $direction];
255
        }
256
257
        return $this;
258
    }
259
260
    /**
261
     * Column or expression to group query by.
262
     *
263
     * @param string $expression
264
     *
265
     * @return self|$this
266
     */
267
    public function groupBy($expression): AbstractSelect
268
    {
269
        $this->grouping[] = $expression;
270
271
        return $this;
272
    }
273
274
    /**
275
     * Mark selection as cached one, result will be passed thought database->cached() method and
276
     * will be stored in cache storage for specified amount of seconds.
277
     *
278
     * @see Database::cached()
279
     *
280
     * @param int            $lifetime Cache lifetime in seconds.
281
     * @param string         $key      Optional, Database will generate key based on query.
282
     * @param StoreInterface $store    Optional, Database will resolve cache store using container.
283
     *
284
     * @return self|$this
285
     */
286
    public function cache(
287
        int $lifetime,
288
        string $key = '',
289
        StoreInterface $store = null
290
    ): AbstractSelect {
291
292
        $this->cacheLifetime = $lifetime;
293
        $this->cacheKey = $key;
294
        $this->cacheStore = $store;
295
296
        return $this;
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     *
302
     * @param bool $paginate Apply pagination to result, can be disabled in honor of count method.
303
     *
304
     * @return PDOResult|CachedResult
305
     */
306
    public function run(bool $paginate = true)
307
    {
308
        if ($paginate && $this->hasPaginator()) {
309
            /**
310
             * To prevent original select builder altering
311
             *
312
             * @var AbstractSelect $select
313
             */
314
            $select = clone $this;
315
316
            //Getting selection specific paginator
317
            $paginator = $this->configurePaginator($this->count());
318
319
            //We have to ensure that selection works inside given pagination window
320
            $select = $select->limit(min($this->getLimit(), $paginator->getLimit()));
321
322
            //Making sure that window is shifted
323
            $select = $select->offset($this->getOffset() + $paginator->getOffset());
324
325
            //No inner pagination
326
            return $select->run(false);
327
        }
328
329
        if (!empty($this->cacheLifetime)) {
330
            //Cached query
331
            return $this->driver->cachedQuery(
332
                $this->sqlStatement(),
333
                $this->getParameters(),
334
                $this->cacheLifetime,
335
                $this->cacheKey,
336
                $this->cacheStore
337
            );
338
        }
339
340
        return $this->driver->query($this->sqlStatement(), $this->getParameters());
341
    }
342
343
    /**
344
     * Iterate thought result using smaller data chinks with defined size and walk function.
345
     *
346
     * Example:
347
     * $select->chunked(100, function(PDOResult $result, $offset, $count) {
348
     *      dump($result);
349
     * });
350
     *
351
     * You must return FALSE from walk function to stop chunking.
352
     *
353
     * @param int      $limit
354
     * @param callable $callback
355
     */
356
    public function runChunks(int $limit, callable $callback)
357
    {
358
        $count = $this->count();
359
360
        //To keep original query untouched
361
        $select = clone $this;
362
363
        $select->limit($limit);
364
365
        $offset = 0;
366
        while ($offset + $limit <= $count) {
367
            $result = call_user_func_array(
368
                $callback,
369
                [$select->offset($offset)->getIterator(), $offset, $count]
370
            );
371
372
            if ($result === false) {
373
                //Stop iteration
374
                return;
375
            }
376
377
            $offset += $limit;
378
        }
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     *
384
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored. Do
385
     * not count united queries, or queries in complex joins.
386
     *
387
     * @param string $column Column to count by (every column by default).
388
     *
389
     * @return int
390
     */
391
    public function count(string $column = '*'): int
392
    {
393
        /**
394
         * @var AbstractSelect $select
395
         */
396
        $select = clone $this;
397
        $select->columns = ["COUNT({$column})"];
398
        $select->ordering = [];
399
        $select->grouping = [];
400
401
        return (int)$select->run(false)->fetchColumn();
402
    }
403
404
    /**
405
     * {@inheritdoc}
406
     *
407
     * Shortcut to execute one of aggregation methods (AVG, MAX, MIN, SUM) using method name as
408
     * reference.
409
     *
410
     * Example:
411
     * echo $select->sum('user.balance');
412
     *
413
     * @param string $method
414
     * @param array  $arguments
415
     *
416
     * @return int
417
     *
418
     * @throws BuilderException
419
     * @throws QueryException
420
     */
421
    public function __call(string $method, array $arguments)
422
    {
423
        if (!in_array($method = strtoupper($method), ['AVG', 'MIN', 'MAX', 'SUM'])) {
424
            throw new BuilderException("Unknown method '{$method}' in '" . get_class($this) . "'");
0 ignored issues
show
Unused Code introduced by
The call to BuilderException::__construct() has too many arguments starting with "Unknown method '{$metho...get_class($this) . '\''.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
425
        }
426
427
        if (!isset($arguments[0]) || count($arguments) > 1) {
428
            throw new BuilderException('Aggregation methods can support exactly one column');
0 ignored issues
show
Unused Code introduced by
The call to BuilderException::__construct() has too many arguments starting with 'Aggregation methods can...ort exactly one column'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
429
        }
430
431
        /**
432
         * @var AbstractSelect $select
433
         */
434
        $select = clone $this;
435
        $select->columns = ["{$method}({$arguments[0]})"];
436
437
        return (int)$this->run(false)->fetchColumn();
438
    }
439
440
    /**
441
     * {@inheritdoc}
442
     *
443
     * @return \PDOStatement|PDOResult
444
     */
445
    public function getIterator()
446
    {
447
        return $this->run();
448
    }
449
450
    /**
451
     * {@inheritdoc}
452
     */
453
    public function jsonSerialize()
454
    {
455
        return $this->getIterator()->jsonSerialize();
456
    }
457
458
    /**
459
     * Applied to every potential parameter while having tokens generation.
460
     *
461
     * @return \Closure
462
     */
463 View Code Duplication
    private function havingWrapper()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
464
    {
465
        return function ($parameter) {
466
            if ($parameter instanceof FragmentInterface) {
467
468
                //We are only not creating bindings for plan fragments
469
                if (!$parameter instanceof ParameterInterface && !$parameter instanceof QueryBuilder) {
470
                    return $parameter;
471
                }
472
            }
473
474
            if (is_array($parameter)) {
475
                throw new BuilderException('Arrays must be wrapped with Parameter instance');
0 ignored issues
show
Unused Code introduced by
The call to BuilderException::__construct() has too many arguments starting with 'Arrays must be wrapped with Parameter instance'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
476
            }
477
478
            //Wrapping all values with ParameterInterface
479
            if (!$parameter instanceof ParameterInterface && !$parameter instanceof ExpressionInterface) {
480
                $parameter = new Parameter($parameter, Parameter::DETECT_TYPE);
481
            };
482
483
            //Let's store to sent to driver when needed
484
            $this->havingParameters[] = $parameter;
485
486
            return $parameter;
487
        };
488
    }
489
}
490