Passed
Push — master ( 8b0690...6822a9 )
by y
01:22
created

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