Completed
Branch develop (85a9c8)
by Anton
05:44
created

AbstractSelect::chunked()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 9.2
cc 3
eloc 12
nc 3
nop 2
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Database\Builders\Prototypes;
9
10
use Spiral\Cache\StoreInterface;
11
use Spiral\Database\Builders\Traits\JoinsTrait;
12
use Spiral\Database\Entities\QueryBuilder;
13
use Spiral\Database\Entities\QueryCompiler;
14
use Spiral\Database\Exceptions\BuilderException;
15
use Spiral\Database\Exceptions\QueryException;
16
use Spiral\Database\Injections\ExpressionInterface;
17
use Spiral\Database\Injections\FragmentInterface;
18
use Spiral\Database\Injections\Parameter;
19
use Spiral\Database\Injections\ParameterInterface;
20
use Spiral\Database\Query\CachedResult;
21
use Spiral\Database\Query\QueryResult;
22
use Spiral\Pagination\PaginableInterface;
23
use Spiral\Pagination\PaginatorAwareInterface;
24
use Spiral\Pagination\Traits\PaginatorTrait;
25
26
/**
27
 * Prototype for select queries, include ability to cache, paginate or chunk results. Support WHERE,
28
 * JOIN, HAVING, ORDER BY, GROUP BY, UNION and DISTINCT statements. In addition only desired set
29
 * of columns can be selected. In addition select
30
 *
31
 * @see AbstractWhere
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
    PaginableInterface,
40
    PaginatorAwareInterface,
41
    \JsonSerializable
42
{
43
    /**
44
     * Abstract select query must fully support joins and be paginable.
45
     */
46
    use JoinsTrait, PaginatorTrait;
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
     * @var array
73
     */
74
    protected $havingTokens = [];
75
76
    /**
77
     * Parameters collected while generating HAVING tokens, must be in a same order as parameters
78
     * in resulted query.
79
     *
80
     * @see AbstractWhere
81
     * @var array
82
     */
83
    protected $havingParameters = [];
84
85
    /**
86
     * Columns/expression associated with their sort direction (ASK|DESC).
87
     *
88
     * @var array
89
     */
90
    protected $ordering = [];
91
92
    /**
93
     * Columns/expressions to group by.
94
     *
95
     * @var array
96
     */
97
    protected $grouping = [];
98
99
    /**
100
     * Associated cache store.
101
     *
102
     * @var StoreInterface
103
     */
104
    protected $cacheStore = null;
105
106
    /**
107
     * Cache lifetime in seconds.
108
     *
109
     * @var int
110
     */
111
    protected $cacheLifetime = 0;
112
113
    /**
114
     * User specified cache key (optional).
115
     *
116
     * @var string
117
     */
118
    protected $cacheKey = '';
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 View Code Duplication
    public function getParameters(QueryCompiler $compiler = null)
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...
124
    {
125
        if (empty($compiler)) {
126
            //Using associated compiler
127
            $compiler = $this->compiler;
128
        }
129
130
        return $this->flattenParameters(
131
            $compiler->orderParameters(
132
                QueryCompiler::SELECT_QUERY,
133
                $this->whereParameters,
134
                $this->onParameters,
135
                $this->havingParameters
136
            )
137
        );
138
    }
139
140
    /**
141
     * Mark query to return only distinct results.
142
     *
143
     * @param bool|string $distinct You are only allowed to use string value for Postgres databases.
144
     * @return $this
145
     */
146
    public function distinct($distinct = true)
147
    {
148
        $this->distinct = $distinct;
149
150
        return $this;
151
    }
152
153
    /**
154
     * Simple HAVING condition with various set of arguments.
155
     *
156
     * @see AbstractWhere
157
     * @param string|mixed $identifier Column or expression.
158
     * @param mixed        $variousA   Operator or value.
159
     * @param mixed        $variousB   Value, if operator specified.
160
     * @param mixed        $variousC   Required only in between statements.
161
     * @return $this
162
     * @throws BuilderException
163
     */
