Passed
Push — master ( 6a4462...5500dc )
by y
01:44
created

Select::getFirst()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Helix\DB;
4
5
use Closure;
6
use Countable;
7
use Generator;
8
use Helix\DB;
9
use Helix\DB\SQL\ExpressionInterface;
10
use Helix\DB\SQL\Predicate;
11
use IteratorAggregate;
12
13
/**
14
 * Represents a `SELECT` query.
15
 */
16
class Select extends AbstractTable implements Countable, IteratorAggregate, ExpressionInterface {
17
18
    /**
19
     * Compiled column list.
20
     *
21
     * @internal
22
     * @var string
23
     */
24
    protected $_columns = '';
25
26
    /**
27
     * Compiled column list.
28
     *
29
     * @internal
30
     * @var string
31
     */
32
    protected $_group = '';
33
34
    /**
35
     * Compiled predicate chain.
36
     *
37
     * @internal
38
     * @var string
39
     */
40
    protected $_having = '';
41
42
    /**
43
     * Compiled join chain.
44
     *
45
     * @internal
46
     * @var string
47
     */
48
    protected $_join = '';
49
50
    /**
51
     * Compiled limit and offset.
52
     *
53
     * @internal
54
     * @var string
55
     */
56
    protected $_limit = '';
57
58
    /**
59
     * Compiled column list.
60
     *
61
     * @internal
62
     * @var string
63
     */
64
    protected $_order = '';
65
66
    /**
67
     * Compiled predicate chain.
68
     *
69
     * @internal
70
     * @var string
71
     */
72
    protected $_where = '';
73
74
    /**
75
     * Human-readable alias.
76
     * This is initialized by using `uniqid()` and the table's name.
77
     *
78
     * @var string
79
     */
80
    protected $alias;
81
82
    /**
83
     * A callback to yield each result.
84
     *
85
     * @var Closure `(Statement $statement):Generator`
86
     */
87
    protected $fetcher;
88
89
    /**
90
     * Identifiers that can be used as a column by an outer query,
91
     * keyed ordinally and associatively.
92
     *
93
     * In order to be referable, selected expressions must be aliased, or `Column` instances.
94
     *
95
     * @var string[]
96
     */
97
    protected $referable = [];
98
99
    /**
100
     * @var string
101
     */
102
    protected $table;
103
104
    /**
105
     * @param DB $db
106
     * @param string|Select $table
107
     * @param string[] $columns
108
     */
109
    public function __construct (DB $db, $table, array $columns) {
110
        parent::__construct($db);
111
        if ($table instanceof Select) {
112
            $this->table = $table->toSubquery();
113
            $this->alias = uniqid('_') . "_{$table->alias}";
114
        }
115
        else {
116
            $this->table = (string)$table;
117
            $this->alias = uniqid('_') . "__{$table}";
118
        }
119
        $this->setColumns($columns);
120
        $this->fetcher = function(Statement $statement) {
121
            yield from $statement;
122
        };
123
    }
124
125
    /**
126
     * @param array $args
127
     * @return Statement
128
     */
129
    public function __invoke (array $args = []) {
130
        return $this->execute($args);
131
    }
132
133
    /**
134
     * Returns the alias.
135
     *
136
     * @return string
137
     */
138
    final public function __toString () {
139
        return $this->alias;
140
    }
141
142
    /**
143
     * Clones the instance and selects `COUNT(*)`, using the given execution arguments.
144
     *
145
     * @param array $args Execution arguments.
146
     * @return int
147
     */
148
    public function count (array $args = []): int {
149
        $clone = clone $this;
150
        $clone->_columns = 'COUNT(*)';
151
        $clone->_order = '';
152
        return (int)$clone->execute($args)->fetchColumn();
153
    }
154
155
    /**
156
     * Executes the select, preparing a statement first if arguments are used.
157
     *
158
     * @param array $args
159
     * @return Statement
160
     */
161
    public function execute (array $args = []) {
162
        if (empty($args)) {
163
            return $this->db->query($this->toSql());
164
        }
165
        return $this->prepare()->__invoke($args);
166
    }
167
168
    /**
169
     * Executes and fetches all results.
170
     *
171
     * @see fetcher
172
     *
173
     * @param array $args Execution arguments.
174
     * @return array
175
     */
176
    public function getAll (array $args = []): array {
177
        return iterator_to_array($this->fetcher->__invoke($this->execute($args)));
178
    }
179
180
    /**
181
     * Executes and yields from the fetcher.
182
     * This is preferable over `fetchAll()` for iterating large result sets.
183
     *
184
     * @see fetcher
185
     *
186
     * @param array $args Execution arguments.
187
     * @return Generator
188
     */
189
    public function getEach (array $args = []) {
190
        yield from $this->fetcher->__invoke($this->execute($args));
191
    }
192
193
    /**
194
     * Executes and returns from the fetcher.
195
     *
196
     * @see fetcher
197
     *
198
     * @param array $args
199
     * @return mixed
200
     */
201
    public function getFirst (array $args = []) {
202
        return $this->getEach($args)->current();
203
    }
204
205
    /**
206
     * Executes without arguments and yields from the fetcher.
207
     *
208
     * @see fetcher
209
     *
210
     * @return Generator
211
     */
212
    public function getIterator () {
213
        yield from $this->getEach();
214
    }
215
216
    /**
217
     * Adds a column to the `GROUP BY` clause.
218
     *
219
     * @param string $column
220
     * @return $this
221
     */
222
    public function group (string $column) {
223
        if (!strlen($this->_group)) {
224
            $this->_group = " GROUP BY {$column}";
225
        }
226
        else {
227
            $this->_group .= ", {$column}";
228
        }
229
        return $this;
230
    }
231
232
    /**
233
     * Adds a condition to the `HAVING` clause.
234
     *
235
     * @param string $condition
236
     * @return $this
237
     */
238
    public function having (string $condition) {
239
        if (!strlen($this->_having)) {
240
            $this->_having = " HAVING {$condition}";
241
        }
242
        else {
243
            $this->_having .= " AND {$condition}";
244
        }
245
        return $this;
246
    }
247
248
    /**
249
     * `EXISTS (SELECT ...)`
250
     *
251
     * @return Predicate
252
     */
253
    public function isCorrelated () {
254
        return new Predicate("EXISTS ({$this->toSql()})");
255
    }
256
257
    /**
258
     * `NOT EXISTS (SELECT ...)`
259
     *
260
     * @return Predicate
261
     */
262
    public function isNotCorrelated () {
263
        return new Predicate("NOT EXISTS ({$this->toSql()})");
264
    }
265
266
    /**
267
     * Adds a `JOIN` clause.
268
     *
269
     * @param string|Select $table
270
     * @param string $condition
271
     * @param string $type
272
     * @return $this
273
     */
274
    public function join ($table, string $condition, string $type = 'INNER') {
275
        if ($table instanceof Select) {
276
            $table = $table->toSubquery();
277
        }
278
        $this->_join .= " {$type} JOIN {$table} ON {$condition}";
279
        return $this;
280
    }
281
282
    /**
283
     * Sets the `LIMIT` clause.
284
     *
285
     * @param int $limit
286
     * @param int $offset
287
     * @return $this
288
     */
289
    public function limit (int $limit, int $offset = 0) {
290
        if ($limit == 0) {
291
            $this->_limit = '';
292
        }
293
        else {
294
            $this->_limit = " LIMIT {$limit}";
295
            if ($offset > 1) {
296
                $this->_limit .= " OFFSET {$offset}";
297
            }
298
        }
299
        return $this;
300
    }
301
302
    /**
303
     * Whether a selected column can be referenced by an outer query.
304
     *
305
     * @param mixed $offset Ordinal or reference name.
306
     * @return bool
307
     */
308
    public function offsetExists ($offset): bool {
309
        return isset($this->referable[$offset]);
310
    }
311
312
    /**
313
     * Returns a reference {@link Column}, qualified by the instance's alias.
314
     *
315
     * @param mixed $offset Ordinal or reference name.
316
     * @return Column
317
     */
318
    public function offsetGet ($offset) {
319
        return new Column($this->db, $this->referable[$offset], $this->alias);
320
    }
321
322
    /**
323
     * Sets the `ORDER BY` clause.
324
     *
325
     * @param string $order
326
     * @return $this
327
     */
328
    public function order (string $order) {
329
        if (strlen($order)) {
330
            $order = " ORDER BY {$order}";
331
        }
332
        $this->_order = $order;
333
        return $this;
334
    }
335
336
    /**
337
     * @return Statement
338
     */
339
    public function prepare () {
340
        return $this->db->prepare($this->toSql());
341
    }
342
343
    /**
344
     * @param string $alias
345
     * @return $this
346
     */
347
    public function setAlias (string $alias) {
348
        $this->alias = $alias;
349
        return $this;
350
    }
351
352
    /**
353
     * @param string[] $columns
354
     * @return $this
355
     */
356
    public function setColumns (array $columns) {
357
        $this->referable = [];
358
        $_columns = [];
359
        $i = 0;
360
        foreach ($columns as $alias => $column) {
361
            $name = $alias;
362
            if ($column instanceof Column) {
363
                $name = $column->getName();
364
                if (is_int($alias)) {
365
                    $alias = $name;
366
                }
367
            }
368
            if ($name !== $alias) {
369
                $_columns[] = "{$column} AS {$alias}";
370
            }
371
            else {
372
                $_columns[] = "{$column}";
373
            }
374
            if (is_string($name)) {
375
                $this->referable[$name] = $name;
376
                $this->referable[$i] = $name;
377
            }
378
            $i++;
379
        }
380
        $this->_columns = implode(', ', $_columns);
381
        return $this;
382
    }
383
384
    /**
385
     * @param Closure $fetcher
386
     * @return $this
387
     */
388
    public function setFetcher (Closure $fetcher) {
389
        $this->fetcher = $fetcher;
390
        return $this;
391
    }
392
393
    /**
394
     * `SELECT ...`
395
     *
396
     * @return string
397
     */
398
    public function toSql (): string {
399
        $sql = "SELECT {$this->_columns} FROM {$this->table}";
400
        $sql .= $this->_join;
401
        $sql .= $this->_where;
402
        $sql .= $this->_group;
403
        $sql .= $this->_having;
404
        $sql .= $this->_order;
405
        $sql .= $this->_limit;
406
        return $sql;
407
    }
408
409
    /**
410
     * `(SELECT ...) AS ALIAS`
411
     *
412
     * @return string
413
     */
414
    public function toSubquery (): string {
415
        return "({$this->toSql()}) AS {$this->alias}";
416
    }
417
418
    /**
419
     * Adds a condition to the `WHERE` clause.
420
     *
421
     * @param string $condition
422
     * @return $this
423
     */
424
    public function where (string $condition) {
425
        if (!strlen($this->_where)) {
426
            $this->_where = " WHERE {$condition}";
427
        }
428
        else {
429
            $this->_where .= " AND {$condition}";
430
        }
431
        return $this;
432
    }
433
}