Completed
Push — 2.x ( 04f7c3...67d9b2 )
by Aleksei
18s queued 15s
created

Compiler   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 496
Duplicated Lines 0 %

Test Coverage

Coverage 95.98%

Importance

Changes 0
Metric Value
eloc 212
dl 0
loc 496
ccs 215
cts 224
cp 0.9598
rs 2.24
c 0
b 0
f 0
wmc 77

21 Methods

Rating   Name   Duplication   Size   Complexity  
A compile() 0 10 1
A __construct() 0 3 1
A quoteIdentifier() 0 3 1
A hashLimit() 0 11 3
B fragment() 0 54 11
A insertQuery() 0 19 3
A selectQuery() 0 24 4
A distinct() 0 3 2
A nameWithAlias() 0 16 2
A joins() 0 17 2
A name() 0 11 3
A columns() 0 11 1
A groupBy() 0 8 2
A orderBy() 0 14 3
A deleteQuery() 0 11 1
A optional() 0 11 4
A unions() 0 20 4
B where() 0 56 9
C condition() 0 49 13
A value() 0 22 5
A updateQuery() 0 19 2

How to fix   Complexity   

Complex Class

Complex classes like Compiler 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 Compiler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver;
13
14
use Cycle\Database\Exception\CompilerException;
15
use Cycle\Database\Injection\FragmentInterface;
16
use Cycle\Database\Injection\Parameter;
17
use Cycle\Database\Injection\ParameterInterface;
18
use Cycle\Database\Query\QueryParameters;
19
20
abstract class Compiler implements CompilerInterface
21
{
22
    private Quoter $quoter;
23
24
    /**
25
     * @psalm-param non-empty-string $quotes
26
     */
27 82
    public function __construct(string $quotes = '""')
28
    {
29 82
        $this->quoter = new Quoter('', $quotes);
30 82
    }
31
32
    /**
33
     * @psalm-param non-empty-string $identifier
34
     *
35
     * @psalm-return non-empty-string
36
     */
37 3460
    public function quoteIdentifier(string $identifier): string
38
    {
39 3460
        return $this->quoter->identifier($identifier);
40
    }
41
42
    /**
43
     * @psalm-return non-empty-string
44
     */
45 2254
    public function compile(
46
        QueryParameters $params,
47
        string $prefix,
48
        FragmentInterface $fragment
49
    ): string {
50 2254
        return $this->fragment(
51
            $params,
52 2254
            $this->quoter->withPrefix($prefix),
53
            $fragment,
54 2254
            false
55
        );
56
    }
57
58
    /**
59
     * @psalm-return non-empty-string
60
     */
61 1344
    public function hashLimit(QueryParameters $params, array $tokens): string
62
    {
63 1344
        if ($tokens['limit'] !== null) {
64 66
            $params->push(new Parameter($tokens['limit']));
65
        }
66
67 1344
        if ($tokens['offset'] !== null) {
68 48
            $params->push(new Parameter($tokens['offset']));
69
        }
70
71 1344
        return '_' . ($tokens['limit'] === null) . '_' . ($tokens['offset'] === null);
72
    }
73
74
    /**
75
     * @psalm-return non-empty-string
76
     */
77 2254
    protected function fragment(
78
        QueryParameters $params,
79
        Quoter $q,
80
        FragmentInterface $fragment,
81
        bool $nestedQuery = true
82
    ): string {
83 2254
        $tokens = $fragment->getTokens();
84
85 2254
        switch ($fragment->getType()) {
86 2254
            case self::FRAGMENT:
87 662
                foreach ($tokens['parameters'] as $param) {
88 16
                    $params->push($param);
89
                }
90
91 662
                return $tokens['fragment'];
92
93 1674
            case self::EXPRESSION:
94 342
                foreach ($tokens['parameters'] as $param) {
95 26
                    $params->push($param);
96
                }
97
98 342
                return $q->quote($tokens['expression']);
99
100 1670
            case self::INSERT_QUERY:
101 272
                return $this->insertQuery($params, $q, $tokens);
102
103 1560
            case self::SELECT_QUERY:
104 1440
                if ($nestedQuery) {
105 112
                    if ($fragment->getPrefix() !== null) {
0 ignored issues
show
Bug introduced by
The method getPrefix() does not exist on Cycle\Database\Injection\FragmentInterface. It seems like you code against a sub-type of Cycle\Database\Injection\FragmentInterface such as Cycle\Database\Query\QueryInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
                    if ($fragment->/** @scrutinizer ignore-call */ getPrefix() !== null) {
Loading history...
106 72
                        $q = $q->withPrefix(
107 72
                            $fragment->getPrefix(),
108 72
                            true
109
                        );
110
                    }
111
112 112
                    return sprintf(
113 112
                        '(%s)',
114 112
                        $this->selectQuery($params, $q, $tokens)
115
                    );
116
                }
117
118 1432
                return $this->selectQuery($params, $q, $tokens);
119
120 160
            case self::UPDATE_QUERY:
121 104
                return $this->updateQuery($params, $q, $tokens);
122
123 56
            case self::DELETE_QUERY:
124 56
                return $this->deleteQuery($params, $q, $tokens);
125
        }
126
127
        throw new CompilerException(
128
            sprintf(
129
                'Unknown fragment type %s',
130
                $fragment->getType()
131
            )
132
        );
133
    }
134
135
    /**
136
     * @psalm-return non-empty-string
137
     */
138 236
    protected function insertQuery(QueryParameters $params, Quoter $q, array $tokens): string
139
    {
140 236
        $values = [];
141 236
        foreach ($tokens['values'] as $value) {
142 228
            $values[] = $this->value($params, $q, $value);
143
        }
144
145 236
        if ($tokens['columns'] === []) {
146 8
            return sprintf(
147 8
                'INSERT INTO %s DEFAULT VALUES',
148 8
                $this->name($params, $q, $tokens['table'], true)
149
            );
150
        }
151
152 228
        return sprintf(
153 228
            'INSERT INTO %s (%s) VALUES %s',
154 228
            $this->name($params, $q, $tokens['table'], true),
155 228
            $this->columns($params, $q, $tokens['columns']),
156 228
            implode(', ', $values)
157
        );
158
    }
159
160
    /**
161
     * @psalm-return non-empty-string
162
     */
163 990
    protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens): string
164
    {
165
        // This statement(s) parts should be processed first to define set of table and column aliases
166 990
        $tables = [];
167 990
        foreach ($tokens['from'] as $table) {
168 990
            $tables[] = $this->name($params, $q, $table, true);
169
        }
170 990
        foreach ($tokens['join'] as $join) {
171 132
            $this->nameWithAlias(new QueryParameters(), $q, $join['outer'], $join['alias'], true);
172
        }
173
174 990
        return sprintf(
175 990
            "SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s",
176 990
            $this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])),
