Completed
Push — master ( a87ad6...72f952 )
by Michal
04:22
created

Query::_getFlagsSelect()   F

Complexity

Conditions 20
Paths 9728

Size

Total Lines 66
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 66
ccs 36
cts 36
cp 1
rs 2.774
c 0
b 0
f 0
cc 20
eloc 36
nc 9728
nop 2
crap 20

How to fix   Long Method    Complexity   

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
 * Statement utilities.
5
 */
6
7
namespace PhpMyAdmin\SqlParser\Utils;
8
9
use PhpMyAdmin\SqlParser\Lexer;
10
use PhpMyAdmin\SqlParser\Parser;
11
use PhpMyAdmin\SqlParser\Statement;
12
use PhpMyAdmin\SqlParser\Token;
13
use PhpMyAdmin\SqlParser\TokensList;
14
use PhpMyAdmin\SqlParser\Components\Expression;
15
use PhpMyAdmin\SqlParser\Statements\AlterStatement;
16
use PhpMyAdmin\SqlParser\Statements\AnalyzeStatement;
17
use PhpMyAdmin\SqlParser\Statements\CallStatement;
18
use PhpMyAdmin\SqlParser\Statements\CheckStatement;
19
use PhpMyAdmin\SqlParser\Statements\ChecksumStatement;
20
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
21
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
22
use PhpMyAdmin\SqlParser\Statements\DropStatement;
23
use PhpMyAdmin\SqlParser\Statements\ExplainStatement;
24
use PhpMyAdmin\SqlParser\Statements\InsertStatement;
25
use PhpMyAdmin\SqlParser\Statements\OptimizeStatement;
26
use PhpMyAdmin\SqlParser\Statements\RenameStatement;
27
use PhpMyAdmin\SqlParser\Statements\RepairStatement;
28
use PhpMyAdmin\SqlParser\Statements\ReplaceStatement;
29
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
30
use PhpMyAdmin\SqlParser\Statements\ShowStatement;
31
use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
32
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
33
34
/**
35
 * Statement utilities.
36
 *
37
 * @category   Statement
38
 *
39
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
40
 */
