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

Statement::validateClauseOrder()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 20
nc 5
nop 2
dl 0
loc 32
ccs 0
cts 0
cp 0
crap 56
rs 6.7272
c 0
b 0
f 0
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
 * @package SqlParser
10
 */
11
namespace SqlParser;
12
13
use SqlParser\Components\OptionsArray;
14
15
/**
16
 * Abstract statement definition.
17
 *
18
 * @category Statements
19
 * @package  SqlParser
20
 * @license  https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
21
 */
22
abstract class Statement
23
{
24
25
    /**
26
     * Options for this statement.
27
     *
28
     * The option would be the key and the value can be an integer or an array.
29
     *
30
     * The integer represents only the index used.
31
     *
32
     * The array may have two keys: `0` is used to represent the index used and
33
     * `1` is the type of the option (which may be 'var' or 'var='). Both
34
     * options mean they expect a value after the option (e.g. `A = B` or `A B`,
35
     * in which case `A` is the key and `B` is the value). The only difference
36
     * is in the building process. `var` options are built as `A B` and  `var=`
37
     * options are built as `A = B`
38
     *
39
     * Two options that can be used together must have different values for
40
     * indexes, else, when they will be used together, an error will occur.
41
     *
42
     * @var array
43
     */
44
    public static $OPTIONS = array();
45
46
    /**
47
     * The clauses of this statement, in order.
48
     *
49
     * The value attributed to each clause is used by the builder and it may
50
     * have one of the following values:
51
     *
52
     *     - 1 = 01 - add the clause only
53
     *     - 2 = 10 - add the keyword
54
     *     - 3 = 11 - add both the keyword and the clause
55
     *
56
     * @var array
57
     */
58
    public static $CLAUSES = array();
59
60
    /**
61
     * The options of this query.
62
     *
63
     * @var OptionsArray
64
     *
65
     * @see static::$OPTIONS
66
     */
67
    public $options;
68
69
    /**
70
     * The index of the first token used in this statement.
71
     *
72
     * @var int
73
     */
74
    public $first;
75
76
    /**
77
     * The index of the last token used in this statement.
78
     *
79
     * @var int
80
     */
81
    public $last;
82
83
    /**
84
     * Constructor.
85
     *
86
     * @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...
87
     * @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...
88
     */
89
    public function __construct(Parser $parser = null, TokensList $list = null)
90 139
    {
91
        if (($parser !== null) && ($list !== null)) {
92 139
            $this->parse($parser, $list);
93 137
        }
94 137
    }
95 139
96
    /**
97
     * Builds the string representation of this statement.
98
     *
99
     * @return string
100
     */
101
    public function build()
102 7
    {
103
        /**
104
         * Query to be returned.
105
         *
106
         * @var string $query
107
         */
108
        $query = '';
109 7
110
        /**
111
         * Clauses which were built already.
112
         *
113
         * It is required to keep track of built clauses because some fields,
114
         * for example `join` is used by multiple clauses (`JOIN`, `LEFT JOIN`,
115
         * `LEFT OUTER JOIN`, etc.). The same happens for `VALUE` and `VALUES`.
116
         *
117
         * A clause is considered built just after fields' value
118
         * (`$this->field`) was used in building.
119
         *
120
         * @var array
121
         */
122
        $built = array();
123 7
124
        /**
125
         * Statement's clauses.
126
         *
127
         * @var array
128
         */
129
        $clauses = $this->getClauses();
130 7
131
        foreach ($clauses as $clause) {
132 7
            /**
133
             * The name of the clause.
134
             *
135
             * @var string $name
136
             */
137
            $name = $clause[0];
138 6
139
            /**
140
             * The type of the clause.
141
             *
142
             * @see self::$CLAUSES
143
             * @var int $type
144
             */
145
            $type = $clause[1];
146 6
147
            /**
148
             * The builder (parser) of this clause.
149
             *
150
             * @var Component $class
151
             */
152
            $class = Parser::$KEYWORD_PARSERS[$name]['class'];
153 6
154
            /**
155
             * The name of the field that is used as source for the builder.
156
             * Same field is used to store the result of parsing.
157
             *
158
             * @var string $field
159
             */
160
            $field = Parser::$KEYWORD_PARSERS[$name]['field'];
161 6
162
            // The field is empty, there is nothing to be built.
163
            if (empty($this->$field)) {
164 6
                continue;
165 6
            }
166
167
            // Checking if this field was already built.
168
            if ($type & 1) {
169 6
                if (!empty($built[$field])) {
170 6
                    continue;
171 2
                }
172
                $built[$field] = true;
173 6
            }
174 6
175
            // Checking if the name of the clause should be added.
176
            if ($type & 2) {
177 6
                $query .= $name . ' ';
178 6
            }
179 6
180
            // Checking if the result of the builder should be added.
181
            if ($type & 1) {
182 6
                $query .= $class::build($this->$field) . ' ';
183 6
            }
184 6
        }
185 7
186
        return $query;
187 7
    }
188
189
    /**
190
     * Parses the statements defined by the tokens list.
191
     *
192
     * @param Parser     $parser The instance that requests parsing.
193
     * @param TokensList $list   The list of tokens to be parsed.
194
     *
195
     * @return void
196
     */
197
    public function parse(Parser $parser, TokensList $list)
198 89
    {
199
        /**
200
         * Array containing all list of clauses parsed.
201
         * This is used to check for duplicates.
202
         *
203
         * @var array $parsedClauses
204
         */
205
        $parsedClauses = array();
206 89
207
        // This may be corrected by the parser.
208
        $this->first = $list->idx;
209 89
210
        /**
211
         * Whether options were parsed or not.
212
         * For statements that do not have any options this is set to `true` by
213
         * default.
214
         *
215
         * @var bool $parsedOptions
216
         */
217
        $parsedOptions = empty(static::$OPTIONS);
218 89
219
        for (; $list->idx < $list->count; ++$list->idx) {
220 89
            /**
221
             * Token parsed at this moment.
222
             *
223
             * @var Token $token
224
             */
225
            $token = $list->tokens[$list->idx];
226 89
227
            // End of statement.
228
            if ($token->type === Token::TYPE_DELIMITER) {
229 89
                break;
230 89
            }
231
232
            // Checking if this closing bracket is the pair for a bracket
233
            // outside the statement.
234
            if (($token->value === ')') && ($parser->brackets > 0)) {
235 89
                --$parser->brackets;
236 1
                continue;
237 1
            }
238
239
            // Only keywords are relevant here. Other parts of the query are
240
            // processed in the functions below.
241
            if ($token->type !== Token::TYPE_KEYWORD) {
242 89 View Code Duplication
                if (($token->type !== TOKEN::TYPE_COMMENT)
243 11
                    && ($token->type !== Token::TYPE_WHITESPACE)
244 11
                ) {
245 11
                    $parser->error(__('Unexpected token.'), $token);
246 3
                }
247 3
                continue;
248 11
            }
249
250
            // Unions are parsed by the parser because they represent more than
251
            // one statement.
252 View Code Duplication
            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...
253 89
                break;
254 3
            }
255
256
            $lastIdx = $list->idx;
257
258
            // ON DUPLICATE KEY UPDATE ...
259
            // has to be parsed in parent statement (INSERT or REPLACE)
260
            // so look for it and break
261
            if (get_class($this) === 'SqlParser\Statements\SelectStatement'
262 89
                && $token->value === 'ON'
263
            ) {
264
                ++$list->idx; // Skip ON
265
266
                // look for ON DUPLICATE KEY UPDATE
267
                $first = $list->getNextOfType(Token::TYPE_KEYWORD);
268
                $second = $list->getNextOfType(Token::TYPE_KEYWORD);
269 89
                $third = $list->getNextOfType(Token::TYPE_KEYWORD);
270
271
                if ($first && $second && $third
272
                    && $first->value === 'DUPLICATE'
273
                    && $second->value === 'KEY'
274
                    && $third->value === 'UPDATE'
275
                ) {
276 89
                    $list->idx = $lastIdx;
277
                    break;
278
                }
279 89
            }
280 19
            $list->idx = $lastIdx;
281 89
282 89
            /**
283 3
             * The name of the class that is used for parsing.
284 3
             *
285
             * @var Component $class
286 3
             */
287 3
            $class = null;
288
289 89
            /**
290 89
             * The name of the field where the result of the parsing is stored.
291
             *
292
             * @var string $field
293 89
             */
294 88
            $field = null;
295 88
296 88
            /**
297 62
             * Parser's options.
298 62
             *
299 88
             * @var array $options
300
             */
301
            $options = array();
302 89
303 89
            // Looking for duplicated clauses.
304 89
            if ((!empty(Parser::$KEYWORD_PARSERS[$token->value]))
305 89
                || (!empty(Parser::$STATEMENT_PARSERS[$token->value]))
306
            ) {
307
                if (!empty($parsedClauses[$token->value])) {
308
                    $parser->error(
309
                        __('This type of clause was previously parsed.'),
310
                        $token
311 1
                    );
312 1
                    break;
313
                }
314 1
                $parsedClauses[$token->value] = true;
315 1
            }
316
317 89
            // Checking if this is the beginning of a clause.
318 78
            if (!empty(Parser::$KEYWORD_PARSERS[$token->value])) {
319
                $class = Parser::$KEYWORD_PARSERS[$token->value]['class'];
320 77
                $field = Parser::$KEYWORD_PARSERS[$token->value]['field'];
321 77
                if (!empty(Parser::$KEYWORD_PARSERS[$token->value]['options'])) {
322 78
                    $options = Parser::$KEYWORD_PARSERS[$token->value]['options'];
323 78
                }
324 78
            }
325
326 78
            // Checking if this is the beginning of the statement.
327 78
            if (!empty(Parser::$STATEMENT_PARSERS[$token->value])) {
328 78
                if ((!empty(static::$CLAUSES)) // Undefined for some statements.
329 89
                    && (empty(static::$CLAUSES[$token->value]))
330
                ) {
331
                    // Some keywords (e.g. `SET`) may be the beginning of a
332 3
                    // statement and a clause.
333 3
                    // If such keyword was found and it cannot be a clause of
334
                    // this statement it means it is a new statement, but no
335
                    // delimiter was found between them.
336 89
                    $parser->error(
337
                        __('A new statement was found, but no delimiter between it and the previous one.'),
338
                        $token
339 89
                    );
340 88
                    break;
341 88
                }
342 88
                if (!$parsedOptions) {
343
                    if (empty(static::$OPTIONS[$token->value])) {
344 89
                        // Skipping keyword because if it is not a option.
345 89
                        ++$list->idx;
346
                    }
347
                    $this->options = OptionsArray::parse(
348 89
                        $parser,
349 89
                        $list,
350
                        static::$OPTIONS
351
                    );
352
                    $parsedOptions = true;
353
                }
354
            } elseif ($class === null) {
355
                // There is no parser for this keyword and isn't the beginning
356
                // of a statement (so no options) either.
357
                $parser->error(__('Unrecognized keyword.'), $token);
358
                continue;
359
            }
360 82
361
            $this->before($parser, $list, $token);
362
363 82
            // Parsing this keyword.
364
            if ($class !== null) {
365
                ++$list->idx; // Skipping keyword or last option.
366
                $this->$field = $class::parse($parser, $list, $options);
367
            }
368
369
            $this->after($parser, $list, $token);
370
        }
371
372
        // This may be corrected by the parser.
373
        $this->last = --$list->idx; // Go back to last used token.
374 85
    }
