Passed
Push — master ( 6c4562...bd418e )
by y
01:33
created

Select   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 458
Duplicated Lines 0 %

Importance

Changes 14
Bugs 1 Features 1
Metric Value
wmc 44
eloc 118
c 14
b 1
f 1
dl 0
loc 458
rs 8.8798

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getFirst() 0 2 1
A getEach() 0 2 1
A getAll() 0 2 1
A intersect() 0 11 2
A isNotEmpty() 0 2 1
A isEmpty() 0 2 1
A execute() 0 5 2
A __invoke() 0 2 1
A join() 0 6 2
A having() 0 8 2
A count() 0 5 1
A group() 0 8 2
A getIterator() 0 2 1
A limit() 0 11 3
A __construct() 0 13 2
A __toString() 0 2 1
A offsetGet() 0 5 3
A toSubquery() 0 2 1
A where() 0 8 2
A prepare() 0 2 1
A order() 0 6 2
A toSql() 0 10 1
A setFetcher() 0 3 1
A setAlias() 0 6 2
A setColumns() 0 19 5
A union() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like Select often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Select, and based on these observations, apply Extract Interface, too.

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 predicates.
36
     *
37
     * @internal
38
     * @var string
39
     */
40
    protected $_having = '';
41
42
    /**
43
     * Compiled unions and intersections.
44
     *
45
     * @internal
46
     * @var string
47
     */
48
    protected $_import = '';
49
50
    /**
51
     * Compiled joins.
52
     *
53
     * @internal
54
     * @var string
55
     */
56
    protected $_join = '';
57
58
    /**
59
     * Compiled limit and offset.
60
     *
61
     * @internal
62
     * @var string
63
     */
64
    protected $_limit = '';
65
66
    /**
67
     * Compiled column list.
68
     *
69
     * @internal
70
     * @var string
71
     */
72
    protected $_order = '';
73
74
    /**
75
     * Compiled predicates.
76
     *
77
     * @internal
78
     * @var string
79
     */
80
    protected $_where = '';
81
82
    /**
83
     * Human-readable alias.
84
     * This is initialized using `uniqid()` and the table's name.
85
     *
86
     * @var string
87
     */
88
    protected $alias;
89
90
    /**
91
     * A callback to yield each result.
92
     * Defaults to yielding directly from the statement.
93
     *
94
     * @var Closure `(Statement $statement):Generator`
95
     */
96
    protected $fetcher;
97
98
    /**
99
     * Columns that can be accessed by an outer query.
100
     *
101
     * @var Column[]
102
     */
103
    protected $refs = [];
104
105
    /**
106
     * @var string
107
     */
108
    protected $table;
109
110
    /**
111
     * @param DB $db
112
     * @param string|Select $table
113
     * @param string[] $columns
114
     */
115
    public function __construct (DB $db, $table, array $columns) {
116
        parent::__construct($db);
117
        if ($table instanceof Select) {
118
            $this->table = $table->toSubquery();
119
            $this->alias = uniqid('_') . "_{$table->alias}";
120
        }
121
        else {
122
            $this->table = (string)$table;
123
            $this->alias = uniqid('_') . "__{$table}";
124
        }
125
        $this->setColumns($columns);
126
        $this->fetcher = function(Statement $statement) {
127
            yield from $statement;
128
        };
129
    }
130
131
    /**
132
     * @param array $args
133
     * @return Statement
134
     */
135
    public function __invoke (array $args = []) {
136
        return $this->execute($args);
137
    }
138
139
    /**
140
     * Returns the alias.
141
     *
142
     * @return string
143
     */
144
    final public function __toString () {
145
        return $this->alias;
146
    }
147
148
    /**
149
     * Clones the instance and selects `COUNT(*)`, using the given execution arguments.
150
     *
151
     * @param array $args Execution arguments.
152
     * @return int
153
     */
154
    public function count (array $args = []): int {
155
        $clone = clone $this;
156
        $clone->_columns = 'COUNT(*)';
157
        $clone->_order = '';
158
        return (int)$clone->execute($args)->fetchColumn();
159
    }
160
161
    /**
162
     * Executes the select, preparing a statement first if arguments are used.
163
     *
164
     * @param array $args
165
     * @return Statement
166
     */
167
    public function execute (array $args = []) {
168
        if (empty($args)) {
169
            return $this->db->query($this->toSql());
170
        }
171
        return $this->prepare()->__invoke($args);
172
    }