177 990
            $this->columns($params, $q, $tokens['columns']),
178 990
            \implode(', ', $tables),
179 990
            $this->optional(' ', $this->joins($params, $q, $tokens['join']), ' '),
180 990
            $this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])),
181 990
            $this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '),
182 990
            $this->optional("\nHAVING", $this->where($params, $q, $tokens['having'])),
183 990
            $this->optional("\n", $this->unions($params, $q, $tokens['union'])),
184 990
            $this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])),
185 990
            $this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])),
186 990
            $this->optional(' ', $tokens['forUpdate'] ? 'FOR UPDATE' : '')
187
        );
188
    }
189
190 1102
    protected function distinct(QueryParameters $params, Quoter $q, string|bool|array $distinct): string
191
    {
192 1102
        return $distinct === false ? '' : 'DISTINCT';
0 ignored issues
show
introduced by
The condition $distinct === false is always false.
Loading history...
193
    }
194
195 1440
    protected function joins(QueryParameters $params, Quoter $q, array $joins): string
196
    {
197 1440
        $statement = '';
198 1440
        foreach ($joins as $join) {
199 202
            $statement .= sprintf(
200 202
                "\n%s JOIN %s",
201 202
                $join['type'],
202 202
                $this->nameWithAlias($params, $q, $join['outer'], $join['alias'], true)
203
            );
204
205 202
            $statement .= $this->optional(
206 74
                "\n    ON",
207
                $this->where($params, $q, $join['on'])
208 74
            );
209
        }
210
211 202
        return $statement;
212 202
    }
213 202
214
    protected function unions(QueryParameters $params, Quoter $q, array $unions): string
215
    {
216
        if ($unions === []) {
217 1440
            return '';
218
        }
219
220 1440
        $statement = '';
221
        foreach ($unions as $union) {
222 1440
            $select = $this->fragment($params, $q, $union[1]);
223 1440
224
            if ($union[0] !== '') {
225
                //First key is union type, second united query (no need to share compiler)
226 24
                $statement .= "\nUNION {$union[0]}\n{$select}";
227 24
            } else {
228 24
                //No extra space
229
                $statement .= "\nUNION \n{$select}";
230 24
            }
231
        }
232 16
233
        return \ltrim($statement, "\n");
234
    }
