Completed
Push — master ( 4cd61e...038f60 )
by Michal
03:53
created

Statement::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 3
nc 2
nop 2
crap 3
1
<?php
2
3
/**
4
 * The result of the parser is an array of statements are extensions of the
5
 * class defined here.
6
 *
7
 * A statement represents the result of parsing the lexemes.
8
 */
9
10
namespace PhpMyAdmin\SqlParser;
11
12
use PhpMyAdmin\SqlParser\Components\OptionsArray;
13
14
/**
15
 * Abstract statement definition.
16
 *
17
 * @category Statements
18
 *
19
 * @license  https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
20
 */
21
abstract class Statement
22
{
23
    /**
24
     * Options for this statement.
25
     *
26
     * The option would be the key and the value can be an integer or an array.
27
     *
28
     * The integer represents only the index used.
29
     *
30
     * The array may have two keys: `0` is used to represent the index used and
31
     * `1` is the type of the option (which may be 'var' or 'var='). Both
32
     * options mean they expect a value after the option (e.g. `A = B` or `A B`,
33
     * in which case `A` is the key and `B` is the value). The only difference
34
     * is in the building process. `var` options are built as `A B` and  `var=`
35
     * options are built as `A = B`
36
     *
37
     * Two options that can be used together must have different values for
38
     * indexes, else, when they will be used together, an error will occur.
39
     *
40
     * @var array
41
     */
42
    public static $OPTIONS = array();
43
44
    /**
45
     * The clauses of this statement, in order.
46
     *
47
     * The value attributed to each clause is used by the builder and it may
48
     * have one of the following values:
49
     *
50
     *     - 1 = 01 - add the clause only
51
     *     - 2 = 10 - add the keyword
52
     *     - 3 = 11 - add both the keyword and the clause
53
     *
54
     * @var array
55
     */
56
    public static $CLAUSES = array();
57
58
    /**
59
     * The options of this query.
60
     *
61
     * @var OptionsArray
62
     *
63
     * @see static::$OPTIONS
64
     */
65
    public $options;
66
67
    /**
68
     * The index of the first token used in this statement.
69
     *
70
     * @var int
71
     */
72
    public $first;
73
74
    /**
75
     * The index of the last token used in this statement.
76
     *
77
     * @var int
78
     */
79
    public $last;
80
81
    /**
82
     * Constructor.
83
     *
84
     * @param Parser     $parser the instance that requests parsing
0 ignored issues
show
Documentation introduced by
Should the type for parameter $parser not be null|Parser?

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...
85
     * @param TokensList $list   the list of tokens to be parsed
0 ignored issues
show
Documentation introduced by
Should the type for parameter $list not be null|TokensList?

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...
86
     */
87 247
    public function __construct(Parser $parser = null, TokensList $list = null)
88
    {
89 247
        if (($parser !== null) && ($list !== null)) {
90 245
            $this->parse($parser, $list);
91
        }
92 247
    }
93
94
    /**
95
     * Builds the string representation of this statement.
96
     *
97
     * @return string
98
     */
99 11
    public function build()
100
    {
101
        /**
102
         * Query to be returned.
103
         *
104
         * @var string
105
         */
106 11
        $query = '';
107
108
        /**
109
         * Clauses which were built already.
110
         *
111
         * It is required to keep track of built clauses because some fields,
112
         * for example `join` is used by multiple clauses (`JOIN`, `LEFT JOIN`,
113
         * `LEFT OUTER JOIN`, etc.). The same happens for `VALUE` and `VALUES`.
114
         *
115
         * A clause is considered built just after fields' value
116
         * (`$this->field`) was used in building.
117
         *
118
         * @var array
119
         */
120 11
        $built = array();
121
122
        /**
123
         * Statement's clauses.
124
         *
125
         * @var array
126
         */
127 11
        $clauses = $this->getClauses();
128
129 11
        foreach ($clauses as $clause) {
130
            /**
131
             * The name of the clause.
132
             *
133
             * @var string
134
             */
135 10
            $name = $clause[0];
136
137
            /**
138
             * The type of the clause.
139
             *
140
             * @see self::$CLAUSES
141
             *
142
             * @var int
143
             */
144 10
            $type = $clause[1];
145
146
            /**
147
             * The builder (parser) of this clause.
148
             *
149
             * @var Component
150
             */
151 10
            $class = Parser::$KEYWORD_PARSERS[$name]['class'];
152
153
            /**
154
             * The name of the field that is used as source for the builder.
155
             * Same field is used to store the result of parsing.
156
             *
157
             * @var string
158
             */
159 10
            $field = Parser::$KEYWORD_PARSERS[$name]['field'];
160
161
            // The field is empty, there is nothing to be built.
162 10
            if (empty($this->$field)) {
163 10
                continue;
164
            }
165
166
            // Checking if this field was already built.
167 10
            if ($type & 1) {
168 10
                if (!empty($built[$field])) {
169 2
                    continue;
170
                }
171 10
                $built[$field] = true;
172
            }
173
174
            // Checking if the name of the clause should be added.
175 10
            if ($type & 2) {
176 10
                $query .= $name . ' ';
177
            }
178
179
            // Checking if the result of the builder should be added.
180 10
            if ($type & 1) {
181 10
                $query .= $class::build($this->$field) . ' ';
182
            }
183
        }
184
185 11
        return $query;
186
    }
187
188
    /**
189
     * Parses the statements defined by the tokens list.
190
     *
191
     * @param Parser     $parser the instance that requests parsing
192
     * @param TokensList $list   the list of tokens to be parsed
193
     */
194 136
    public function parse(Parser $parser, TokensList $list)
195
    {
196
        /**
197
         * Array containing all list of clauses parsed.
198
         * This is used to check for duplicates.
199
         *
200
         * @var array
201
         */
202 136
        $parsedClauses = array();
203
204
        // This may be corrected by the parser.
205 136
        $this->first = $list->idx;
206
207
        /**
208
         * Whether options were parsed or not.
209
         * For statements that do not have any options this is set to `true` by
210
         * default.
211
         *
212
         * @var bool
213
         */
214 136
        $parsedOptions = empty(static::$OPTIONS);
215
216 136
        for (; $list->idx < $list->count; ++$list->idx) {
217
            /**
218
             * Token parsed at this moment.
219
             *
220
             * @var Token
221
             */
222 136
            $token = $list->tokens[$list->idx];
223
224
            // End of statement.
225 136
            if ($token->type === Token::TYPE_DELIMITER) {
226 135
                break;
227
            }
228
229
            // Checking if this closing bracket is the pair for a bracket
230
            // outside the statement.
231 136
            if (($token->value === ')') && ($parser->brackets > 0)) {
232 3
                --$parser->brackets;
233 3
                continue;
234
            }
235
236
            // Only keywords are relevant here. Other parts of the query are
237
            // processed in the functions below.
238 136
            if ($token->type !== Token::TYPE_KEYWORD) {
239 11 View Code Duplication
                if (($token->type !== TOKEN::TYPE_COMMENT)
240 11
                    && ($token->type !== Token::TYPE_WHITESPACE)
241
                ) {
242 6
                    $parser->error('Unexpected token.', $token);
243
                }
244 11
                continue;
245
            }
246
247
            // Unions are parsed by the parser because they represent more than
248
            // one statement.
249 136
            if (($token->value === 'UNION') || ($token->value === 'UNION ALL') || ($token->value === 'UNION DISTINCT')) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
250 5
                break;
251
            }
252
253 136
            $lastIdx = $list->idx;
254
255
            // ON DUPLICATE KEY UPDATE ...
256
            // has to be parsed in parent statement (INSERT or REPLACE)
257
            // so look for it and break
258 136
            if ($this instanceof \PhpMyAdmin\SqlParser\Statements\SelectStatement
259 136
                && $token->value === 'ON'
260
            ) {
261 2
                ++$list->idx; // Skip ON
262
263
                // look for ON DUPLICATE KEY UPDATE
264 2
                $first = $list->getNextOfType(Token::TYPE_KEYWORD);
265 2
                $second = $list->getNextOfType(Token::TYPE_KEYWORD);
266 2
                $third = $list->getNextOfType(Token::TYPE_KEYWORD);
267
268 2
                if ($first && $second && $third
269 2
                    && $first->value === 'DUPLICATE'
270 2
                    && $second->value === 'KEY'
271 2
                    && $third->value === 'UPDATE'
272
                ) {
273 2
                    $list->idx = $lastIdx;
274 2
                    break;
275
                }
276
            }
277 136
            $list->idx = $lastIdx;
278
279
            /**
280
             * The name of the class that is used for parsing.
281
             *
282
             * @var Component
283
             */
284 136
            $class = null;
285
286
            /**
287
             * The name of the field where the result of the parsing is stored.
288
             *
289
             * @var string
290
             */
291 136
            $field = null;
292
293
            /**
294
             * Parser's options.
295
             *
296
             * @var array
297
             */
298 136
            $options = array();
299
300
            // Looking for duplicated clauses.
301 136
            if ((!empty(Parser::$KEYWORD_PARSERS[$token->value]))
302 136
                || (!empty(Parser::$STATEMENT_PARSERS[$token->value]))
303
            ) {
304 136
                if (!empty($parsedClauses[$token->value])) {
305 2
                    $parser->error(
306 2
                        'This type of clause was previously parsed.',
307
                        $token
308
                    );
309 2
                    break;
310
                }
311 136
                $parsedClauses[$token->value] = true;
312
            }
313
314
            // Checking if this is the beginning of a clause.
315 136
            if (!empty(Parser::$KEYWORD_PARSERS[$token->value])) {
316 135
                $class = Parser::$KEYWORD_PARSERS[$token->value]['class'];
317 135
                $field = Parser::$KEYWORD_PARSERS[$token->value]['field'];
318 135
                if (!empty(Parser::$KEYWORD_PARSERS[$token->value]['options'])) {
319 102
                    $options = Parser::$KEYWORD_PARSERS[$token->value]['options'];
320
                }
321
            }
322
323
            // Checking if this is the beginning of the statement.
324 136
            if (!empty(Parser::$STATEMENT_PARSERS[$token->value])) {
325 136
                if ((!empty(static::$CLAUSES)) // Undefined for some statements.
326 136
                    && (empty(static::$CLAUSES[$token->value]))
327
                ) {
328
                    // Some keywords (e.g. `SET`) may be the beginning of a
329
                    // statement and a clause.
330
                    // If such keyword was found and it cannot be a clause of
331
                    // this statement it means it is a new statement, but no
332
                    // delimiter was found between them.
333 1
                    $parser->error(
334 1
                        'A new statement was found, but no delimiter between it and the previous one.',
335
                        $token
336
                    );
337 1
                    break;
338
                }
339 136
                if (!$parsedOptions) {
340 125
                    if (empty(static::$OPTIONS[$token->value])) {
341
                        // Skipping keyword because if it is not a option.
342 124
                        ++$list->idx;
343
                    }
344 125
                    $this->options = OptionsArray::parse(
345
                        $parser,
346
                        $list,
347 125
                        static::$OPTIONS
348
                    );
349 136
                    $parsedOptions = true;
350
                }
351 89
            } elseif ($class === null) {
352
                // Handle special end options in Select statement
353
                // See Statements\SelectStatement::$END_OPTIONS
354 11
                if ($this instanceof \PhpMyAdmin\SqlParser\Statements\SelectStatement
355 10
                    && ($token->value === 'FOR UPDATE'
356 11
                    || $token->value === 'LOCK IN SHARE MODE')
357
                ) {
358 4
                    $this->end_options = OptionsArray::parse(
0 ignored issues
show
Bug introduced by
The property end_options does not seem to exist. Did you mean OPTIONS?

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...
359
                        $parser,
360
                        $list,
361 4
                        static::$END_OPTIONS
362
                    );
363
                } else {
364
                    // There is no parser for this keyword and isn't the beginning
365
                    // of a statement (so no options) either.
366 7
                    $parser->error('Unrecognized keyword.', $token);
367 7
                    continue;
368
                }
369
            }
370
371 136
            $this->before($parser, $list, $token);
372
373
            // Parsing this keyword.
374 136
            if ($class !== null) {
375 135
                ++$list->idx; // Skipping keyword or last option.
376 135
                $this->$field = $class::parse($parser, $list, $options);
377
            }
378
379 136
            $this->after($parser, $list, $token);
380
        }
381
382
        // This may be corrected by the parser.
383 136
        $this->last = --$list->idx; // Go back to last used token.
384 136
    }
