Completed
Branch feature/pre-split (ca29cf)
by Anton
03:23
created

AbstractSelect::cache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 3
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
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\Database\Builders\QueryBuilder;
12
use Spiral\Database\Builders\Traits\JoinsTrait;
13
use Spiral\Database\Entities\PDOResult;
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\Pagination\PaginatorAwareInterface;
22
use Spiral\Pagination\Traits\LimitsTrait;
23
use Spiral\Pagination\Traits\PaginatorTrait;
24
25
/**
26
 * Prototype for select queries, include ability to cache, paginate or chunk results. Support WHERE,
27
 * JOIN, HAVING, ORDER BY, GROUP BY, UNION and DISTINCT statements. In addition only desired set
28
 * of columns can be selected. In addition select.
29
 *
30
 * @see AbstractWhere
31
 *
32
 * @method int avg($identifier) Perform aggregation (AVG) based on column or expression value.
33
 * @method int min($identifier) Perform aggregation (MIN) based on column or expression value.
34
 * @method int max($identifier) Perform aggregation (MAX) based on column or expression value.
35
 * @method int sum($identifier) Perform aggregation (SUM) based on column or expression value.
36
 */
37
abstract class AbstractSelect extends AbstractWhere implements
38
    \IteratorAggregate,
39
    PaginatorAwareInterface
40
{
41
    use JoinsTrait, LimitsTrait, PaginatorTrait;
42
43
    /**
44
     * Query type.
45
     */
46
    const QUERY_TYPE = QueryCompiler::SELECT_QUERY;
47
48
    /**
49
     * Sort directions.
50
     */
51
    const SORT_ASC  = 'ASC';
52
    const SORT_DESC = 'DESC';
53
54
    /**
55
     * Query must return only unique rows.
56
     *
57
     * @var bool|string
58
     */
59
    protected $distinct = false;
60
61
    /**
62
     * Columns or expressions to be fetched from database, can include aliases (AS).
63
     *
64
     * @var array
65
     */
66
    protected $columns = ['*'];
67
68
    /**
69
     * Set of generated having tokens, format must be supported by QueryCompilers.
70
     *
71
     * @see AbstractWhere
72
     *
73
     * @var array
74
     */
75
    protected $havingTokens = [];
76
77
    /**
78
     * Parameters collected while generating HAVING tokens, must be in a same order as parameters
79
     * in resulted query.
80
     *
81
     * @see AbstractWhere
82
     *
83
     * @var array
84
     */
85
    protected $havingParameters = [];
86
87
    /**
88
     * Columns/expression associated with their sort direction (ASK|DESC).
89
     *
90
     * @var array
91
     */
92
    protected $ordering = [];
93
94
    /**
95
     * Columns/expressions to group by.
96
     *
97
     * @var array
98
     */
99
    protected $grouping = [];
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 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...
105
    {
106
        $compiler = $compiler ?? $this->compiler;
107
108
        return $this->flattenParameters(
109
            $compiler->orderParameters(
110
                self::QUERY_TYPE,
111
                $this->whereParameters,
112
                $this->onParameters,
113
                $this->havingParameters
114
            )
115
        );
116
    }
117
118
    /**
119
     * Mark query to return only distinct results.
120
     *
121
     * @param bool|string $distinct You are only allowed to use string value for Postgres databases.
122
     *
123
     * @return self|$this
124
     */
125
    public function distinct($distinct = true): AbstractSelect
126
    {
127
        $this->distinct = $distinct;
128
129
        return $this;
130
    }
131
132
    /**
133
     * Simple HAVING condition with various set of arguments.
134
     *
135
     * @see AbstractWhere
136
     *
137
     * @param string|mixed $identifier Column or expression.
138
     * @param mixed        $variousA   Operator or value.
139
     * @param mixed        $variousB   Value, if operator specified.
140
     * @param mixed        $variousC   Required only in between statements.
141
     *
142
     * @return self|$this
143
     *
144
     * @throws BuilderException
145
     */
146 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...
147
        $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...
148
        $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...
149
        $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...
150
        $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...
151
    ): AbstractSelect {
152
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
153
154
        return $this;
155
    }
