Completed
Branch feature/split-orm (60a911)
by Anton
03:15
created

AbstractSelect   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 442
Duplicated Lines 15.61 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
dl 69
loc 442
rs 10
c 0
b 0
f 0
wmc 30
lcom 2
cbo 9

14 Methods

Rating   Name   Duplication   Size   Complexity  
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 count() 0 12 1
A __call() 0 18 4
A getIterator() 0 4 1
A getParameters() 13 13 1
A cache() 0 12 1
B run() 0 36 4
B runChunks() 0 24 3
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
    PaginatorAwareInterface
42
{
43
    use JoinsTrait, LimitsTrait, PaginatorTrait;
44
45
    /**
46
     * Query type.
47
     */
48
    const QUERY_TYPE = QueryCompiler::SELECT_QUERY;
49
50
    /**
51
     * Sort directions.
52
     */
53
    const SORT_ASC  = 'ASC';
54
    const SORT_DESC = 'DESC';
55
56
    /**
57
     * Query must return only unique rows.
58
     *
59
     * @var bool|string
60
     */
61
    protected $distinct = false;
62
63
    /**
64
     * Columns or expressions to be fetched from database, can include aliases (AS).
65
     *
66
     * @var array
67
     */
68
    protected $columns = ['*'];
69
70
    /**
71
     * Set of generated having tokens, format must be supported by QueryCompilers.
72
     *
73
     * @see AbstractWhere
74
     *
75
     * @var array
76
     */
77
    protected $havingTokens = [];
78
79
    /**
80
     * Parameters collected while generating HAVING tokens, must be in a same order as parameters
81
     * in resulted query.
82
     *
83
     * @see AbstractWhere
84
     *
85
     * @var array
86
     */
87
    protected $havingParameters = [];
88
89
    /**
90
     * Columns/expression associated with their sort direction (ASK|DESC).
91
     *
92
     * @var array
93
     */
94
    protected $ordering = [];
95
96
    /**
97
     * Columns/expressions to group by.
98
     *
99
     * @var array
100
     */
101
    protected $grouping = [];
102
103
    /**
104
     * Associated cache store.
105
     *
106
     * @var StoreInterface
107
     */
108
    protected $cacheStore = null;
109
110
    /**
111
     * Cache lifetime in seconds.
112
     *
113
     * @var int
114
     */
115
    protected $cacheLifetime = 0;
116
117
    /**
118
     * User specified cache key (optional).
119
     *
120
     * @var string
121
     */
122
    protected $cacheKey = '';
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 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...
128
    {
129
        $compiler = $compiler ?? $this->compiler;
130
131
        return $this->flattenParameters(
132
            $compiler->orderParameters(
133
                self::QUERY_TYPE,
134
                $this->whereParameters,
135
                $this->onParameters,
136
                $this->havingParameters
137
            )
138
        );
139
    }
140
141
    /**
142
     * Mark query to return only distinct results.
143
     *
144
     * @param bool|string $distinct You are only allowed to use string value for Postgres databases.
145
     *
146
     * @return self|$this
147
     */
148
    public function distinct($distinct = true): AbstractSelect
149
    {
150
        $this->distinct = $distinct;
151
152
        return $this;
153
    }
154
155
    /**
156
     * Simple HAVING condition with various set of arguments.
157
     *
158
     * @see AbstractWhere
159
     *
160
     * @param string|mixed $identifier Column or expression.
161
     * @param mixed        $variousA   Operator or value.
162
     * @param mixed        $variousB   Value, if operator specified.
163
     * @param mixed        $variousC   Required only in between statements.
164
     *
165
     * @return self|$this
166
     *
167
     * @throws BuilderException
168
     */
169 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...
170
        $identifier,
0 ignored issues
show
Unused Code introduced by
The parameter $identifier is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
171
        $variousA = null,
0 ignored issues
show
Unused Code introduced by
The parameter $variousA is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
172
        $variousB = null,
0 ignored issues
show
Unused Code introduced by
The parameter $variousB is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
173
        $variousC = null
0 ignored issues
show
Unused Code introduced by
The parameter $variousC is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
174
    ): AbstractSelect {
175
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
176
177
        return $this;
178
    }
179
180
    /**
181
     * Simple AND HAVING condition with various set of arguments.
182
     *
183
     * @see AbstractWhere
184
     *
185
     * @param string|mixed $identifier Column or expression.
186
     * @param mixed        $variousA   Operator or value.
187
     * @param mixed        $variousB   Value, if operator specified.
188
     * @param mixed        $variousC   Required only in between statements.
189
     *
190
     * @return self|$this
191
     *
192
     * @throws BuilderException
193
     */