385
386
    /**
387
     * Function called before the token is processed.
388
     *
389
     * @param Parser     $parser the instance that requests parsing
390
     * @param TokensList $list   the list of tokens to be parsed
391
     * @param Token      $token  the token that is being parsed
392
     */
393 129
    public function before(Parser $parser, TokensList $list, Token $token)
394
    {
395 129
    }
396
397
    /**
398
     * Function called after the token was processed.
399
     *
400
     * @param Parser     $parser the instance that requests parsing
401
     * @param TokensList $list   the list of tokens to be parsed
402
     * @param Token      $token  the token that is being parsed
403
     */
404 132
    public function after(Parser $parser, TokensList $list, Token $token)
405
    {
406 132
    }
407
408
    /**
409
     * Gets the clauses of this statement.
410
     *
411
     * @return array
412
     */
413 159
    public function getClauses()
414
    {
415 159
        return static::$CLAUSES;
416
    }
417
418
    /**
419
     * Builds the string representation of this statement.
420
     *
421
     * @see static::build
422
     *
423
     * @return string
424
     */
425 4
    public function __toString()
426
    {
427 4
        return $this->build();
428
    }
429
430
    /**
431
     * Validates the order of the clauses in parsed statement
432
     * Ideally this should be called after successfully
433
     * completing the parsing of each statement.
434
     *
435
     * @param Parser     $parser the instance that requests parsing
436
     * @param TokensList $list   the list of tokens to be parsed
437
     *
438
     * @return bool
439
     */