41
class Query
42
{
43
    /**
44
     * Functions that set the flag `is_func`.
45
     *
46
     * @var array
47
     */
48
    public static $FUNCTIONS = array(
49
        'SUM', 'AVG', 'STD', 'STDDEV', 'MIN', 'MAX', 'BIT_OR', 'BIT_AND',
50
    );
51
52
    public static $ALLFLAGS = array(
53
        /*
54
         * select ... DISTINCT ...
55
         */
56
        'distinct' => false,
57
58
        /*
59
         * drop ... DATABASE ...
60
         */
61
        'drop_database' => false,
62
63
        /*
64
         * ... GROUP BY ...
65
         */
66
        'group' => false,
67
68
        /*
69
         * ... HAVING ...
70
         */
71
        'having' => false,
72
73
        /*
74
         * INSERT ...
75
         * or
76
         * REPLACE ...
77
         * or
78
         * DELETE ...
79
         */
80
        'is_affected' => false,
81
82
        /*
83
         * select ... PROCEDURE ANALYSE( ... ) ...
84
         */
85
        'is_analyse' => false,
86
87
        /*
88
         * select COUNT( ... ) ...
89
         */
90
        'is_count' => false,
91
92
        /*
93
         * DELETE ...
94
         */
95
        'is_delete' => false, // @deprecated; use `querytype`
96
97
        /*
98
         * EXPLAIN ...
99
         */
100
        'is_explain' => false, // @deprecated; use `querytype`
101
102
        /*
103
         * select ... INTO OUTFILE ...
104
         */
105
        'is_export' => false,
106
107
        /*
108
         * select FUNC( ... ) ...
109
         */
110
        'is_func' => false,
111
112
        /*
113
         * select ... GROUP BY ...
114
         * or
115
         * select ... HAVING ...
116
         */
117
        'is_group' => false,
118
119
        /*
120
         * INSERT ...
121
         * or
122
         * REPLACE ...
123
         * or
124
         * 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...
125
         */
126
        'is_insert' => false,
127
128
        /*
129
         * ANALYZE ...
130
         * or
131
         * CHECK ...
132
         * or
133
         * CHECKSUM ...
134
         * or
135
         * OPTIMIZE ...
136
         * or
137
         * REPAIR ...
138
         */
139
        'is_maint' => false,
140
141
        /*
142
         * CALL ...
143
         */
144
        'is_procedure' => false,
145
146
        /*
147
         * REPLACE ...
148
         */
149
        'is_replace' => false, // @deprecated; use `querytype`
150
151
        /*
152
         * SELECT ...
153
         */
154
        'is_select' => false, // @deprecated; use `querytype`
155
156
        /*
157
         * SHOW ...
158
         */
159
        'is_show' => false, // @deprecated; use `querytype`
160
161
        /*
162
         * Contains a subquery.
163
         */
164
        'is_subquery' => false,
165
166
        /*
167
         * ... JOIN ...
168
         */
169
        'join' => false,
170
171
        /*
172
         * ... LIMIT ...
173
         */
174
        'limit' => false,
175
176
        /*
177
         * 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...
178
         */
179
        'offset' => false,
180
181
        /*
182
         * ... ORDER ...
183
         */
184
        'order' => false,
185
186
        /*
187
         * The type of the query (which is usually the first keyword of
188
         * the statement).
189
         */
190
        'querytype' => false,
191
192
        /*
193
         * Whether a page reload is required.
194
         */
195
        'reload' => false,
196
197
        /*
198
         * SELECT ... FROM ...
199
         */
200
        'select_from' => false,
201
202
        /*
203
         * ... UNION ...
204
         */
205
        'union' => false,
206
    );
207
208
    /**
209
     * Gets an array with flags select statement has.
210
     *
211
     * @param Statement|null $statement the statement to be processed
212
     * @param array          $flagsi    flags set so far
0 ignored issues
show
Documentation introduced by
There is no parameter named $flagsi. Did you maybe mean $flags?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
213
     *
214
     * @return array
215
     */
216 13
    private static function _getFlagsSelect($statement, $flags)
217
    {
218 13
        $flags['querytype'] = 'SELECT';
219 13
        $flags['is_select'] = true;
220
221 13
        if (!empty($statement->from)) {
0 ignored issues
show
Bug introduced by
The property from does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
222 11
            $flags['select_from'] = true;
223
        }
224
225 13
        if ($statement->options->has('DISTINCT')) {
226 1
            $flags['distinct'] = true;
227
        }
228
229 13
        if ((!empty($statement->group)) || (!empty($statement->having))) {
0 ignored issues
show
Bug introduced by
The property group does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
Bug introduced by
The property having does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
230 2
            $flags['is_group'] = true;
231
        }
232
233 13
        if ((!empty($statement->into))
0 ignored issues
show
Bug introduced by
The property into does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
234 13
            && ($statement->into->type === 'OUTFILE')
235
        ) {
236 1
            $flags['is_export'] = true;
237
        }
238
239 13
        $expressions = $statement->expr;
0 ignored issues
show
Bug introduced by
The property expr does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
240 13
        if (!empty($statement->join)) {
241 1
            foreach ($statement->join as $join) {
0 ignored issues
show
Bug introduced by
The property join does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
242 1
                $expressions[] = $join->expr;
243
            }
244
        }
245
246 13
        foreach ($expressions as $expr) {
247 13
            if (!empty($expr->function)) {
248 1
                if ($expr->function === 'COUNT') {
249 1
                    $flags['is_count'] = true;
250 1
                } elseif (in_array($expr->function, static::$FUNCTIONS)) {
251 1
                    $flags['is_func'] = true;
252
                }
253
            }
254 13
            if (!empty($expr->subquery)) {
255 13
                $flags['is_subquery'] = true;
256
            }
257
        }
258
259 13
        if ((!empty($statement->procedure))
0 ignored issues
show
Bug introduced by
The property procedure does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
260 13
            && ($statement->procedure->name === 'ANALYSE')
261
        ) {
262 1
            $flags['is_analyse'] = true;
263
        }
264
265 13
        if (!empty($statement->group)) {
266 1
            $flags['group'] = true;
267
        }
268
269 13
        if (!empty($statement->having)) {
270 1
            $flags['having'] = true;
271
        }
272
273 13
        if (!empty($statement->union)) {
0 ignored issues
show
Bug introduced by
The property union does not seem to exist in PhpMyAdmin\SqlParser\Statement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
274 1
            $flags['union'] = true;
275
        }
276
277 13
        if (!empty($statement->join)) {
278 1
            $flags['join'] = true;
279
        }
280 13
        return $flags;
281
    }
282
283
    /**
284
     * Gets an array with flags this statement has.
285
     *
286
     * @param Statement|null $statement the statement to be processed
287
     * @param bool           $all       if `false`, false values will not be included
288
     *
289
     * @return array
290
     */
291 29
    public static function getFlags($statement, $all = false)
292
    {
293 29
        $flags = array();
294 29
        if ($all) {
295 1
            $flags = self::$ALLFLAGS;
296
        }
297
298 29
        if ($statement instanceof AlterStatement) {
299 1
            $flags['querytype'] = 'ALTER';
300 1
            $flags['reload'] = true;
301
        } elseif ($statement instanceof CreateStatement) {
302 1
            $flags['querytype'] = 'CREATE';
303 1
            $flags['reload'] = true;
304 20
        } elseif ($statement instanceof AnalyzeStatement) {
305 1
            $flags['querytype'] = 'ANALYZE';
306 1
            $flags['is_maint'] = true;
307 1
        } elseif ($statement instanceof CheckStatement) {
308 1
            $flags['querytype'] = 'CHECK';
309 1
            $flags['is_maint'] = true;
310 19
        } elseif ($statement instanceof ChecksumStatement) {
311 1
            $flags['querytype'] = 'CHECKSUM';
312 1
            $flags['is_maint'] = true;
313 19
        } elseif ($statement instanceof OptimizeStatement) {
314 1
            $flags['querytype'] = 'OPTIMIZE';
315 1
            $flags['is_maint'] = true;
316 19
        } elseif ($statement instanceof RepairStatement) {
317 1
            $flags['querytype'] = 'REPAIR';
318 1
            $flags['is_maint'] = true;
319
        } elseif ($statement instanceof CallStatement) {
320 1
            $flags['querytype'] = 'CALL';
321 1
            $flags['is_procedure'] = true;
322
        } elseif ($statement instanceof DeleteStatement) {
323 1
            $flags['querytype'] = 'DELETE';
324 1
            $flags['is_delete'] = true;
325 1
            $flags['is_affected'] = true;
326
        } elseif ($statement instanceof DropStatement) {
327 2
            $flags['querytype'] = 'DROP';
328 2
            $flags['reload'] = true;
329
330 2
            if (($statement->options->has('DATABASE')
331 2
                || ($statement->options->has('SCHEMA')))
332
            ) {
333 2
                $flags['drop_database'] = true;
334
            }
335
        } elseif ($statement instanceof ExplainStatement) {
336 1
            $flags['querytype'] = 'EXPLAIN';
337 1
            $flags['is_explain'] = true;
338
        } elseif ($statement instanceof InsertStatement) {
339 1
            $flags['querytype'] = 'INSERT';
340 1
            $flags['is_affected'] = true;
341 1
            $flags['is_insert'] = true;
342
        } elseif ($statement instanceof ReplaceStatement) {
343 1
            $flags['querytype'] = 'REPLACE';
344 1
            $flags['is_affected'] = true;
345 1
            $flags['is_replace'] = true;
346 1
            $flags['is_insert'] = true;
347
        } elseif ($statement instanceof SelectStatement) {
348 13
            $flags = self::_getFlagsSelect($statement, $flags);
349
        } elseif ($statement instanceof ShowStatement) {
350 1
            $flags['querytype'] = 'SHOW';
351 1
            $flags['is_show'] = true;
352
        } elseif ($statement instanceof UpdateStatement) {
353 1
            $flags['querytype'] = 'UPDATE';
354 1
            $flags['is_affected'] = true;
355
        }
356
357 29
        if (($statement instanceof SelectStatement)
358 17
            || ($statement instanceof UpdateStatement)
359 29
            || ($statement instanceof DeleteStatement)
360
        ) {
361 15
            if (!empty($statement->limit)) {
362 2
                $flags['limit'] = true;
363
            }
364 15
            if (!empty($statement->order)) {
365 2
                $flags['order'] = true;
366
            }
367
        }
368
369 29
        return $flags;
370
    }
371
372
    /**
373
     * Parses a query and gets all information about it.
374
     *
375
     * @param string $query the query to be parsed
376
     *
377
     * @return array The array returned is the one returned by
378
     *               `static::getFlags()`, with the following keys added:
379
     *               - parser - the parser used to analyze the query;
380
     *               - statement - the first statement resulted from parsing;
381
     *               - select_tables - the real name of the tables selected;
382
     *               if there are no table names in the `SELECT`
383
     *               expressions, the table names are fetched from the
384
     *               `FROM` expressions
385
     *               - select_expr - selected expressions
386
     */
387 1
    public static function getAll($query)
388
    {
389 1
        $parser = new Parser($query);
390
391 1
        if (empty($parser->statements[0])) {
392 1
            return static::getFlags(null, true);
393
        }
394
395 1
        $statement = $parser->statements[0];
396
397 1
        $ret = static::getFlags($statement, true);
398
399 1
        $ret['parser'] = $parser;
400 1
        $ret['statement'] = $statement;
401
402 1
        if ($statement instanceof SelectStatement) {
403 1
            $ret['select_tables'] = array();
404 1
            $ret['select_expr'] = array();
405
406
            // Finding tables' aliases and their associated real names.
407 1
            $tableAliases = array();
408 1
            foreach ($statement->from as $expr) {
409 1
                if ((isset($expr->table)) && ($expr->table !== '')
410 1
                    && (isset($expr->alias)) && ($expr->alias !== '')
411
                ) {
412 1
                    $tableAliases[$expr->alias] = array(
413 1
                        $expr->table,
414 1
                        isset($expr->database) ? $expr->database : null,
415
                    );
416
                }
417
            }
418
419
            // Trying to find selected tables only from the select expression.
420
            // Sometimes, this is not possible because the tables aren't defined
421
            // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film).
422 1
            foreach ($statement->expr as $expr) {
423 1
                if ((isset($expr->table)) && ($expr->table !== '')) {
424 1
                    if (isset($tableAliases[$expr->table])) {
425 1
                        $arr = $tableAliases[$expr->table];
426
                    } else {
427
                        $arr = array(
428 1
                            $expr->table,
429 1
                            ((isset($expr->database)) && ($expr->database !== '')) ?
430 1
                                $expr->database : null,
431
                        );
432
                    }
433 1
                    if (!in_array($arr, $ret['select_tables'])) {
434 1
                        $ret['select_tables'][] = $arr;
435
                    }
436
                } else {
437 1
                    $ret['select_expr'][] = $expr->expr;
438
                }
439
            }
440
441
            // If no tables names were found in the SELECT clause or if there
442
            // are expressions like * or COUNT(*), etc. tables names should be
443
            // extracted from the FROM clause.
444 1
            if (empty($ret['select_tables'])) {
445 1
                foreach ($statement->from as $expr) {
446 1
                    if ((isset($expr->table)) && ($expr->table !== '')) {
447
                        $arr = array(
448 1
                            $expr->table,
449 1
                            ((isset($expr->database)) && ($expr->database !== '')) ?
450 1
                                $expr->database : null,
451
                        );
452 1
                        if (!in_array($arr, $ret['select_tables'])) {
453 1
                            $ret['select_tables'][] = $arr;
454
                        }
455
                    }
456
                }
457
            }
458
        }
459
460 1
        return $ret;
461
    }
462
463
    /**
464
     * Gets a list of all tables used in this statement.
465
     *
466
     * @param Statement $statement statement to be scanned
467
     *
468
     * @return array
469
     */
470 7
    public static function getTables($statement)
471
    {
472 7
        $expressions = array();
473
474 7
        if (($statement instanceof InsertStatement)
475 7
            || ($statement instanceof ReplaceStatement)
476
        ) {
477 1
            $expressions = array($statement->into->dest);
478
        } elseif ($statement instanceof UpdateStatement) {
479 1
            $expressions = $statement->tables;
480
        } elseif (($statement instanceof SelectStatement)
481 5
            || ($statement instanceof DeleteStatement)
482
        ) {
483 1
            $expressions = $statement->from;
484
        } elseif (($statement instanceof AlterStatement)
485 4
            || ($statement instanceof TruncateStatement)
486
        ) {
487 1
            $expressions = array($statement->table);
488
        } elseif ($statement instanceof DropStatement) {
489 2
            if (!$statement->options->has('TABLE')) {
490
                // No tables are dropped.
491 1
                return array();
492
            }
493 1
            $expressions = $statement->fields;
494
        } elseif ($statement instanceof RenameStatement) {
495 1
            foreach ($statement->renames as $rename) {
496 1
                $expressions[] = $rename->old;
497
            }
498
        }
499
500 6
        $ret = array();
501 6
        foreach ($expressions as $expr) {
502 6
            if (!empty($expr->table)) {
503 6
                $expr->expr = null; // Force rebuild.
504 6
                $expr->alias = null; // Aliases are not required.
505 6
                $ret[] = Expression::build($expr);
506
            }
507
        }
508
509 6
        return $ret;
510
    }
511
512
    /**
513
     * Gets a specific clause.
514
     *
515
     * @param Statement  $statement the parsed query that has to be modified
516
     * @param TokensList $list      the list of tokens
517
     * @param string     $clause    the clause to be returned
518
     * @param int|string $type      The type of the search.
519
     *                              If int,
520
     *                              -1 for everything that was before
521
     *                              0 only for the clause
522
     *                              1 for everything after
523
     *                              If string, the name of the first clause that
524
     *                              should not be included.
525
     * @param bool       $skipFirst whether to skip the first keyword in clause
526
     *
527
     * @return string
528
     */
529 4
    public static function getClause($statement, $list, $clause, $type = 0, $skipFirst = true)
530
    {
531
        /**
532
         * The index of the current clause.
533
         *
534
         * @var int
535
         */
536 4
        $currIdx = 0;
537
538
        /**
539
         * The count of brackets.
540
         * We keep track of them so we won't insert the clause in a subquery.
541
         *
542
         * @var int
543
         */
544 4
        $brackets = 0;
545
546
        /**
547
         * The string to be returned.
548
         *
549
         * @var string
550
         */
551 4
        $ret = '';
552
553
        /**
554
         * The clauses of this type of statement and their index.
555
         *
556
         * @var array
557
         */
558 4
        $clauses = array_flip(array_keys($statement->getClauses()));
559
560
        /**
561
         * Lexer used for lexing the clause.
562
         *
563
         * @var Lexer
564
         */
565 4
        $lexer = new Lexer($clause);
566
567
        /**
568
         * The type of this clause.
569
         *
570
         * @var string
571
         */
572 4
        $clauseType = $lexer->list->getNextOfType(Token::TYPE_KEYWORD)->keyword;
573
574
        /**
575
         * The index of this clause.
576
         *
577
         * @var int
578
         */
579 4
        $clauseIdx = $clauses[$clauseType];
580
581 4
        $firstClauseIdx = $clauseIdx;
582 4
        $lastClauseIdx = $clauseIdx + 1;
583
584
        // Determining the behavior of this function.
585 4
        if ($type === -1) {
586 3
            $firstClauseIdx = -1; // Something small enough.
587 3
            $lastClauseIdx = $clauseIdx - 1;
588 4
        } elseif ($type === 1) {
589 3
            $firstClauseIdx = $clauseIdx + 1;
590 3
            $lastClauseIdx = 10000; // Something big enough.
591 3
        } elseif (is_string($type)) {
592 2
            if ($clauses[$type] > $clauseIdx) {
593 1
                $firstClauseIdx = $clauseIdx + 1;
594 1
                $lastClauseIdx = $clauses[$type] - 1;
595
            } else {
596 1
                $firstClauseIdx = $clauses[$type] + 1;
597 1
                $lastClauseIdx = $clauseIdx - 1;
598
            }
599
        }
600
601
        // This option is unavailable for multiple clauses.
602 4
        if ($type !== 0) {
603 4
            $skipFirst = false;
604
        }
605
606 4
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
607 4
            $token = $list->tokens[$i];
608
609 4
            if ($token->type === Token::TYPE_COMMENT) {
610 1
                continue;
611
            }
612
613 4 View Code Duplication
            if ($token->type === Token::TYPE_OPERATOR) {
614 4
                if ($token->value === '(') {
615 3
                    ++$brackets;
616 4
                } elseif ($token->value === ')') {
617 3
                    --$brackets;
618
                }
619
            }
620
621 4
            if ($brackets == 0) {
622
                // Checking if the section was changed.
623 4
                if (($token->type === Token::TYPE_KEYWORD)
624 4
                    && (isset($clauses[$token->keyword]))
625 4
                    && ($clauses[$token->keyword] >= $currIdx)
626
                ) {
627 4
                    $currIdx = $clauses[$token->keyword];
628 4
                    if (($skipFirst) && ($currIdx == $clauseIdx)) {
629
                        // This token is skipped (not added to the old
630
                        // clause) because it will be replaced.
631 1
                        continue;
632
                    }
633
                }
634
            }
635
636 4
            if (($firstClauseIdx <= $currIdx) && ($currIdx <= $lastClauseIdx)) {
637 4
                $ret .= $token->token;
638
            }
639
        }
640
641 4
        return trim($ret);
642
    }