164
    public function having($identifier, $variousA = null, $variousB = null, $variousC = null)
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...
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...
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...
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...
165
    {
166
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
167
168
        return $this;
169
    }
170
171
    /**
172
     * Simple AND HAVING condition with various set of arguments.
173
     *
174
     * @see AbstractWhere
175
     * @param string|mixed $identifier Column or expression.
176
     * @param mixed        $variousA   Operator or value.
177
     * @param mixed        $variousB   Value, if operator specified.
178
     * @param mixed        $variousC   Required only in between statements.
179
     * @return $this
180
     * @throws BuilderException
181
     */
182
    public function andHaving($identifier, $variousA = null, $variousB = null, $variousC = null)
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...
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...
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...
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...
183
    {
184
        $this->whereToken('AND', func_get_args(), $this->havingTokens, $this->havingWrapper());
185
186
        return $this;
187
    }
188
189
    /**
190
     * Simple OR HAVING condition with various set of arguments.
191
     *
192
     * @see AbstractWhere
193
     * @param string|mixed $identifier Column or expression.
194
     * @param mixed        $variousA   Operator or value.
195
     * @param mixed        $variousB   Value, if operator specified.
196
     * @param mixed        $variousC   Required only in between statements.
197
     * @return $this
198
     * @throws BuilderException
199
     */
