Completed
Pull Request — master (#96)
by Deven
62:33
created

Query   F

Complexity

Total Complexity 125

Size/Duplication

Total Lines 805
Duplicated Lines 1.74 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 14
loc 805
ccs 342
cts 342
cp 1
rs 1.263
c 0
b 0
f 0
wmc 125
lcom 1
cbo 15

8 Methods

Rating   Name   Duplication   Size   Complexity  
F getFlags() 0 297 44
C getAll() 0 75 23
C getTables() 0 40 14
F getClause() 7 115 19
A replaceClause() 0 17 3
B replaceClauses() 0 44 5
C getFirstStatement() 0 50 7
C getClauseStartOffset() 7 52 10

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Query 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Statement utilities.
5
 *
6
 * @package    SqlParser
7
 * @subpackage Utils
8
 */
9
namespace SqlParser\Utils;
10
11
use SqlParser\Lexer;
12
use SqlParser\Parser;
13
use SqlParser\Statement;
14
use SqlParser\Token;
15
use SqlParser\TokensList;
16
use SqlParser\Components\Expression;
17
use SqlParser\Statements\AlterStatement;
18
use SqlParser\Statements\AnalyzeStatement;
19
use SqlParser\Statements\CallStatement;
20
use SqlParser\Statements\CheckStatement;
21
use SqlParser\Statements\ChecksumStatement;
22
use SqlParser\Statements\CreateStatement;
23
use SqlParser\Statements\DeleteStatement;
24
use SqlParser\Statements\DropStatement;
25
use SqlParser\Statements\ExplainStatement;
26
use SqlParser\Statements\InsertStatement;
27
use SqlParser\Statements\OptimizeStatement;
28
use SqlParser\Statements\RenameStatement;
29
use SqlParser\Statements\RepairStatement;
30
use SqlParser\Statements\ReplaceStatement;
31
use SqlParser\Statements\SelectStatement;
32
use SqlParser\Statements\ShowStatement;
33
use SqlParser\Statements\TruncateStatement;
34
use SqlParser\Statements\UpdateStatement;
35
36
/**
37
 * Statement utilities.
38
 *
39
 * @category   Statement
40
 * @package    SqlParser
41
 * @subpackage Utils
42
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
43
 */
44
class Query
45
{
46
47
    /**
48
     * Functions that set the flag `is_func`.
49
     *
50
     * @var array
51
     */
52
    public static $FUNCTIONS = array(
53
        'SUM', 'AVG', 'STD', 'STDDEV', 'MIN', 'MAX', 'BIT_OR', 'BIT_AND'
54
    );
55
56
    /**
57
     * Gets an array with flags this statement has.
58
     *
59
     * @param Statement|null $statement The statement to be processed.
60
     * @param bool           $all       If `false`, false values will not be included.
61
     *
62
     * @return array
63
     */
64
    public static function getFlags($statement, $all = false)
65 29
    {
66
        $flags = array();
67 29
        if ($all) {
68 29
            $flags = array(
69
70
                /**
71
                 * select ... DISTINCT ...
72
                 */
73
                'distinct'      => false,
74 1
75
                /**
76
                 * drop ... DATABASE ...
77
                 */
78
                'drop_database' => false,
79 1
80
                /**
81
                 * ... GROUP BY ...
82
                 */
83
                'group'         => false,
84 1
85
                /**
86
                 * ... HAVING ...
87
                 */
88
                'having'        => false,
89 1
90
                /**
91
                 * INSERT ...
92
                 * or
93
                 * REPLACE ...
94
                 * or
95
                 * DELETE ...
96
                 */
97
                'is_affected'   => false,
98 1
99
                /**
100
                 * select ... PROCEDURE ANALYSE( ... ) ...
101
                 */
102
                'is_analyse'    => false,
103 1
104
                /**
105
                 * select COUNT( ... ) ...
106
                 */
107
                'is_count'      => false,
108 1
109
                /**
110
                 * DELETE ...
111
                 */
112
                'is_delete'     => false, // @deprecated; use `querytype`
113 1
114
                /**
115
                 * EXPLAIN ...
116
                 */
117
                'is_explain'    => false, // @deprecated; use `querytype`
118 1
119
                /**
120
                 * select ... INTO OUTFILE ...
121
                 */
122
                'is_export'     => false,
123 1
124
                /**
125
                 * select FUNC( ... ) ...
126
                 */
127
                'is_func'       => false,
128 1
129
                /**
130
                 * select ... GROUP BY ...
131
                 * or
132
                 * select ... HAVING ...
133
                 */
134
                'is_group'      => false,
135 1
136
                /**
137
                 * INSERT ...
138
                 * or
139
                 * REPLACE ...
140
                 * or
141
                 * TODO: LOAD DATA ...
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
142
                 */
143
                'is_insert'     => false,
144 1
145
                /**
146
                 * ANALYZE ...
147
                 * or
148
                 * CHECK ...
149
                 * or
150
                 * CHECKSUM ...
151
                 * or
152
                 * OPTIMIZE ...
153
                 * or
154
                 * REPAIR ...
155
                 */
156
                'is_maint'      => false,
157 1
158
                /**
159
                 * CALL ...
160
                 */
161
                'is_procedure'  => false,
162 1
163
                /**
164
                 * REPLACE ...
165
                 */
166
                'is_replace'    => false, // @deprecated; use `querytype`
167 1
168
                /**
169
                 * SELECT ...
170
                 */
171
                'is_select'     => false, // @deprecated; use `querytype`
172 1
173
                /**
174
                 * SHOW ...
175
                 */
176
                'is_show'       => false, // @deprecated; use `querytype`
177 1
178
                /**
179
                 * Contains a subquery.
180
                 */
181
                'is_subquery'   => false,
182 1
183
                /**
184
                 * ... JOIN ...
185
                 */
186
                'join'          => false,
187 1
188
                /**
189
                 * ... LIMIT ...
190
                 */
191
                'limit'         => false,
192 1
193
                /**
194
                 * TODO
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
195
                 */
196
                'offset'        => false,
197 1
198
                /**
199
                 * ... ORDER ...
200
                 */
201
                'order'         => false,
202 1
203
                /**
204
                 * The type of the query (which is usually the first keyword of
205
                 * the statement).
206
                 */
207
                'querytype'     => false,
208 1
209
                /**
210
                 * Whether a page reload is required.
211
                 */
212
                'reload'        => false,
213 1
214
                /**
215
                 * SELECT ... FROM ...
216
                 */
217
                'select_from'   => false,
218 1
219
                /**
220
                 * ... UNION ...
221
                 */
222
                'union'         => false
223
            );
224 1
        }
225 1
226
        if ($statement instanceof AlterStatement) {
227 29
            $flags['querytype'] = 'ALTER';
228 1
            $flags['reload'] = true;
229 1
        } elseif ($statement instanceof CreateStatement) {
230 29
            $flags['querytype'] = 'CREATE';
231 1
            $flags['reload'] = true;
232 1
        } elseif ($statement instanceof AnalyzeStatement) {
233 28
            $flags['querytype'] = 'ANALYZE';
234 1
            $flags['is_maint'] = true;
235 1
        } elseif ($statement instanceof CheckStatement) {
236 27
            $flags['querytype'] = 'CHECK';
237 1
            $flags['is_maint'] = true;
238 1
        } elseif ($statement instanceof ChecksumStatement) {
239 26
            $flags['querytype'] = 'CHECKSUM';
240 1
            $flags['is_maint'] = true;
241 1
        } elseif ($statement instanceof OptimizeStatement) {
242 25
            $flags['querytype'] = 'OPTIMIZE';
243 1
            $flags['is_maint'] = true;
244 1
        } elseif ($statement instanceof RepairStatement) {
245 24
            $flags['querytype'] = 'REPAIR';
246 1
            $flags['is_maint'] = true;
247 1
        } elseif ($statement instanceof CallStatement) {
248 23
            $flags['querytype'] = 'CALL';
249 1
            $flags['is_procedure'] = true;
250 1
        } elseif ($statement instanceof DeleteStatement) {
251 22
            $flags['querytype'] = 'DELETE';
252 1
            $flags['is_delete'] = true;
253 1
            $flags['is_affected'] = true;
254 1
        } elseif ($statement instanceof DropStatement) {
255 21
            $flags['querytype'] = 'DROP';
256 2
            $flags['reload'] = true;
257 2
258
            if (($statement->options->has('DATABASE')
259 2
                || ($statement->options->has('SCHEMA')))
260 1
            ) {
261 2
                $flags['drop_database'] = true;
262 1
            }
263 1
        } elseif ($statement instanceof ExplainStatement) {
264 20
            $flags['querytype'] = 'EXPLAIN';
265 1
            $flags['is_explain'] = true;
266 1
        } elseif ($statement instanceof InsertStatement) {
267 18
            $flags['querytype'] = 'INSERT';
268 1
            $flags['is_affected'] = true;
269 1
            $flags['is_insert'] = true;
270 1
        } elseif ($statement instanceof ReplaceStatement) {
271 17
            $flags['querytype'] = 'REPLACE';
272 1
            $flags['is_affected'] = true;
273 1
            $flags['is_replace'] = true;
274 1
            $flags['is_insert'] = true;
275 1
        } elseif ($statement instanceof SelectStatement) {
276 16
            $flags['querytype'] = 'SELECT';
277 13
            $flags['is_select'] = true;
278 13
279
            if (!empty($statement->from)) {
280 13
                $flags['select_from'] = true;
281 11
            }
282 11
283
            if ($statement->options->has('DISTINCT')) {
284 13
                $flags['distinct'] = true;
285 1
            }
286 1
287
            if ((!empty($statement->group)) || (!empty($statement->having))) {
288 13
                $flags['is_group'] = true;
289 2
            }
290 2
291
            if ((!empty($statement->into))
292 13
                && ($statement->into->type === 'OUTFILE')
293 13
            ) {
294 13
                $flags['is_export'] = true;
295 1
            }
296 1
297
            $expressions = $statement->expr;
298 13
            if (!empty($statement->join)) {
299 13
                foreach ($statement->join as $join) {
300 1
                    $expressions[] = $join->expr;
301 1
                }
302 1
            }
303 1
304
            foreach ($expressions as $expr) {
305 13
                if (!empty($expr->function)) {
306 13
                    if ($expr->function === 'COUNT') {
307 1
                        $flags['is_count'] = true;
308 1
                    } elseif (in_array($expr->function, static::$FUNCTIONS)) {
309 1
                        $flags['is_func'] = true;
310 1
                    }
311 1
                }
312 1
                if (!empty($expr->subquery)) {
313 13
                    $flags['is_subquery'] = true;
314 1
                }
315 1
            }
316 13
317
            if ((!empty($statement->procedure))
318 13
                && ($statement->procedure->name === 'ANALYSE')
319 13
            ) {
320 13
                $flags['is_analyse'] = true;
321 1
            }
322 1
323
            if (!empty($statement->group)) {
324 13
                $flags['group'] = true;
325 1
            }
326 1
327
            if (!empty($statement->having)) {
328 13
                $flags['having'] = true;
329 1
            }
330 1
331
            if (!empty($statement->union)) {
332 13
                $flags['union'] = true;
333 1
            }
334 1
335
            if (!empty($statement->join)) {
336 13
                $flags['join'] = true;
337 1
            }
338 1
339
        } elseif ($statement instanceof ShowStatement) {
340 15
            $flags['querytype'] = 'SHOW';
341 1
            $flags['is_show'] = true;
342 1
        } elseif ($statement instanceof UpdateStatement) {
343 3
            $flags['querytype'] = 'UPDATE';
344 1
            $flags['is_affected'] = true;
345 1
        }
346 1
347
        if (($statement instanceof SelectStatement)
348 29
            || ($statement instanceof UpdateStatement)
349 17
            || ($statement instanceof DeleteStatement)
350 17
        ) {
351 29
            if (!empty($statement->limit)) {
352 15
                $flags['limit'] = true;
353 2
            }
354 2
            if (!empty($statement->order)) {
355 15
                $flags['order'] = true;
356 2
            }
357 2
        }
358 15
359
        return $flags;
360 29
    }
361
362
    /**
363
     * Parses a query and gets all information about it.
364
     *
365
     * @param string $query The query to be parsed.
366
     *
367
     * @return array The array returned is the one returned by
368
     *               `static::getFlags()`, with the following keys added:
369
     *                 - parser - the parser used to analyze the query;
370
     *                 - statement - the first statement resulted from parsing;
371
     *                 - select_tables - the real name of the tables selected;
372
     *                       if there are no table names in the `SELECT`
373
     *                       expressions, the table names are fetched from the
374
     *                       `FROM` expressions
375
     *                 - select_expr - selected expressions
376
     */
377
    public static function getAll($query)
378 1
    {
379
        $parser = new Parser($query);
380 1
381
        if (empty($parser->statements[0])) {
382 1
            return static::getFlags(null, true);
383 1
        }
384
385
        $statement = $parser->statements[0];
386 1
387
        $ret = static::getFlags($statement, true);
388 1
389
        $ret['parser'] = $parser;
390 1
        $ret['statement'] = $statement;
391 1
392
        if ($statement instanceof SelectStatement) {
393 1
            $ret['select_tables'] = array();
394 1
            $ret['select_expr'] = array();
395 1
396
            // Finding tables' aliases and their associated real names.
397
            $tableAliases = array();
398 1
            foreach ($statement->from as $expr) {
399 1
                if ((isset($expr->table)) && ($expr->table !== '')
400 1
                    && (isset($expr->alias)) && ($expr->alias !== '')
401 1
                ) {
402 1
                    $tableAliases[$expr->alias] = array(
403 1
                        $expr->table,
404 1
                        isset($expr->database) ? $expr->database : null
405 1
                    );
406 1
                }
407 1
            }
408 1
409
            // Trying to find selected tables only from the select expression.
410
            // Sometimes, this is not possible because the tables aren't defined
411
            // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film).
412
            foreach ($statement->expr as $expr) {
413 1
                if ((isset($expr->table)) && ($expr->table !== '')) {
414 1
                    if (isset($tableAliases[$expr->table])) {
415 1
                        $arr = $tableAliases[$expr->table];
416 1
                    } else {
417 1
                        $arr = array(
418
                            $expr->table,
419 1
                            ((isset($expr->database)) && ($expr->database !== '')) ?
420 1
                                $expr->database : null
421 1
                        );
422 1
                    }
423
                    if (!in_array($arr, $ret['select_tables'])) {
424 1
                        $ret['select_tables'][] = $arr;
425 1
                    }
426 1
                } else {
427 1
                    $ret['select_expr'][] = $expr->expr;
428 1
                }
429
            }
430 1
431
            // If no tables names were found in the SELECT clause or if there
432
            // are expressions like * or COUNT(*), etc. tables names should be
433
            // extracted from the FROM clause.
434
            if (empty($ret['select_tables'])) {
435 1
                foreach ($statement->from as $expr) {
436 1
                    if ((isset($expr->table)) && ($expr->table !== '')) {
437 1
                        $arr = array(
438
                            $expr->table,
439 1
                            ((isset($expr->database)) && ($expr->database !== '')) ?
440 1
                                $expr->database : null
441 1
                        );
442 1
                        if (!in_array($arr, $ret['select_tables'])) {
443 1
                            $ret['select_tables'][] = $arr;
444 1
                        }
445 1
                    }
446 1
                }
447 1
            }
448 1
        }
449 1
450
        return $ret;
451 1
    }
452
453
    /**
454
     * Gets a list of all tables used in this statement.
455
     *
456
     * @param Statement $statement Statement to be scanned.
457
     *
458
     * @return array
459
     */
460
    public static function getTables($statement)
461 7
    {
462
        $expressions = array();
463 7
464
        if (($statement instanceof InsertStatement)
465 7
            || ($statement instanceof ReplaceStatement)
466 6
        ) {
467 7
            $expressions = array($statement->into->dest);
468 1
        } elseif ($statement instanceof UpdateStatement) {
469 7
            $expressions = $statement->tables;
470 1
        } elseif (($statement instanceof SelectStatement)
471 6
            || ($statement instanceof DeleteStatement)
472 5
        ) {
473 5
            $expressions = $statement->from;
474 1
        } elseif (($statement instanceof AlterStatement)
475 5
            || ($statement instanceof TruncateStatement)
476 4
        ) {
477 4
            $expressions = array($statement->table);
478 1
        } elseif ($statement instanceof DropStatement) {
479 4
            if (!$statement->options->has('TABLE')) {
480 2
                // No tables are dropped.
481
                return array();
482 1
            }
483
            $expressions = $statement->fields;
484 1
        } elseif ($statement instanceof RenameStatement) {
485 2
            foreach ($statement->renames as $rename) {
486 1
                $expressions[] = $rename->old;
487 1
            }
488 1
        }
489 1
490
        $ret = array();
491 6
        foreach ($expressions as $expr) {
492 6
            if (!empty($expr->table)) {
493 6
                $expr->expr = null; // Force rebuild.
494 6
                $expr->alias = null; // Aliases are not required.
495 6
                $ret[] = Expression::build($expr);
496 6
            }
497 6
        }
498 6
        return $ret;
499 6
    }
500
501
    /**
502
     * Gets a specific clause.
503
     *
504
     * @param Statement  $statement The parsed query that has to be modified.
505
     * @param TokensList $list      The list of tokens.
506
     * @param string     $clause    The clause to be returned.
507
     * @param int|string $type      The type of the search.
508
     *                              If int,
509
     *                                -1 for everything that was before
510
     *                                 0 only for the clause
511
     *                                 1 for everything after
512
     *                              If string, the name of the first clause that
513
     *                              should not be included.
514
     * @param bool       $skipFirst Whether to skip the first keyword in clause.
515
     *
516
     * @return string
517
     */
518
    public static function getClause($statement, $list, $clause, $type = 0, $skipFirst = true)
519 4
    {
520
521
        /**
522
         * The index of the current clause.
523
         *
524
         * @var int $currIdx
525
         */
526
        $currIdx = 0;
527 4
528
        /**
529
         * The count of brackets.
530
         * We keep track of them so we won't insert the clause in a subquery.
531
         *
532
         * @var int $brackets
533
         */
534
        $brackets = 0;
535 4
536
        /**
537
         * The string to be returned.
538
         *
539
         * @var string $ret
540
         */
541
        $ret = '';
542 4
543
        /**
544
         * The clauses of this type of statement and their index.
545
         *
546
         * @var array $clauses
547
         */
548
        $clauses = array_flip(array_keys($statement->getClauses()));
549 4
550
        /**
551
         * Lexer used for lexing the clause.
552
         *
553
         * @var Lexer $lexer
554
         */
555
        $lexer = new Lexer($clause);
556 4
557
        /**
558
         * The type of this clause.
559
         *
560
         * @var string $clauseType
561
         */
562
        $clauseType = $lexer->list->getNextOfType(Token::TYPE_KEYWORD)->value;
563 4
564
        /**
565
         * The index of this clause.
566
         *
567
         * @var int $clauseIdx
568
         */
569
        $clauseIdx = $clauses[$clauseType];
570 4
571
        $firstClauseIdx = $clauseIdx;
572 4
        $lastClauseIdx = $clauseIdx + 1;
573 4
574
        // Determining the behavior of this function.
575
        if ($type === -1) {
576 4
            $firstClauseIdx = -1; // Something small enough.
577 3
            $lastClauseIdx = $clauseIdx - 1;
578 3
        } elseif ($type === 1) {
579 4
            $firstClauseIdx = $clauseIdx + 1;
580 3
            $lastClauseIdx = 10000; // Something big enough.
581 3
        } elseif (is_string($type)) {
582 4
            if ($clauses[$type] > $clauseIdx) {
583 2
                $firstClauseIdx = $clauseIdx + 1;
584 1
                $lastClauseIdx = $clauses[$type] - 1;
585 1
            } else {
586 1
                $firstClauseIdx = $clauses[$type] + 1;
587 1
                $lastClauseIdx = $clauseIdx - 1;
588 1
            }
589
        }
590 2
591
        // This option is unavailable for multiple clauses.
592
        if ($type !== 0) {
593 4
            $skipFirst = false;
594 4
        }
595 4
596
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
597 4
            $token = $list->tokens[$i];
598 4
599
            if ($token->type === Token::TYPE_COMMENT) {
600 4
                continue;
601 1
            }
602
603 View Code Duplication
            if ($token->type === Token::TYPE_OPERATOR) {
604 4
                if ($token->value === '(') {
605 4
                    ++$brackets;
606 3
                } elseif ($token->value === ')') {
607 4
                    --$brackets;
608 3
                }
609 3
            }
610 4
611
            if ($brackets == 0) {
612 4
                // Checking if the section was changed.
613
                if (($token->type === Token::TYPE_KEYWORD)
614 4
                    && (isset($clauses[$token->value]))
615 4
                    && ($clauses[$token->value] >= $currIdx)
616 4
                ) {
617 4
                    $currIdx = $clauses[$token->value];
618 4
                    if (($skipFirst) && ($currIdx == $clauseIdx)) {
619 4
                        // This token is skipped (not added to the old
620
                        // clause) because it will be replaced.
621
                        continue;
622 1
                    }
623
                }
624 4
            }
625 4
626
            if (($firstClauseIdx <= $currIdx) && ($currIdx <= $lastClauseIdx)) {
627 4
                $ret .= $token->token;
628 4
            }
629 4
        }
630 4
631
        return trim($ret);
632 4
    }
633
634
    /**
635
     * Builds a query by rebuilding the statement from the tokens list supplied
636
     * and replaces a clause.
637
     *
638
     * It is a very basic version of a query builder.
639
     *
640
     * @param Statement  $statement The parsed query that has to be modified.
641
     * @param TokensList $list      The list of tokens.
642
     * @param string     $old       The type of the clause that should be
643
     *                              replaced. This can be an entire clause.
644
     * @param string     $new       The new clause. If this parameter is omitted
0 ignored issues
show
Documentation introduced by
Should the type for parameter $new not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
645
     *                              it is considered to be equal with `$old`.
646
     * @param bool       $onlyType  Whether only the type of the clause should
647
     *                              be replaced or the entire clause.
648
     *
649
     * @return string
650
     */
651
    public static function replaceClause($statement, $list, $old, $new = null, $onlyType = false)
652 3
    {
653
        // TODO: Update the tokens list and the statement.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
654
655
        if ($new === null) {
656 3
            $new = $old;
657 2
        }
658 2
659
        if ($onlyType) {
660 3
            return static::getClause($statement, $list, $old, -1, false) . ' ' .
661 1
                $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' .
662 1
                static::getClause($statement, $list, $old, 1, false);
663 1
        }
664
665
        return static::getClause($statement, $list, $old, -1, false) . ' ' .
666 2
            $new . ' ' . static::getClause($statement, $list, $old, 1, false);
667 2
    }
668
669
    /**
670
     * Builds a query by rebuilding the statement from the tokens list supplied
671
     * and replaces multiple clauses.
672
     *
673
     * @param Statement  $statement The parsed query that has to be modified.
674
     * @param TokensList $list      The list of tokens.
675
     * @param array      $ops       Clauses to be replaced. Contains multiple
676
     *                              arrays having two values: array($old, $new).
677
     *                              Clauses must be sorted.
678
     *
679
     * @return string
680
     */
681
    public static function replaceClauses($statement, $list, array $ops)
682 1
    {
683
        $count = count($ops);
684 1
685
        // Nothing to do.
686
        if ($count === 0) {
687 1
            return '';
688 1
        }
689
690
        /**
691
         * Value to be returned.
692
         *
693
         * @var string $ret
694
         */
695
        $ret = '';
696 1
697
        // If there is only one clause, `replaceClause()` should be used.
698
        if ($count === 1) {
699 1
            return static::replaceClause(
700 1
                $statement,
701 1
                $list,
702 1
                $ops[0][0],
703 1
                $ops[0][1]
704 1
            );
705 1
        }
706
707
        // Adding everything before first replacement.
708
        $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' ';
709 1
710
        // Doing replacements.
711
        for ($i = 0; $i < $count; ++$i) {
712 1
            $ret .= $ops[$i][1] . ' ';
713 1
714
            // Adding everything between this and next replacement.
715
            if ($i + 1 !== $count) {
716 1
                $ret .= static::getClause($statement, $list, $ops[$i][0], $ops[$i + 1][0]) . ' ';
717 1
            }
718 1
        }
719 1
720
        // Adding everything after the last replacement.
721
        $ret .= static::getClause($statement, $list, $ops[$count - 1][0], 1);
722 1
723
        return $ret;
724 1
    }
725
726
    /**
727
     * Gets the first full statement in the query.
728
     *
729
     * @param string $query     The query to be analyzed.
730
     * @param string $delimiter The delimiter to be used.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $delimiter not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
731
     *
732
     * @return array                Array containing the first full query, the
733
     *                              remaining part of the query and the last
734
     *                              delimiter.
735
     */
736
    public static function getFirstStatement($query, $delimiter = null)
737 1
    {
738
        $lexer = new Lexer($query, false, $delimiter);
739 1
        $list = $lexer->list;
740 1
741
        /**
742
         * Whether a full statement was found.
743
         *
744
         * @var bool $fullStatement
745
         */
746
        $fullStatement = false;
747 1
748
        /**
749
         * The first full statement.
750
         *
751
         * @var string $statement
752
         */
753
        $statement = '';
754 1
755
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
756 1
            $token = $list->tokens[$list->idx];
757 1
758
            if ($token->type === Token::TYPE_COMMENT) {
759 1
                continue;
760 1
            }
761
762
            $statement .= $token->token;
763 1
764
            if (($token->type === Token::TYPE_DELIMITER) && (!empty($token->token))) {
765 1
                $delimiter = $token->token;
766 1
                $fullStatement = true;
767 1
                break;
768 1
            }
769
        }
770 1
771
        // No statement was found so we return the entire query as being the
772
        // remaining part.
773
        if (!$fullStatement) {
774 1
            return array(null, $query, $delimiter);
775 1
        }
776
777
        // At least one query was found so we have to build the rest of the
778
        // remaining query.
779
        $query = '';
780 1
        for (++$list->idx; $list->idx < $list->count; ++$list->idx) {
781 1
            $query .= $list->tokens[$list->idx]->token;
782 1
        }
783 1
784
        return array(trim($statement), $query, $delimiter);
785 1
    }