643
644
    /**
645
     * Builds a query by rebuilding the statement from the tokens list supplied
646
     * and replaces a clause.
647
     *
648
     * It is a very basic version of a query builder.
649
     *
650
     * @param Statement  $statement the parsed query that has to be modified
651
     * @param TokensList $list      the list of tokens
652
     * @param string     $old       The type of the clause that should be
653
     *                              replaced. This can be an entire clause.
654
     * @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...
655
     *                              it is considered to be equal with `$old`.
656
     * @param bool       $onlyType  whether only the type of the clause should
657
     *                              be replaced or the entire clause
658
     *
659
     * @return string
660
     */
661 3
    public static function replaceClause($statement, $list, $old, $new = null, $onlyType = false)
662
    {
663
        // 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...
664
665 3
        if ($new === null) {
666 2
            $new = $old;
667
        }
668
669 3
        if ($onlyType) {
670 1
            return static::getClause($statement, $list, $old, -1, false) . ' ' .
671 1
                $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' .
672 1
                static::getClause($statement, $list, $old, 1, false);
673
        }
674
675 2
        return static::getClause($statement, $list, $old, -1, false) . ' ' .
676 2
            $new . ' ' . static::getClause($statement, $list, $old, 1, false);
677
    }
678
679
    /**
680
     * Builds a query by rebuilding the statement from the tokens list supplied
681
     * and replaces multiple clauses.
682
     *
683
     * @param Statement  $statement the parsed query that has to be modified
684
     * @param TokensList $list      the list of tokens
685
     * @param array      $ops       Clauses to be replaced. Contains multiple
686
     *                              arrays having two values: array($old, $new).
687
     *                              Clauses must be sorted.
688
     *
689
     * @return string
690
     */