200
    public function orHaving($identifier, $variousA = [], $variousB = null, $variousC = null)
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...
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...
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...
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
    {
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
     * @return $this
219
     */
220
    public function orderBy($expression, $direction = self::SORT_ASC)
221
    {
222
        if (!is_array($expression)) {
223
            $this->ordering[] = [$expression, $direction];
224
225
            return $this;
226
        }
227
228
        foreach ($expression as $nested => $direction) {
229
            $this->ordering[] = [$nested, $direction];
230
        }
231
232
        return $this;
233
    }
234
235
    /**
236
     * Column or expression to group query by.
237
     *
238
     * @param string $expression
239
     * @return $this
240
     */
241
    public function groupBy($expression)
242
    {
243
        $this->grouping[] = $expression;
244
245
        return $this;
246
    }
247
248
    /**
249
     * Mark selection as cached one, result will be passed thought database->cached() method and
250
     * will be stored in cache storage for specified amount of seconds.
251
     *
252
     * @see Database::cached()
253
     * @param int            $lifetime Cache lifetime in seconds.
254
     * @param string         $key      Optional, Database will generate key based on query.
255
     * @param StoreInterface $store    Optional, Database will resolve cache store using container.
256
     * @return $this
257
     */
258
    public function cache($lifetime, $key = '', StoreInterface $store = null)
259
    {
260
        $this->cacheLifetime = $lifetime;
261
        $this->cacheKey = $key;
262
        $this->cacheStore = $store;
263
264
        return $this;
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     *
270
     * @param bool $paginate Apply pagination to result, can be disabled in honor of count method.
271
     * @return QueryResult|CachedResult
272
     */
273
    public function run($paginate = true)
274
    {
275
        $backup = [$this->limit, $this->offset];
276
277
        if ($paginate) {
278
            $this->applyPagination();
279
        } else {
280
            //We have to flush limit and offset values when pagination is not required.
281
            $this->limit = $this->offset = 0;
282
        }
283
284
        if (empty($this->cacheLifetime)) {
285
            $result = $this->database->query(
286
                $this->sqlStatement(),
287
                $this->getParameters()
288
            );
289
        } else {
290
            $result = $this->database->cached(
291
                $this->cacheLifetime,
292
                $this->sqlStatement(),
293
                $this->getParameters(),
294
                $this->cacheKey,
295
                $this->cacheStore
296
            );
297
        }
298
299
        //Restoring limit and offset values
300
        list($this->limit, $this->offset) = $backup;
301
302
        return $result;
303
    }
304
305
    /**
306
     * Iterate thought result using smaller data chinks with defined size and walk function.
307
     *
308
     * Example:
309
     * $select->chunked(100, function(QueryResult $result, $offset, $count) {
310
     *      dump($result);
311
     * });
312
     *
313
     * You must return FALSE from walk function to stop chunking.
314
     *
315
     * @param int      $limit
316
     * @param callable $callback
317
     */
318
    public function chunked($limit, callable $callback)
319
    {
320
        $count = $this->count();
321
322
        $this->limit($limit);
323
        $offset = 0;
324
325
        while ($offset + $limit <= $count) {
326
            $result = call_user_func_array($callback, [
327
                $this->offset($offset)->getIterator(),
328
                $offset,
329
                $count
330
            ]);
331
332
            if ($result === false) {
333
                //Stop iteration
334
                return;
335
            }
336
337
            $offset += $limit;
338
        }
339
    }
340
341
    /**
342
     * {@inheritdoc}
343
     *
344
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored. Do
345
     * not count united queries, or queries in complex joins.
346
     *
347
     * @param string $column Column to count by (every column by default).
348
     * @return int
349
     */
350
    public function count($column = '*')
351
    {
352
        $backup = [$this->columns, $this->ordering, $this->grouping, $this->limit, $this->offset];
353
        $this->columns = ["COUNT({$column})"];
354
355
        //Can not be used with COUNT()
356
        $this->ordering = $this->grouping = [];
357
        $this->limit = $this->offset = 0;
358
359
        $result = $this->run(false)->fetchColumn();
360
        list($this->columns, $this->ordering, $this->grouping, $this->limit, $this->offset) = $backup;
361
362
        return (int)$result;
363
    }
364
365
    /**
366
     * {@inheritdoc}
367
     *
368
     * Shortcut to execute one of aggregation methods (AVG, MAX, MIN, SUM) using method name as
369
     * reference.
370
     *
371
     * Example:
372
     * echo $select->sum('user.balance');
373
     *
374
     * @param string $method
375
     * @param string $arguments
376
     * @return int
377
     * @throws BuilderException
378
     * @throws QueryException
379
     */
380
    public function __call($method, $arguments)
381
    {
382
        if (!in_array($method = strtoupper($method), ['AVG', 'MIN', 'MAX', 'SUM'])) {
383
            throw new BuilderException("Unknown aggregation method '{$method}'.");
384
        }
385
386
        if (!isset($arguments[0]) || count($arguments) > 1) {
387
            throw new BuilderException("Aggregation methods can support exactly one column.");
388
        }
389
390
        $columns = $this->columns;
391
        $this->columns = ["{$method}({$arguments[0]})"];
392
        $result = $this->run(false)->fetchColumn();
393
        $this->columns = $columns;
394
395
        return (int)$result;
396
    }
397
398
    /**
399
     * {@inheritdoc}
400
     *
401
     * @return QueryResult
402
     */
403
    public function getIterator()
404
    {
405
        return $this->run();
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411
    public function jsonSerialize()
412
    {
413
        return $this->getIterator()->jsonSerialize();
414
    }
415
416
    /**
417
     * Applied to every potential parameter while having tokens generation.
418
     *
419
     * @return \Closure
420
     */
421 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...
422
    {
423
        return function ($parameter) {
424
            if ($parameter instanceof FragmentInterface) {
425
                //We are only not creating bindings for plan fragments
426
                if (!$parameter instanceof ParameterInterface && !$parameter instanceof QueryBuilder) {
427
                    return $parameter;
428
                }
429
            }
430
431
            if (is_array($parameter)) {
432
                throw new BuilderException("Arrays must be wrapped with Parameter instance.");
433
            }
434
435
            //Wrapping all values with ParameterInterface
436
            if (!$parameter instanceof ParameterInterface && !$parameter instanceof ExpressionInterface) {
437
                $parameter = new Parameter($parameter, Parameter::DETECT_TYPE);
438
            };
439
440
            //Let's store to sent to driver when needed
441
            $this->havingParameters[] = $parameter;
442
443
            return $parameter;
444
        };
445
    }
446
}