235 16
236
    protected function orderBy(QueryParameters $params, Quoter $q, array $orderBy): string
237
    {
238
        $result = [];
239 24
        foreach ($orderBy as $order) {
240
            $direction = \strtoupper($order[1]);
241
242 1440
            \in_array($direction, ['ASC', 'DESC']) or throw new CompilerException(
243
                'Invalid sorting direction, only ASC and DESC are allowed'
244 1440
            );
245 1440
246 108
            $result[] = $this->name($params, $q, $order[0]) . ' ' . $direction;
247
        }
248 108
249
        return \implode(', ', $result);
250
    }
251
252 108
    protected function groupBy(QueryParameters $params, Quoter $q, array $groupBy): string
253
    {
254
        $result = [];
255 1440
        foreach ($groupBy as $identifier) {
256
            $result[] = $this->name($params, $q, $identifier);
257
        }
258 1440
259
        return \implode(', ', $result);
260 1440
    }
261 1440
262 80
    abstract protected function limit(
263
        QueryParameters $params,
264
        Quoter $q,
265 1440
        int $limit = null,
266
        int $offset = null
267
    ): string;
268
269
    protected function updateQuery(
270
        QueryParameters $parameters,
271
        Quoter $quoter,
272
        array $tokens
273
    ): string {
274
        $values = [];
275 104
        foreach ($tokens['values'] as $column => $value) {
276
            $values[] = sprintf(
277
                '%s = %s',
278
                $this->name($parameters, $quoter, $column),
279
                $this->value($parameters, $quoter, $value)
280 104
            );
281 104
        }
282 104
283 104
        return sprintf(
284 104
            "UPDATE %s\nSET %s%s",
285 104
            $this->name($parameters, $quoter, $tokens['table'], true),
286
            trim(implode(', ', $values)),
287
            $this->optional("\nWHERE", $this->where($parameters, $quoter, $tokens['where']))
288
        );
289 104
    }
290 104
291 104
    /**
292 104
     * @psalm-return non-empty-string
293 104
     */
294
    protected function deleteQuery(
295
        QueryParameters $parameters,
296
        Quoter $quoter,
297
        array $tokens
298
    ): string {
299
        return sprintf(
300 56
            'DELETE FROM %s%s',
301
            $this->name($parameters, $quoter, $tokens['table'], true),
302
            $this->optional(
303
                "\nWHERE",
304
                $this->where($parameters, $quoter, $tokens['where'])
305 56
            )
306 56
        );
307 56
    }
308 56
309 56
    /**
310 56
     * @psalm-return non-empty-string
311
     */
312
    protected function name(QueryParameters $params, Quoter $q, $name, bool $table = false): string
313
    {
314
        if ($name instanceof FragmentInterface) {
315
            return $this->fragment($params, $q, $name);
316
        }
317
318 1670
        if ($name instanceof ParameterInterface) {
319
            return $this->value($params, $q, $name);
320 1670
        }
321 184
322
        return $q->quote($name, $table);
323
    }
324 1670
325 8
    /**
326
     * @psalm-return non-empty-string
327
     */
328 1670
    protected function nameWithAlias(
329
        QueryParameters $params,
330
        Quoter $q,
331
        $name,
332
        ?string $alias = null,
333
        bool $table = false,
334 1546
    ): string {
335
        $quotedName = $this->name($params, $q, $name, $table);
336
337 1546
        if ($alias !== null) {
338 1546
            $q->registerAlias($alias, (string) $name);
339 1546
340 1546
            $quotedName .= ' AS ' . $this->name($params, $q, $alias);
341
        }
342
343
        return $quotedName;
344 1546
    }
345
346
    /**
347
     * @psalm-return non-empty-string
348
     */
349
    protected function columns(QueryParameters $params, Quoter $q, array $columns, int $maxLength = 180): string
350 338
    {
351
        // let's quote every identifier
352 338
        $columns = array_map(
353 16
            function ($column) use ($params, $q) {
354
                return $this->name($params, $q, $column);
355
            },
356 338
            $columns
357 330
        );
358
359
        return wordwrap(implode(', ', $columns), $maxLength);
360 338
    }
361 256
362 256
    /**
363 256
     * @psalm-return non-empty-string
364
     */
365
    protected function value(QueryParameters $params, Quoter $q, $value): string
