Completed
Push — 2.x ( be80d1...0b4d06 )
by Aleksei
17s queued 15s
created

Compiler::where()   B

Complexity

Conditions 9
Paths 37

Size

Total Lines 56
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 9.0294

Importance

Changes 0
Metric Value
cc 9
eloc 28
c 0
b 0
f 0
nc 37
nop 3
dl 0
loc 56
rs 8.0555
ccs 26
cts 28
cp 0.9286
crap 9.0294

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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::JSON_EXPRESSION:
101 272
                foreach ($tokens['parameters'] as $param) {
102
                    $params->push($param);
103 1560
                }
104 1440
105 112
                return $tokens['expression'];
106 72
107 72
            case self::INSERT_QUERY:
108 72
                return $this->insertQuery($params, $q, $tokens);
109
110
            case self::SELECT_QUERY:
111
                if ($nestedQuery) {
112 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

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