691 1
    public static function replaceClauses($statement, $list, array $ops)
692
    {
693 1
        $count = count($ops);
694
695
        // Nothing to do.
696 1
        if ($count === 0) {
697 1
            return '';
698
        }
699
700
        /**
701
         * Value to be returned.
702
         *
703
         * @var string
704
         */
705 1
        $ret = '';
706
707
        // If there is only one clause, `replaceClause()` should be used.
708 1
        if ($count === 1) {
709 1
            return static::replaceClause(
710
                $statement,
711
                $list,
712 1
                $ops[0][0],
713 1
                $ops[0][1]
714
            );
715
        }
716
717
        // Adding everything before first replacement.
718 1
        $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' ';
719
720
        // Doing replacements.
721 1
        for ($i = 0; $i < $count; ++$i) {
722 1
            $ret .= $ops[$i][1] . ' ';
723
724
            // Adding everything between this and next replacement.
725 1
            if ($i + 1 !== $count) {
726 1
                $ret .= static::getClause($statement, $list, $ops[$i][0], $ops[$i + 1][0]) . ' ';
727
            }
728
        }
729
730
        // Adding everything after the last replacement.
731 1
        $ret .= static::getClause($statement, $list, $ops[$count - 1][0], 1);
732
733 1
        return $ret;
734
    }