440 245
    public function validateClauseOrder($parser, $list)
441
    {
442 245
        $clauses = array_flip(array_keys($this->getClauses()));
443
444 245
        if (empty($clauses)
445 245
            || count($clauses) == 0
446
        ) {
447 114
            return true;
448
        }
449
450 137
        $minIdx = -1;
451
452
        /**
453
         * For tracking JOIN clauses in a query
454
         *   = 0 - JOIN not found till now
455
         *   > 0 - Index of first JOIN clause in the statement
456
         *
457
         * @var int
458
         */
459 137
        $minJoin = 0;
460
461
        /**
462
         * For tracking JOIN clauses in a query
463
         *   = 0 - JOIN not found till now
464
         *   > 0 - Index of last JOIN clause
465
         *         (which appears together with other JOINs)
466
         *         in the statement
467
         *
468
         * @var int
469
         */
470 137
        $maxJoin = 0;
471
472 137
        $error = 0;
473 137
        $lastIdx = 0;
474 137
        foreach ($clauses as $clauseType => $index) {
475 137
            $clauseStartIdx = Utils\Query::getClauseStartOffset(
476
                $this,
477
                $list,
478
                $clauseType
479
            );
480
481
            // Handle ordering of Multiple Joins in a query
482 137
            if ($clauseStartIdx != -1) {
483 136
                if ($minJoin === 0 && stripos($clauseType, 'JOIN')) {
484
                    // First JOIN clause is detected
485 14
                    $minJoin = $maxJoin = $clauseStartIdx;
486 136
                } elseif ($minJoin !== 0 && ! stripos($clauseType, 'JOIN')) {
487
                    // After a previous JOIN clause, a non-JOIN clause has been detected
488 5
                    $maxJoin = $lastIdx;
489 136
                } elseif ($maxJoin < $clauseStartIdx && stripos($clauseType, 'JOIN')) {
490 1
                    $error = 1;
491
                }
492
            }
493
494 137
            if ($clauseStartIdx != -1 && $clauseStartIdx < $minIdx) {
495 6
                if ($minJoin === 0 || $error === 1) {
496 5
                    $token = $list->tokens[$clauseStartIdx];
497 5
                    $parser->error(
498 5
                        'Unexpected ordering of clauses.',
499
                        $token
500
                    );
501
502 5
                    return false;
503
                }
504 1
                $minIdx = $clauseStartIdx;
505 137
            } elseif ($clauseStartIdx != -1) {
506 136
                $minIdx = $clauseStartIdx;
507
            }
508
509 137
            $lastIdx = ($clauseStartIdx !== -1) ? $clauseStartIdx : $lastIdx;
510
        }
511
512 132
        return true;
513
    }
514
}
515