786
787
    /**
788
     * Gets a starting offset of a specific clause.
789
     *
790
     * @param Statement  $statement The parsed query that has to be modified.
791
     * @param TokensList $list      The list of tokens.
792
     * @param string     $clause    The clause to be returned.
793
     *
794
     * @return int
795
     */
796
    public static function getClauseStartOffset($statement, $list, $clause)
797
    {
798
799
        /**
800
         * The index of the current clause.
801
         *
802
         * @var int $currIdx
803
         */
804
        $currIdx = 0;
0 ignored issues
show
Unused Code introduced by
$currIdx is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
805
806
        /**
807
         * The count of brackets.
808
         * We keep track of them so we won't insert the clause in a subquery.
809
         *
810
         * @var int $brackets
811
         */
812
        $brackets = 0;
813
814
        /**
815
         * The clauses of this type of statement and their index.
816
         *
817
         * @var array $clauses
818
         */
819
        $clauses = array_flip(array_keys($statement->getClauses()));
820
821
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
822
            $token = $list->tokens[$i];
823
824
            if ($token->type === Token::TYPE_COMMENT) {
825
                continue;
826
            }
827
828 View Code Duplication
            if ($token->type === Token::TYPE_OPERATOR) {
829
                if ($token->value === '(') {
830
                    ++$brackets;
831
                } elseif ($token->value === ')') {
832
                    --$brackets;
833
                }
834
            }
835
836
            if ($brackets == 0) {
837
                if (($token->type === Token::TYPE_KEYWORD)
838
                    && (isset($clauses[$token->value]))
839
                    && ($clause === $token->value)
840
                ) {
841
                    return $i;
842
                }
843
            }
844
        }
845
846
        return -1;
847
    }
848
}
849