Completed
Branch develop (f7dc53)
by Anton
05:49
created

AbstractSelect::havingWrapper()   B

Complexity

Conditions 6
Paths 1

Size

Total Lines 25
Code Lines 11

Duplication

Lines 25
Ratio 100 %

Importance

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