173
174
    /**
175
     * Executes and fetches all results.
176
     *
177
     * @see fetcher
178
     *
179
     * @param array $args Execution arguments.
180
     * @return array
181
     */
182
    public function getAll (array $args = []): array {
183
        return iterator_to_array($this->fetcher->__invoke($this->execute($args)));
184
    }
185
186
    /**
187
     * Executes and yields from the fetcher.
188
     * This is preferable over `fetchAll()` for iterating large result sets.
189
     *
190
     * @see fetcher
191
     *
192
     * @param array $args Execution arguments.
193
     * @return Generator
194
     */
195
    public function getEach (array $args = []) {
196
        yield from $this->fetcher->__invoke($this->execute($args));
197
    }
198
199
    /**
200
     * Executes and returns from the fetcher.
201
     *
202
     * @see fetcher
203
     *
204
     * @param array $args
205
     * @return mixed
206
     */
207
    public function getFirst (array $args = []) {
208
        return $this->getEach($args)->current();
209
    }
210
211
    /**
212
     * Executes without arguments and yields from the fetcher.
213
     *
214
     * @see fetcher
215
     *
216
     * @return Generator
217
     */
218
    public function getIterator () {
219
        yield from $this->getEach();
220
    }
221
222
    /**
223
     * Adds a column to the `GROUP BY` clause.
224
     *
225
     * @param string $column
226
     * @return $this
227
     */
228
    public function group (string $column) {
229
        if (!strlen($this->_group)) {
230
            $this->_group = " GROUP BY {$column}";
231
        }
232
        else {
233
            $this->_group .= ", {$column}";
234
        }
235
        return $this;
236
    }
237
238
    /**
239
     * Adds a condition to the `HAVING` clause.
240
     *
241
     * @param string $condition
242
     * @return $this
243
     */
244
    public function having (string $condition) {
245
        if (!strlen($this->_having)) {
246
            $this->_having = " HAVING {$condition}";
247
        }
248
        else {
249
            $this->_having .= " AND {$condition}";
250
        }
251
        return $this;
252
    }
253
254
    /**
255
     * `INTERSECT` or `INTERSECT ALL`
256
     *
257
     * @param Select $select
258
     * @param bool $all
259
     * @return $this
260
     */
261
    public function intersect (Select $select, $all = false) {
262
        $select = clone $select;
263
        $select->_order = '';
264
        $select->_limit = '';
265
        if ($all) {
266
            $this->_import .= " INTERSECT ALL {$select->toSql()}";
267
        }
268
        else {
269
            $this->_import .= " INTERSECT {$select->toSql()}";
270
        }
271
        return $this;
272
    }
273
274
    /**
275
     * `NOT EXISTS (SELECT ...)`
276
     *
277
     * @return Predicate
278
     */
279
    public function isEmpty () {
280
        return $this->db->factory(Predicate::class, "NOT EXISTS ({$this->toSql()})");
281
    }
282
283
    /**
284
     * `EXISTS (SELECT ...)`
285
     *
286
     * @return Predicate
287
     */
288
    public function isNotEmpty () {
289
        return $this->db->factory(Predicate::class, "EXISTS ({$this->toSql()})");
290
    }
291
292
    /**
293
     * Adds a `JOIN` clause.
294
     *
295
     * @param string|Select $table
296
     * @param string $condition
297
     * @param string $type
298
     * @return $this
299
     */
300
    public function join ($table, string $condition, string $type = 'INNER') {
301
        if ($table instanceof Select) {
302
            $table = $table->toSubquery();
303
        }
304
        $this->_join .= " {$type} JOIN {$table} ON {$condition}";
305
        return $this;
306
    }
307
308
    /**
309
     * Sets the `LIMIT` clause.
310
     *
311
     * @param int $limit
312
     * @param int $offset
313
     * @return $this
314
     */
315
    public function limit (int $limit, int $offset = 0) {
316
        if ($limit == 0) {
317
            $this->_limit = '';
318
        }
319
        else {
320
            $this->_limit = " LIMIT {$limit}";
321
            if ($offset > 1) {
322
                $this->_limit .= " OFFSET {$offset}";
323
            }
324
        }
325
        return $this;
326
    }