194 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...
195
        $identifier,
0 ignored issues
show
Unused Code introduced by
The parameter $identifier is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
196
        $variousA = null,
0 ignored issues
show
Unused Code introduced by
The parameter $variousA is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
197
        $variousB = null,
0 ignored issues
show
Unused Code introduced by
The parameter $variousB is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
198
        $variousC = null
0 ignored issues
show
Unused Code introduced by
The parameter $variousC is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
199
    ): AbstractSelect {
200
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
201
202
        return $this;
203
    }
204
205
    /**
206
     * Simple OR HAVING condition with various set of arguments.
207
     *
208
     * @see AbstractWhere
209
     *
210
     * @param string|mixed $identifier Column or expression.
211
     * @param mixed        $variousA   Operator or value.
212
     * @param mixed        $variousB   Value, if operator specified.
213
     * @param mixed        $variousC   Required only in between statements.
214
     *
215
     * @return self|$this
216
     *
217
     * @throws BuilderException
218
     */
219 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...
220
        $identifier,
0 ignored issues
show
Unused Code introduced by
The parameter $identifier is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
221
        $variousA = [],
0 ignored issues
show
Unused Code introduced by
The parameter $variousA is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
222
        $variousB = null,
0 ignored issues
show
Unused Code introduced by
The parameter $variousB is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
223
        $variousC = null
0 ignored issues
show
Unused Code introduced by
The parameter $variousC is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
224
    ): AbstractSelect {
225
        $this->whereToken('OR', func_get_args(), $this->havingTokens, $this->havingWrapper());
226
227
        return $this;
228
    }
229
230
    /**
231
     * Sort result by column/expression. You can apply multiple sortings to query via calling method
232
     * few times or by specifying values using array of sort parameters:.
233
     *
234
     * $select->orderBy([
235
     *      'id'   => SelectQuery::SORT_DESC,
236
     *      'name' => SelectQuery::SORT_ASC
237
     * ]);
238
     *
239
     * @param string|array $expression
240
     * @param string       $direction Sorting direction, ASC|DESC.
241
     *
242
     * @return self|$this
243
     */
244
    public function orderBy($expression, $direction = self::SORT_ASC): AbstractSelect
245
    {
246
        if (!is_array($expression)) {
247
            $this->ordering[] = [$expression, $direction];
248
249
            return $this;
250
        }
251
252
        foreach ($expression as $nested => $direction) {
253
            $this->ordering[] = [$nested, $direction];
254
        }
255
256
        return $this;
257
    }
258
259
    /**
260
     * Column or expression to group query by.
261
     *
262
     * @param string $expression
263
     *
264
     * @return self|$this
265
     */
266
    public function groupBy($expression): AbstractSelect
267
    {
268
        $this->grouping[] = $expression;
269
270
        return $this;
271
    }
272
273
    /**
274
     * Mark selection as cached one, result will be passed thought database->cached() method and
275
     * will be stored in cache storage for specified amount of seconds.
276
     *
277
     * @see Database::cached()
278
     *
279
     * @param int            $lifetime Cache lifetime in seconds.
280
     * @param string         $key      Optional, Database will generate key based on query.
281
     * @param StoreInterface $store    Optional, Database will resolve cache store using container.
282
     *
283
     * @return self|$this
284
     */