375
376
    /**
377 85
     * Function called before the token is processed.
378
     *
379
     * @param Parser     $parser The instance that requests parsing.
380
     * @param TokensList $list   The list of tokens to be parsed.
381
     * @param Token      $token  The token that is being parsed.
382
     *
383
     * @return void
384 2
     */
385
    public function before(Parser $parser, TokensList $list, Token $token)
386 2
    {
387
388
    }
389
390
    /**
391
     * Function called after the token was processed.
392
     *
393
     * @param Parser     $parser The instance that requests parsing.
394
     * @param TokensList $list   The list of tokens to be parsed.
395
     * @param Token      $token  The token that is being parsed.
396 2
     *
397
     * @return void
398 2
     */
399
    public function after(Parser $parser, TokensList $list, Token $token)
400
    {
401
402
    }
403
404
    /**
405
     * Gets the clauses of this statement.
406
     *
407
     * @return array
408
     */
409
    public function getClauses()
410
    {
411
        return static::$CLAUSES;
412
    }
413
414
    /**
415
     * Builds the string representation of this statement.
416
     *
417
     * @see static::build
418
     *
419
     * @return string
420
     */
421
    public function __toString()
422
    {
423
        return $this->build();
424
    }
425
426
    /**
427
     * Validates the order of the clauses in parsed statement
428
     * Ideally this should be called after successfully
429
     * completing the parsing of each statement
430
     *
431
     * @param Parser     $parser The instance that requests parsing.
432
     * @param TokensList $list   The list of tokens to be parsed.
433
     *
434
     * @return boolean
435
     */
436
    public function validateClauseOrder($parser, $list)
437
    {
438
        $clauses = array_flip(array_keys($this->getClauses()));
439
440
        if (empty($clauses)
441
            || count($clauses) == 0
442
        ) {
443
            return true;
444
        }
445
446
        $minIdx = -1;
447
        foreach ($clauses as $clauseType => $index) {
448
            $clauseStartIdx = Utils\Query::getClauseStartOffset(
449
                $this,
450
                $list,
451
                $clauseType
452
            );
453
454
            if ($clauseStartIdx != -1 && $clauseStartIdx < $minIdx) {
455
                $token = $list->tokens[$clauseStartIdx];
456
                $parser->error(
457
                    __('Unexpected ordering of clauses.'),
458
                    $token
459
                );
460
                return false;
461
            } elseif ($clauseStartIdx != -1) {
462
                $minIdx = $clauseStartIdx;
463
            }
464
        }
465
466
        return true;
467
    }
468
}
469