156
157
    /**
158
     * Simple AND HAVING condition with various set of arguments.
159
     *
160
     * @see AbstractWhere
161
     *
162
     * @param string|mixed $identifier Column or expression.
163
     * @param mixed        $variousA   Operator or value.
164
     * @param mixed        $variousB   Value, if operator specified.
165
     * @param mixed        $variousC   Required only in between statements.
166
     *
167
     * @return self|$this
168
     *
169
     * @throws BuilderException
170
     */
171 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...
172
        $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...
173
        $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...
174
        $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...
175
        $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...
176
    ): AbstractSelect {
177
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
178
179
        return $this;
180
    }
181
182
    /**
183
     * Simple OR HAVING condition with various set of arguments.
184
     *
185
     * @see AbstractWhere
186
     *
187
     * @param string|mixed $identifier Column or expression.
188
     * @param mixed        $variousA   Operator or value.
189
     * @param mixed        $variousB   Value, if operator specified.
190
     * @param mixed        $variousC   Required only in between statements.
191
     *
192
     * @return self|$this
193
     *
194
     * @throws BuilderException
195
     */
196 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...
197
        $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...
198
        $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...
199
        $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...
200
        $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...
201
    ): AbstractSelect {
202
        $this->whereToken('OR', func_get_args(), $this->havingTokens, $this->havingWrapper());
203
204
        return $this;
205
    }
206
207
    /**
208
     * Sort result by column/expression. You can apply multiple sortings to query via calling method
209
     * few times or by specifying values using array of sort parameters:.
210
     *
211
     * $select->orderBy([
212
     *      'id'   => SelectQuery::SORT_DESC,
213
     *      'name' => SelectQuery::SORT_ASC
214
     * ]);
215
     *
216
     * @param string|array $expression
217
     * @param string       $direction Sorting direction, ASC|DESC.
218
     *
219
     * @return self|$this
220
     */
221
    public function orderBy($expression, $direction = self::SORT_ASC): AbstractSelect
222
    {
223
        if (!is_array($expression)) {
224
            $this->ordering[] = [$expression, $direction];
225
226
            return $this;
227
        }
228
229
        foreach ($expression as $nested => $direction) {
230
            $this->ordering[] = [$nested, $direction];
231
        }
232
233
        return $this;
234
    }
235
236
    /**
237
     * Column or expression to group query by.
238
     *
239
     * @param string $expression
240
     *
241
     * @return self|$this
242
     */
243
    public function groupBy($expression): AbstractSelect
