Completed
Branch develop (c2aa4c)
by Anton
05:17
created

AbstractSelect::groupBy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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