Passed
Push — master ( 305c34...2bac56 )
by y
01:32
created

Select::isNotEmpty()   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 0
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
     * @var string
20
     */
21
    protected $_columns = '';
22
23
    /**
24
     * @var string
25
     */
26
    protected $_group = '';
27
28
    /**
29
     * @var string
30
     */
31
    protected $_having = '';
32
33
    /**
34
     * @var string
35
     */
36
    protected $_join = '';
37
38
    /**
39
     * @var string
40
     */
41
    protected $_limit = '';
42
43
    /**
44
     * @var string
45
     */
46
    protected $_order = '';
47
48
    /**
49
     * @var string
50
     */
51
    protected $_where = '';
52
53
    /**
54
     * Human-friendly [uniqid()](https://php.net/uniqid)
55
     *
56
     * @var string
57
     */
58
    protected $alias;
59
60
    /**
61
     * A callback to yield each result.
62
     *
63
     * @var Closure `(Statement $statement):Generator`
64
     */
65
    protected $fetcher;
66
67
    /**
68
     * @var string
69
     */
70
    protected $table;
71
72
    /**
73
     * @param DB $db
74
     * @param string|Select $table
75
     * @param string[] $columns
76
     */
77
    public function __construct (DB $db, $table, array $columns) {
78
        parent::__construct($db);
79
        if ($table instanceof Select) {
80
            $table = $table->toSubquery();
81
        }
82
        $this->table = $table;
83
        $this->alias = uniqid('_') . "__{$table}";
84
        foreach ($columns as $alias => $name) {
85
            if (is_string($alias) and $name !== $alias) {
86
                $name = "{$name} AS {$alias}";
87
            }
88
            $columns[$alias] = $name;
89
        }
90
        $this->_columns = implode(', ', $columns);
91
        $this->fetcher = function(Statement $statement) {
92
            yield from $statement;
93
        };
94
    }
95
96
    /**
97
     * Gives the clone a new alias.
98
     */
99
    public function __clone () {
100
        $this->alias = uniqid('_') . "__{$this->table}";
101
    }
102
103
    /**
104
     * @param array $args
105
     * @return Statement
106
     */
107
    public function __invoke (array $args = []) {
108
        return $this->execute($args);
109
    }
110
111
    /**
112
     * Returns the alias.
113
     *
114
     * @return string
115
     */
116
    final public function __toString () {
117
        return $this->alias;
118
    }
119
120
    /**
121
     * Clones the instance and selects `COUNT(*)`, using the given execution arguments.
122
     *
123
     * @param array $args Execution arguments.
124
     * @return int
125
     */
126
    public function count (array $args = []): int {
127
        $clone = clone $this;
128
        $clone->_columns = 'COUNT(*)';
129
        return (int)$clone->execute($args)->fetchColumn();
130
    }
131
132
    /**
133
     * Executes the select, preparing a statement first if arguments are used.
134
     *
135
     * @param array $args
136
     * @return Statement
137
     */
138
    public function execute (array $args = []) {
139
        if (empty($args)) {
140
            return $this->db->query($this->toSql());
141
        }
142
        return $this->prepare()->__invoke($args);
143
    }
144
145
    /**
146
     * Executes and fetches all results.
147
     *
148
     * @see fetcher
149
     *
150
     * @param array $args Execution arguments.
151
     * @return array
152
     */
153
    public function fetchAll (array $args = []): array {
154
        return iterator_to_array($this->fetcher->__invoke($this->execute($args)));
155
    }
156
157
    /**
158
     * Executes and yields from the fetcher.
159
     * This is preferable over `fetchAll()` for iterating large result sets.
160
     *
161
     * @see fetcher
162
     *
163
     * @param array $args Execution arguments.
164
     * @return Generator
165
     */
166
    public function fetchEach (array $args = []) {
167
        yield from $this->fetcher->__invoke($this->execute($args));
168
    }
169
170
    /**
171
     * Executes without arguments and yields from the fetcher.
172
     *
173
     * @see fetcher
174
     *
175
     * @return Generator
176
     */
177
    public function getIterator () {
178
        yield from $this->fetchEach();
179
    }
180
181
    /**
182
     * Adds a column for grouping.
183
     *
184
     * @param string $column
185
     * @return $this
186
     */