285
    public function cache(
286
        int $lifetime,
287
        string $key = '',
288
        StoreInterface $store = null
289
    ): AbstractSelect {
290
291
        $this->cacheLifetime = $lifetime;
292
        $this->cacheKey = $key;
293
        $this->cacheStore = $store;
294
295
        return $this;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     *
301
     * @param bool $paginate Apply pagination to result, can be disabled in honor of count method.
302
     *
303
     * @return PDOResult|CachedResult
304
     */
305
    public function run(bool $paginate = true)
306
    {
307
        if ($paginate && $this->hasPaginator()) {
308
            /**
309
             * To prevent original select builder altering
310
             *
311
             * @var AbstractSelect $select
312
             */
313
            $select = clone $this;
314
315
            //Getting selection specific paginator
316
            $paginator = $this->configurePaginator($this->count());
317
318
            //We have to ensure that selection works inside given pagination window
319
            $select = $select->limit(min($this->getLimit(), $paginator->getLimit()));
320
321
            //Making sure that window is shifted
322
            $select = $select->offset($this->getOffset() + $paginator->getOffset());
323
324
            //No inner pagination
325
            return $select->run(false);
326
        }
327
328
        if (!empty($this->cacheLifetime)) {
329
            //Cached query
330
            return $this->driver->cachedQuery(
331
                $this->sqlStatement(),
332
                $this->getParameters(),
333
                $this->cacheLifetime,
334
                $this->cacheKey,
335
                $this->cacheStore
336
            );
337
        }
338
339
        return $this->driver->query($this->sqlStatement(), $this->getParameters());
340
    }
341
342
    /**
343
     * Iterate thought result using smaller data chinks with defined size and walk function.
344
     *
345
     * Example:
346
     * $select->chunked(100, function(PDOResult $result, $offset, $count) {
347
     *      dump($result);
348
     * });
349
     *
350
     * You must return FALSE from walk function to stop chunking.
351
     *
352
     * @param int      $limit
353
     * @param callable $callback
354
     */
355
    public function runChunks(int $limit, callable $callback)
356
    {
357
        $count = $this->count();
358
359
        //To keep original query untouched
360
        $select = clone $this;
361
362
        $select->limit($limit);
363
364
        $offset = 0;
365
        while ($offset + $limit <= $count) {
366
            $result = call_user_func_array(
367
                $callback,
368
                [$select->offset($offset)->getIterator(), $offset, $count]
369
            );
370
371
            if ($result === false) {
372
                //Stop iteration
373
                return;
374
            }
375
376
            $offset += $limit;
377
        }
378
    }
379
380
    /**
381
     * {@inheritdoc}
382
     *
383
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored. Do
384
     * not count united queries, or queries in complex joins.
385
     *
386
     * @param string $column Column to count by (every column by default).
387
     *
388
     * @return int
389
     */
390
    public function count(string $column = '*'): int
391
    {
392
        /**
393
         * @var AbstractSelect $select
394
         */
395
        $select = clone $this;
396
        $select->columns = ["COUNT({$column})"];
397
        $select->ordering = [];
398
        $select->grouping = [];
399
400
        return (int)$select->run(false)->fetchColumn();
401
    }
402
403
    /**
404
     * {@inheritdoc}
405
     *
406
     * Shortcut to execute one of aggregation methods (AVG, MAX, MIN, SUM) using method name as
407
     * reference.
408
     *
409
     * Example:
410
     * echo $select->sum('user.balance');
411
     *
412
     * @param string $method
413
     * @param array  $arguments
414
     *
415
     * @return int
416
     *
417
     * @throws BuilderException
418
     * @throws QueryException
419
     */
420
    public function __call(string $method, array $arguments)
421
    {
422
        if (!in_array($method = strtoupper($method), ['AVG', 'MIN', 'MAX', 'SUM'])) {
423
            throw new BuilderException("Unknown method '{$method}' in '" . get_class($this) . "'");
424
        }
425
426
        if (!isset($arguments[0]) || count($arguments) > 1) {
427
            throw new BuilderException('Aggregation methods can support exactly one column');
428
        }
429
430
        /**
431
         * @var AbstractSelect $select
432
         */
433
        $select = clone $this;
434
        $select->columns = ["{$method}({$arguments[0]})"];
435
436
        return (int)$this->run(false)->fetchColumn();
437
    }
438
439
    /**
440
     * {@inheritdoc}
441
     *
442
     * @return \PDOStatement|PDOResult
443
     */
444
    public function getIterator()
445
    {
446
        return $this->run();
447
    }
448
449
    /**
450
     * Applied to every potential parameter while having tokens generation.
451
     *
452
     * @return \Closure
453
     */
454 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...
455
    {
456
        return function ($parameter) {
457
            if ($parameter instanceof FragmentInterface) {
458
459
                //We are only not creating bindings for plan fragments
460
                if (!$parameter instanceof ParameterInterface && !$parameter instanceof QueryBuilder) {
461
                    return $parameter;
462
                }
463
            }
464
465
            if (is_array($parameter)) {
466
                throw new BuilderException('Arrays must be wrapped with Parameter instance');
467
            }
468
469
            //Wrapping all values with ParameterInterface
470
            if (!$parameter instanceof ParameterInterface && !$parameter instanceof ExpressionInterface) {
471
                $parameter = new Parameter($parameter, Parameter::DETECT_TYPE);
472
            };
473
474
            //Let's store to sent to driver when needed
475
            $this->havingParameters[] = $parameter;
476
477
            return $parameter;
478
        };
479
    }
480
}
481