244
    {
245
        $this->grouping[] = $expression;
246
247
        return $this;
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     *
253
     * @param bool $paginate Apply pagination to result, can be disabled in honor of count method.
254
     *
255
     * @return PDOResult
256
     */
257
    public function run(bool $paginate = true)
258
    {
259
        if ($paginate && $this->hasPaginator()) {
260
            /**
261
             * To prevent original select builder altering
262
             *
263
             * @var AbstractSelect $select
264
             */
265
            $select = clone $this;
266
267
            //Getting selection specific paginator
268
            $paginator = $this->configurePaginator($this->count());
269
270
            //We have to ensure that selection works inside given pagination window
271
            $select = $select->limit(min($this->getLimit(), $paginator->getLimit()));
272
273
            //Making sure that window is shifted
274
            $select = $select->offset($this->getOffset() + $paginator->getOffset());
275
276
            //No inner pagination
277
            return $select->run(false);
278
        }
279
280
        return $this->driver->query($this->sqlStatement(), $this->getParameters());
281
    }
282
283
    /**
284
     * Iterate thought result using smaller data chinks with defined size and walk function.
285
     *
286
     * Example:
287
     * $select->chunked(100, function(PDOResult $result, $offset, $count) {
288
     *      dump($result);
289
     * });
290
     *
291
     * You must return FALSE from walk function to stop chunking.
292
     *
293
     * @param int      $limit
294
     * @param callable $callback
295
     */
296
    public function runChunks(int $limit, callable $callback)
297
    {
298
        $count = $this->count();
299
300
        //To keep original query untouched
301
        $select = clone $this;
302
303
        $select->limit($limit);
304
305
        $offset = 0;
306
        while ($offset + $limit <= $count) {
307
            $result = call_user_func_array(
308
                $callback,
309
                [$select->offset($offset)->getIterator(), $offset, $count]
310
            );
311
312
            if ($result === false) {
313
                //Stop iteration
314
                return;
315
            }
316
317
            $offset += $limit;
318
        }
319
    }
320
321
    /**
322
     * {@inheritdoc}
323
     *
324
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored. Do
325
     * not count united queries, or queries in complex joins.
326
     *
327
     * @param string $column Column to count by (every column by default).
328
     *
329
     * @return int
330
     */
331
    public function count(string $column = '*'): int
332
    {
333
        /**
334
         * @var AbstractSelect $select
335
         */
336
        $select = clone $this;
337
        $select->columns = ["COUNT({$column})"];
338
        $select->ordering = [];
339
        $select->grouping = [];
340
341
        return (int)$select->run(false)->fetchColumn();
342
    }
343
344
    /**
345
     * {@inheritdoc}
346
     *
347
     * Shortcut to execute one of aggregation methods (AVG, MAX, MIN, SUM) using method name as
348
     * reference.
349
     *
350
     * Example:
351
     * echo $select->sum('user.balance');
352
     *
353
     * @param string $method
354
     * @param array  $arguments
355
     *
356
     * @return int
357
     *
358
     * @throws BuilderException
359
     * @throws QueryException
360
     */
361
    public function __call(string $method, array $arguments)
362
    {
363
        if (!in_array($method = strtoupper($method), ['AVG', 'MIN', 'MAX', 'SUM'])) {
364
            throw new BuilderException("Unknown method '{$method}' in '" . get_class($this) . "'");
365
        }
366
367
        if (!isset($arguments[0]) || count($arguments) > 1) {
368
            throw new BuilderException('Aggregation methods can support exactly one column');
369
        }
370
371
        /**
372
         * @var AbstractSelect $select
373
         */
374
        $select = clone $this;
375
        $select->columns = ["{$method}({$arguments[0]})"];
376
377
        return (int)$this->run(false)->fetchColumn();
378
    }
379
380
    /**
381
     * {@inheritdoc}
382
     *
383
     * @return \PDOStatement|PDOResult
384
     */
385
    public function getIterator()
386
    {
387
        return $this->run();
388
    }
389
390
    /**
391
     * Applied to every potential parameter while having tokens generation.
392
     *
393
     * @return \Closure
394
     */
395 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...
396
    {
397
        return function ($parameter) {
398
            if ($parameter instanceof FragmentInterface) {
399
400
                //We are only not creating bindings for plan fragments
401
                if (!$parameter instanceof ParameterInterface && !$parameter instanceof QueryBuilder) {
402
                    return $parameter;
403
                }
404
            }
405
406
            if (is_array($parameter)) {
407
                throw new BuilderException('Arrays must be wrapped with Parameter instance');
408
            }
409
410
            //Wrapping all values with ParameterInterface
411
            if (!$parameter instanceof ParameterInterface && !$parameter instanceof ExpressionInterface) {
412
                $parameter = new Parameter($parameter, Parameter::DETECT_TYPE);
413
            };
414
415
            //Let's store to sent to driver when needed
416
            $this->havingParameters[] = $parameter;
417
418
            return $parameter;
419
        };
420
    }
421
}
422