366 256
    {
367
        if ($value instanceof FragmentInterface) {
368
            return $this->fragment($params, $q, $value);
369 338
        }
370
371 338
        if (!$value instanceof ParameterInterface) {
372
            $value = new Parameter($value);
373
        }
374 1560
375
        if ($value->isArray()) {
376 1560
            $values = [];
377 1496
            foreach ($value->getValue() as $child) {
378
                $values[] = $this->value($params, $q, $child);
379
            }
380 1194
381
            return '(' . implode(', ', $values) . ')';
382 1194
        }
383 1194
384
        $params->push($value);
385 1194
386
        return '?';
387
    }
388 1194
389
    protected function where(QueryParameters $params, Quoter $q, array $tokens): string
390 1194
    {
391
        if ($tokens === []) {
392 480
            return '';
393 480
        }
394
395
        $statement = '';
396
397
        $activeGroup = true;
398
        foreach ($tokens as $condition) {
399
            // OR/AND keyword
400 1194
            [$boolean, $context] = $condition;
401 240
402
            // first condition in group/query, no any AND, OR required
403 240
            if ($activeGroup) {
404
                // next conditions require AND or OR
405
                $activeGroup = false;
406 240
            } else {
407 240
                $statement .= $boolean;
408
                $statement .= ' ';
409
            }
410 1186
411 8
            /*
412 8
             * When context is string it usually represent control keyword/syntax such as opening
413 8
             * or closing braces.
414
             */
415
            if (\is_string($context)) {
416
                if ($context === '(') {
417 1186
                    // new where group.
418 1186
                    $activeGroup = true;
419 1186
                }
420 1186
421
                $statement .= $context;
422
                continue;
423 1194
            }
424
425 1194
            if ($context instanceof FragmentInterface) {
426 8
                $statement .= $this->fragment($params, $q, $context);
427
                $statement .= ' ';
428
                continue;
429 1186
            }
430
431
            // identifier can be column name, expression or even query builder
432
            $statement .= $this->name($params, $q, $context[0]);
433
            $statement .= ' ';
434
            $statement .= $this->condition($params, $q, $context);
435 1186
            $statement .= ' ';
436
        }
437 1186
438 1186
        $activeGroup and throw new CompilerException('Unable to build where statement, unclosed where group');
439
440 1186
        if (trim($statement, ' ()') === '') {
441 16
            return '';
442 1170
        }
443
444
        return $statement;
445
    }
446 1186
447 308
    /**
448
     * @psalm-return non-empty-string
449
     */
450 1040
    protected function condition(QueryParameters $params, Quoter $q, array $context): string
451
    {
452
        $operator = $context[1];
453
        $value = $context[2];
454 1040
455 1040
        if ($operator instanceof FragmentInterface) {
456 50
            $operator = $this->fragment($params, $q, $operator);
457
        } elseif (!\is_string($operator)) {
458 50
            throw new CompilerException('Invalid operator type, string or fragment is expected');
459
        }
460
461
        if ($value instanceof FragmentInterface) {
462 50
            return $operator . ' ' . $this->fragment($params, $q, $value);
463 50
        }
464 1022
465 32
        if (!$value instanceof ParameterInterface) {
466 8
            throw new CompilerException('Invalid value format, fragment or parameter is expected');
467 24
        }
468 8
469
        $placeholder = '?';
470
        if ($value->isArray()) {
471 32
            if ($operator === '=') {
472
                $operator = 'IN';
473 990
            } elseif ($operator === '!=') {
474
                $operator = 'NOT IN';
475
            }
476 1040
477 64
            $placeholder = '(' . rtrim(str_repeat('? ,', count($value->getValue())), ', ') . ')';
478
            $params->push($value);
479
        } elseif ($value->isNull()) {
480 64
            if ($operator === '=') {
481
                $operator = 'IS';
482
            } elseif ($operator === '!=') {
483 976
                $operator = 'IS NOT';
484
            }
485
486
            $placeholder = 'NULL';
487
        } else {
488
            $params->push($value);
489
        }
490 1560
491
        if ($operator === 'BETWEEN' || $operator === 'NOT BETWEEN') {
492 1560
            $params->push($context[3]);
493 1496
494
            // possibly support between nested queries
495
            return $operator . ' ? AND ?';
496 1292
        }
497 1236
498
        return $operator . ' ' . $placeholder;
499
    }
500 1292
501
    /**
502
     * Combine expression with prefix/postfix (usually SQL keyword) but only if expression is not
503
     * empty.
504
     */
505
    protected function optional(string $prefix, string $expression, string $postfix = ''): string
506
    {
507
        if ($expression === '') {
508
            return '';
509
        }
510
511
        if ($prefix !== "\n" && $prefix !== ' ') {
512
            $prefix .= ' ';
513
        }
514
515
        return $prefix . $expression . $postfix;
516
    }
517
}
518