735
736
    /**
737
     * Gets the first full statement in the query.
738
     *
739
     * @param string $query     the query to be analyzed
740
     * @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...
741
     *
742
     * @return array array containing the first full query, the
743
     *               remaining part of the query and the last
744
     *               delimiter
745
     */
746 1
    public static function getFirstStatement($query, $delimiter = null)
747
    {
748 1
        $lexer = new Lexer($query, false, $delimiter);
749 1
        $list = $lexer->list;
750
751
        /**
752
         * Whether a full statement was found.
753
         *
754
         * @var bool
755
         */
756 1
        $fullStatement = false;
757
758
        /**
759
         * The first full statement.
760
         *
761
         * @var string
762
         */
763 1
        $statement = '';
764
765 1
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
766 1
            $token = $list->tokens[$list->idx];
767
768 1
            if ($token->type === Token::TYPE_COMMENT) {
769 1
                continue;
770
            }
771
772 1
            $statement .= $token->token;
773
774 1
            if (($token->type === Token::TYPE_DELIMITER) && (!empty($token->token))) {
775 1
                $delimiter = $token->token;
776 1
                $fullStatement = true;
777 1
                break;
778
            }
779
        }
780
781
        // No statement was found so we return the entire query as being the
782
        // remaining part.
783 1
        if (!$fullStatement) {
784 1
            return array(null, $query, $delimiter);
785
        }