327
328
    /**
329
     * Returns a reference {@link Column} for an outer query, qualified by the instance's alias.
330
     *
331
     * @param int|string $ref Ordinal or reference name.
332
     * @return null|Column
333
     */
334
    public function offsetGet ($ref) {
335
        if (is_int($ref)) {
336
            return current(array_slice($this->refs, $ref, 1)) ?: null;
337
        }
338
        return $this->refs[$ref] ?? null;
339
    }
340
341
    /**
342
     * Sets the `ORDER BY` clause.
343
     *
344
     * @param string $order
345
     * @return $this
346
     */
347
    public function order (string $order) {
348
        if (strlen($order)) {
349
            $order = " ORDER BY {$order}";
350
        }
351
        $this->_order = $order;
352
        return $this;
353
    }
354
355
    /**
356
     * @return Statement
357
     */
358
    public function prepare () {
359
        return $this->db->prepare($this->toSql());
360
    }
361
362
    /**
363
     * @param string $alias
364
     * @return $this
365
     */
366
    public function setAlias (string $alias) {
367
        $this->alias = $alias;
368
        foreach ($this->refs as $k => $column) {
369
            $this->refs[$k] = $column->setQualifier($alias);
370
        }
371
        return $this;
372
    }
373
374
    /**
375
     * Compiles the column list and exposed reference columns.
376
     *
377
     * Columns may be expressions, like `COUNT(*)`
378
     *
379
     * Unless an alias is given for such columns, they can't be referenced externally.
380
     *
381
     * @param string[] $expressions Keyed by alias if applicable.
382
     * @return $this
383
     */
384
    public function setColumns (array $expressions) {
385
        $this->refs = [];
386
        $_columns = [];
387
        foreach ($expressions as $alias => $expr) {
388
            preg_match('/^([a-z_][a-z0-9_]+\.)?(?<name>[a-z_][a-z0-9_]+)$/i', $expr, $match);
389
            $name = $match['name'] ?? null;
390
            if (is_int($alias)) {
391
                $alias = $name;
392
            }
393
            elseif ($alias !== $name) {
394
                $expr .= " AS {$alias}";
395
            }
396
            if (isset($alias)) {
397
                $this->refs[$alias] = $this->db->factory(Column::class, $this->db, $alias, $this->alias);
398
            }
399
            $_columns[] = "{$expr}";
400
        }
401
        $this->_columns = implode(', ', $_columns);
402
        return $this;
403
    }
404
405
    /**
406
     * @param Closure $fetcher
407
     * @return $this
408
     */
409
    public function setFetcher (Closure $fetcher) {
410
        $this->fetcher = $fetcher;
411
        return $this;
412
    }
413
414
    /**
415
     * `SELECT ...`
416
     *
417
     * @return string
418
     */
419
    public function toSql (): string {
420
        $sql = "SELECT {$this->_columns} FROM {$this->table}";
421
        $sql .= $this->_join;
422
        $sql .= $this->_where;
423
        $sql .= $this->_group;
424
        $sql .= $this->_having;
425
        $sql .= $this->_import;
426
        $sql .= $this->_order;
427
        $sql .= $this->_limit;
428
        return $sql;
429
    }
430
431
    /**
432
     * `(SELECT ...) AS ALIAS`
433
     *
434
     * @return string
435
     */
436
    public function toSubquery (): string {
437
        return "({$this->toSql()}) AS {$this->alias}";
438
    }
439
440
    /**
441
     * `UNION` or `UNION ALL`
442
     *
443
     * @param Select $select
444
     * @param bool $all
445
     * @return $this
446
     */
447
    public function union (Select $select, $all = false) {
448
        $select = clone $select;
449
        $select->_order = '';
450
        $select->_limit = '';
451
        if ($all) {
452
            $this->_import .= " UNION ALL {$select->toSql()}";
453
        }
454
        else {
455
            $this->_import .= " UNION {$select->toSql()}";
456
        }
457
        return $this;
458
    }
459
460
    /**
461
     * Adds a condition to the `WHERE` clause.
462
     *
463
     * @param string $condition
464
     * @return $this
465
     */
466
    public function where (string $condition) {
467
        if (!strlen($this->_where)) {
468
            $this->_where = " WHERE {$condition}";
469
        }
470
        else {
471
            $this->_where .= " AND {$condition}";
472
        }
473
        return $this;
474
    }
475
}