Completed
Branch feature/pre-split (ecea15)
by Anton
03:28
created

SelectQuery::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
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;
10
11
use Spiral\Database\Builders\Prototypes\AbstractSelect;
12
use Spiral\Database\Entities\Driver;
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\FragmentInterface;
18
19
/**
20
 * SelectQuery extends AbstractSelect with ability to specify selection tables and perform UNION
21
 * of multiple select queries.
22
 */
23
class SelectQuery extends AbstractSelect implements \JsonSerializable
24
{
25
    /**
26
     * Table names to select data from.
27
     *
28
     * @var array
29
     */
30
    protected $tables = [];
31
32
    /**
33
     * Select queries represented by sql fragments or query builders to be united. Stored as
34
     * [UNION TYPE, SELECT QUERY].
35
     *
36
     * @var array
37
     */
38
    protected $unionTokens = [];
39
40
    /**
41
     * {@inheritdoc}
42
     *
43
     * @param array $from    Initial set of table names.
44
     * @param array $columns Initial set of columns to fetch.
45
     */
46
    public function __construct(
47
        Driver $driver,
48
        QueryCompiler $compiler,
49
        array $from = [],
50
        array $columns = []
51
    ) {
52
        parent::__construct($driver, $compiler);
53
54
        $this->tables = $from;
55
        if (!empty($columns)) {
56
            $this->columns = $this->fetchIdentifiers($columns);
57
        }
58
    }
59
60
    /**
61
     * Set table names SELECT query should be performed for. Table names can be provided with
62
     * specified alias (AS construction).
63
     *
64
     * @param array|string|mixed $tables Array of names, comma separated string or set of
65
     *                                   parameters.
66
     *
67
     * @return self|$this
68
     */
69
    public function from($tables): SelectQuery
70
    {
71
        $this->tables = $this->fetchIdentifiers(func_get_args());
72
73
        return $this;
74
    }
75
76
    /**
77
     * Set columns should be fetched as result of SELECT query. Columns can be provided with
78
     * specified alias (AS construction).
79
     *
80
     * @param array|string|mixed $columns Array of names, comma separated string or set of
81
     *                                    parameters.
82
     *
83
     * @return self|$this
84
     */
85
    public function columns($columns): SelectQuery
86
    {
87
        $this->columns = $this->fetchIdentifiers(func_get_args());
88
89
        return $this;
90
    }
91
92
    /**
93
     * Alias for columns() method.
94
     *
95
     * @param array|string|mixed $columns Array of names, comma separated string or set of
96
     *                                    parameters.
97
     *
98
     * @return self|$this
99
     */
100
    public function select($columns): SelectQuery
101
    {
102
        $this->columns = $this->fetchIdentifiers(func_get_args());
103
104
        return $this;
105
    }
106
107
    /**
108
     * Add select query to be united with.
109
     *
110
     * @param FragmentInterface $query
111
     *
112
     * @return self|$this
113
     */
114
    public function union(FragmentInterface $query): SelectQuery
115
    {
116
        $this->unionTokens[] = ['', $query];
117
118
        return $this;
119
    }
120
121
    /**
122
     * Add select query to be united with. Duplicate values will be included in result.
123
     *
124
     * @param FragmentInterface $query
125
     *
126
     * @return self|$this
127
     */
128
    public function unionAll(FragmentInterface $query): SelectQuery
129
    {
130
        $this->unionTokens[] = ['ALL', $query];
131
132
        return $this;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function getParameters(QueryCompiler $compiler = null): array
139
    {
140
        $compiler = $compiler ?? $this->compiler;
141
142
        $parameters = parent::getParameters($compiler);
143
144
        //Unions always located at the end of query.
145
        foreach ($this->unionTokens as $union) {
146
            if ($union[0] instanceof QueryBuilder) {
147
                $parameters = array_merge($parameters, $union[0]->getParameters($compiler));
148
            }
149
        }
150
151
        return $parameters;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     *
157
     * @param bool $paginate Apply pagination to result, can be disabled in honor of count method.
158
     *
159
     * @return PDOResult
160
     */
161
    public function run(bool $paginate = true)
162
    {
163
        if ($paginate && $this->hasPaginator()) {
164
            /**
165
             * To prevent original select builder altering
166
             *
167
             * @var SelectQuery $select
168
             */
169
            $select = clone $this;
170
171
            //Getting selection specific paginator
172
            $paginator = $this->configurePaginator($this->count());
173
174
            //We have to ensure that selection works inside given pagination window
175
            $select = $select->limit(min($this->getLimit(), $paginator->getLimit()));
176
177
            //Making sure that window is shifted
178
            $select = $select->offset($this->getOffset() + $paginator->getOffset());
179
180
            //No inner pagination
181
            return $select->run(false);
182
        }
183
184
        return $this->driver->query($this->sqlStatement(), $this->getParameters());
185
    }
186
187
    /**
188
     * Iterate thought result using smaller data chinks with defined size and walk function.
189
     *
190
     * Example:
191
     * $select->chunked(100, function(PDOResult $result, $offset, $count) {
192
     *      dump($result);
193
     * });
194
     *
195
     * You must return FALSE from walk function to stop chunking.
196
     *
197
     * @param int      $limit
198
     * @param callable $callback
199
     */
200
    public function runChunks(int $limit, callable $callback)
201
    {
202
        $count = $this->count();
203
204
        //To keep original query untouched
205
        $select = clone $this;
206
207
        $select->limit($limit);
208
209
        $offset = 0;
210
        while ($offset + $limit <= $count) {
211
            $result = call_user_func_array(
212
                $callback,
213
                [$select->offset($offset)->getIterator(), $offset, $count]
214
            );
215
216
            if ($result === false) {
217
                //Stop iteration
218
                return;
219
            }
220
221
            $offset += $limit;
222
        }
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     *
228
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored. Do
229
     * not count united queries, or queries in complex joins.
230
     *
231
     * @param string $column Column to count by (every column by default).
232
     *
233
     * @return int
234
     */
235
    public function count(string $column = '*'): int
236
    {
237
        /**
238
         * @var AbstractSelect $select
239
         */
240
        $select = clone $this;
241
        $select->columns = ["COUNT({$column})"];
242
        $select->ordering = [];
243
        $select->grouping = [];
244
245
        return (int)$select->run(false)->fetchColumn();
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     *
251
     * Shortcut to execute one of aggregation methods (AVG, MAX, MIN, SUM) using method name as
252
     * reference.
253
     *
254
     * Example:
255
     * echo $select->sum('user.balance');
256
     *
257
     * @param string $method
258
     * @param array  $arguments
259
     *
260
     * @return int|float
261
     *
262
     * @throws BuilderException
263
     * @throws QueryException
264
     */
265
    public function __call(string $method, array $arguments)
266
    {
267
        if (!in_array($method = strtoupper($method), ['AVG', 'MIN', 'MAX', 'SUM'])) {
268
            throw new BuilderException("Unknown method '{$method}' in '" . get_class($this) . "'");
269
        }
270
271
        if (!isset($arguments[0]) || count($arguments) > 1) {
272
            throw new BuilderException('Aggregation methods can support exactly one column');
273
        }
274
275
        /**
276
         * @var AbstractSelect $select
277
         */
278
        $select = clone $this;
279
        $select->columns = ["{$method}({$arguments[0]})"];
280
281
        $result = $select->run(false)->fetchColumn();
282
283
        //Selecting type between int and float
284
        if ((float)$result == $result && (int)$result != $result) {
285
            //Find more elegant check
286
            return (float)$result;
287
        }
288
289
        return (int)$result;
290
    }
291
292
    /**
293
     * {@inheritdoc}
294
     *
295
     * @return \PDOStatement|PDOResult
296
     */
297
    public function getIterator()
298
    {
299
        return $this->run();
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305
    public function sqlStatement(QueryCompiler $compiler = null): string
306
    {
307
        if (empty($compiler)) {
308
            $compiler = $this->compiler->resetQuoter();
309
        }
310
311
        //11 parameters!
312
        return $compiler->compileSelect(
313
            $this->tables,
314
            $this->distinct,
315
            $this->columns,
316
            $this->joinTokens,
317
            $this->whereTokens,
318
            $this->havingTokens,
319
            $this->grouping,
320
            $this->ordering,
321
            $this->getLimit(),
322
            $this->getOffset(),
323
            $this->unionTokens
324
        );
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    public function jsonSerialize()
331
    {
332
        return $this->fetchAll();
333
    }
334
335
    /**
336
     * Request all results as array.
337
     *
338
     * @return array
339
     */
340
    public function fetchAll(): array
341
    {
342
        return $this->getIterator()->fetchAll(\PDO::FETCH_ASSOC);
343
    }
344
}
345