786
787
        // At least one query was found so we have to build the rest of the
788
        // remaining query.
789 1
        $query = '';
790 1
        for (++$list->idx; $list->idx < $list->count; ++$list->idx) {
791 1
            $query .= $list->tokens[$list->idx]->token;
792
        }
793
794 1
        return array(trim($statement), $query, $delimiter);
795
    }
796
797
    /**
798
     * Gets a starting offset of a specific clause.
799
     *
800
     * @param Statement  $statement the parsed query that has to be modified
801
     * @param TokensList $list      the list of tokens
802
     * @param string     $clause    the clause to be returned
803
     *
804
     * @return int
805
     */
806 136
    public static function getClauseStartOffset($statement, $list, $clause)
807
    {
808
        /**
809
         * The count of brackets.
810
         * We keep track of them so we won't insert the clause in a subquery.
811
         *
812
         * @var int
813
         */
814 136
        $brackets = 0;
815
816
        /**
817
         * The clauses of this type of statement and their index.
818
         *
819
         * @var array
820
         */
821 136
        $clauses = array_flip(array_keys($statement->getClauses()));
822
823 136
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
824 136
            $token = $list->tokens[$i];
825
826 136
            if ($token->type === Token::TYPE_COMMENT) {
827 10
                continue;
828
            }
829
830 136 View Code Duplication
            if ($token->type === Token::TYPE_OPERATOR) {
831 108
                if ($token->value === '(') {
832 42
                    ++$brackets;
833 108
                } elseif ($token->value === ')') {
834 42
                    --$brackets;
835
                }
836
            }
837
838 136
            if ($brackets == 0) {
839 136
                if (($token->type === Token::TYPE_KEYWORD)
840 136
                    && (isset($clauses[$token->keyword]))
841 136
                    && ($clause === $token->keyword)
842
                ) {
843 135
                    return $i;
844 128
                } elseif ($token->keyword === 'UNION') {
845 5
                    return -1;
846
                }
847
            }
848
        }
849
850 128
        return -1;
851
    }
852
}
853