187
    public function group (string $column) {
188
        if (!strlen($this->_group)) {
189
            $this->_group = " GROUP BY {$column}";
190
        }
191
        else {
192
            $this->_group .= ", {$column}";
193
        }
194
        return $this;
195
    }
196
197
    /**
198
     * Adds a condition for a grouped column.
199
     *
200
     * @param string $condition
201
     * @return $this
202
     */
203
    public function having (string $condition) {
204
        if (!strlen($this->_having)) {
205
            $this->_having = " HAVING {$condition}";
206
        }
207
        else {
208
            $this->_having .= " AND {$condition}";
209
        }
210
        return $this;
211
    }
212
213
    /**
214
     * `EXISTS (SELECT ...)`
215
     *
216
     * @return Predicate
217
     */
218
    public function isCorrelated () {
219
        return new Predicate("EXISTS ({$this->toSql()})");
220
    }
221
222
    /**
223
     * `NOT EXISTS (SELECT ...)`
224
     *
225
     * @return Predicate
226
     */
227
    public function isNotCorrelated () {
228
        return new Predicate("NOT EXISTS ({$this->toSql()}");
229
    }
230
231
    /**
232
     * Adds a join.
233
     *
234
     * @param string|Select $table
235
     * @param string $condition
236
     * @param string $type
237
     * @return $this
238
     */
239
    public function join ($table, string $condition, string $type = 'INNER') {
240
        if ($table instanceof Select) {
241
            $table = $table->toSubquery();
242
        }
243
        $this->_join .= " {$type} JOIN {$table} ON {$condition}";
244
        return $this;
245
    }
246
247
    /**
248
     * Sets the limit and offset.
249
     *
250
     * @param int $limit
251
     * @param int $offset
252
     * @return $this
253
     */
254
    public function limit (int $limit, int $offset = 0) {
255
        if ($limit == 0) {
256
            $this->_limit = '';
257
        }
258
        else {
259
            $this->_limit = " LIMIT {$limit}";
260
            if ($offset > 1) {
261
                $this->_limit .= " OFFSET {$offset}";
262
            }
263
        }
264
        return $this;
265
    }
266
267
    /**
268
     * Blindly returns `true`.
269
     *
270
     * @param string $name Name or alias if used.
271
     * @return bool
272
     */
273
    public function offsetExists ($name): bool {
274
        return true;
275
    }
276
277
    /**
278
     * Blindly returns a {@link Column} using the Select's alias for qualification.
279
     *
280
     * @param string $name Name, or alias if used.
281
     * @return Column
282
     */
283
    public function offsetGet ($name) {
284
        return new Column($this->db, $name, $this->alias);
285
    }
286
287
    /**
288
     * Sets the order.
289
     *
290
     * @param string $order
291
     * @return $this
292
     */
293
    public function order (string $order) {
294
        if (strlen($order)) {
295
            $order = " ORDER BY {$order}";
296
        }
297
        $this->_order = $order;
298
        return $this;
299
    }
300
301
    /**
302
     * @return Statement
303
     */
304
    public function prepare () {
305
        return $this->db->prepare($this->toSql());
306
    }
307
308
    /**
309
     * @param Closure $fetcher
310
     * @return $this
311
     */
312
    public function setFetcher (Closure $fetcher) {
313
        $this->fetcher = $fetcher;
314
        return $this;
315
    }
316
317
    /**
318
     * @return string
319
     */
320
    public function toSql (): string {
321
        $sql = "SELECT {$this->_columns} FROM {$this->table}";
322
        $sql .= $this->_join;
323
        $sql .= $this->_where;
324
        $sql .= $this->_group;
325
        $sql .= $this->_having;
326
        $sql .= $this->_order;
327
        $sql .= $this->_limit;
328
        return $sql;
329
    }
330
331
    /**
332
     * `(SELECT ...) AS ALIAS`
333
     *
334
     * @return string
335
     */
336
    public function toSubquery (): string {
337
        return "({$this->toSql()}) AS {$this->alias}";
338
    }
339
340
    /**
341
     * Adds a condition.
342
     *
343
     * @param string $condition
344
     * @return $this
345
     */
346
    public function where (string $condition) {
347
        if (!strlen($this->_where)) {
348
            $this->_where = " WHERE {$condition}";
349
        }
350
        else {
351
            $this->_where .= " AND {$condition}";
352
        }
353
        return $this;
354
    }
355
}