Completed
Push — master ( 38b10f...de8b3e )
by Dan
02:45
created

Context::isSeparator()   B

Complexity

Conditions 8
Paths 23

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 9
rs 7.7777
cc 8
eloc 5
nc 23
nop 1
1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 21 and the first side effect is on line 545.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * Defines a context class that is later extended to define other contexts.
5
 *
6
 * A context is a collection of keywords, operators and functions used for
7
 * parsing.
8
 *
9
 * @package SqlParser
10
 */
11
namespace SqlParser;
12
13
/**
14
 * Holds the configuration of the context that is currently used.
15
 *
16
 * @category Contexts
17
 * @package  SqlParser
18
 * @author   Dan Ungureanu <[email protected]>
19
 * @license  http://opensource.org/licenses/GPL-2.0 GNU Public License
20
 */
21
abstract class Context
22
{
23
24
    /**
25
     * The maximum length of a keyword.
26
     *
27
     * @see static::$TOKEN_KEYWORD
28
     *
29
     * @var int
30
     */
31
    const KEYWORD_MAX_LENGTH = 30;
32
33
    /**
34
     * The maximum length of an operator.
35
     *
36
     * @see static::$TOKEN_OPERATOR
37
     *
38
     * @var int
39
     */
40
    const OPERATOR_MAX_LENGTH = 4;
41
42
    /**
43
     * The name of the default content.
44
     *
45
     * @var string
46
     */
47
    public static $defaultContext = '\\SqlParser\\Contexts\\ContextMySql50700';
48
49
    /**
50
     * The name of the loaded context.
51
     *
52
     * @var string
53
     */
54
    public static $loadedContext = '\\SqlParser\\Contexts\\ContextMySql50700';
55
56
    /**
57
     * The prefix concatenated to the context name when an incomplete class name
58
     * is specified.
59
     *
60
     * @var string
61
     */
62
    public static $contextPrefix = '\\SqlParser\\Contexts\\Context';
63
64
    /**
65
     * List of keywords.
66
     *
67
     * Because, PHP's associative arrays are basically hash tables, it is more
68
     * efficient to store keywords as keys instead of values.
69
     *
70
     * The value associated to each keyword represents its flags.
71
     *
72
     * @see Token::FLAG_KEYWORD_*
73
     *
74
     * Elements are sorted by flags, length and keyword.
75
     *
76
     * @var array
77
     */
78
    public static $KEYWORDS = array();
79
80
    /**
81
     * List of operators and their flags.
82
     *
83
     * @var array
84
     */
85
    public static $OPERATORS = array(
86
87
        // Some operators (*, =) may have ambiguous flags, because they depend on
88
        // the context they are being used in.
89
        // For example: 1. SELECT * FROM table; # SQL specific (wildcard)
90
        //                 SELECT 2 * 3;        # arithmetic
1 ignored issue
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
91
        //              2. SELECT * FROM table WHERE foo = 'bar';
92
        //                 SET @i = 0;
93
94
        // @see Token::FLAG_OPERATOR_ARITHMETIC
95
        '%'   =>  1, '*'   =>  1, '+'    =>  1, '-'   =>  1, '/'   =>  1,
96
97
        // @see Token::FLAG_OPERATOR_LOGICAL
98
        '!'   =>  2, '!='   =>  2, '&&'  =>  2, '<'   =>  2, '<='  =>  2,
99
        '<=>' =>  2, '<>'   =>  2, '='   =>  2, '>'   =>  2, '>='  =>  2,
100
        '||'  =>  2,
101
102
        // @see Token::FLAG_OPERATOR_BITWISE
103
        '&'   =>  4, '<<'   =>  4, '>>'  =>  4, '^'   =>  4, '|'   =>  4,
104
        '~'   =>  4,
105
106
        // @see Token::FLAG_OPERATOR_ASSIGNMENT
107
        ':='  =>  8,
108
109
        // @see Token::FLAG_OPERATOR_SQL
110
        '('   => 16, ')'    => 16, '.'   => 16,  ','  => 16, ';' => 16,
111
    );
112
113
    /**
114
     * The mode of the MySQL server that will be used in lexing, parsing and
115
     * building the statements.
116
     *
117
     * @var int
118
     */
119
    public static $MODE = 0;
120
121
    /*
122
     * Server SQL Modes
123
     * https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html
124
     */
125
126
    // Compatibility mode for Microsoft's SQL server.
127
    // This is the equivalent of ANSI_QUOTES.
128
    const COMPAT_MYSQL                  =       2;
129
130
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_allow_invalid_dates
131
    const ALLOW_INVALID_DATES           =       1;
132
133
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ansi_quotes
134
    const ANSI_QUOTES                   =       2;
135
136
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_error_for_division_by_zero
137
    const ERROR_FOR_DIVISION_BY_ZERO    =       4;
138
139
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_high_not_precedence
140
    const HIGH_NOT_PRECEDENCE           =       8;
141
142
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ignore_space
143
    const IGNORE_SPACE                  =      16;
144
145
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_auto_create_user
146
    const NO_AUTO_CREATE_USER           =      32;
147
148
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_auto_value_on_zero
149
    const NO_AUTO_VALUE_ON_ZERO         =      64;
150
151
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_backslash_escapes
152
    const NO_BACKSLASH_ESCAPES          =     128;
153
154
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_dir_in_create
155
    const NO_DIR_IN_CREATE              =     256;
156
157
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_dir_in_create
158
    const NO_ENGINE_SUBSTITUTION        =     512;
159
160
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_field_options
161
    const NO_FIELD_OPTIONS              =    1024;
162
163
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_key_options
164
    const NO_KEY_OPTIONS                =    2048;
165
166
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_table_options
167
    const NO_TABLE_OPTIONS              =    4096;
168
169
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_unsigned_subtraction
170
    const NO_UNSIGNED_SUBTRACTION       =    8192;
171
172
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_zero_date
173
    const NO_ZERO_DATE                  =   16384;
174
175
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_zero_in_date
176
    const NO_ZERO_IN_DATE               =   32768;
177
178
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_only_full_group_by
179
    const ONLY_FULL_GROUP_BY            =   65536;
180
181
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_pipes_as_concat
182
    const PIPES_AS_CONCAT               =  131072;
183
184
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_real_as_float
185
    const REAL_AS_FLOAT                 =  262144;
186
187
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_strict_all_tables
188
    const STRICT_ALL_TABLES             =  524288;
189
190
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_strict_trans_tables
191
    const STRICT_TRANS_TABLES           = 1048576;
192
193
    // Custom modes.
194
195
    // The table and column names and any other field that must be escaped will
196
    // not be.
197
    // Reserved keywords are being escaped regardless this mode is used or not.
198
    const NO_ENCLOSING_QUOTES           = 1073741824;
199
200
    /*
201
     * Combination SQL Modes
202
     * https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sql-mode-combo
203
     */
204
205
    // REAL_AS_FLOAT, PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE
206
    const SQL_MODE_ANSI                 = 393234;
207
208
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
209
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS,
210
    const SQL_MODE_DB2                  = 138258;
211
212
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
213
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER
214
    const SQL_MODE_MAXDB                = 138290;
215
216
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
217
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS
218
    const SQL_MODE_MSSQL                = 138258;
219
220
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
221
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER
222
    const SQL_MODE_ORACLE               = 138290;
223
224
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
225
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS
226
    const SQL_MODE_POSTGRESQL           = 138258;
227
228
    // STRICT_TRANS_TABLES, STRICT_ALL_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE,
229
    // ERROR_FOR_DIVISION_BY_ZERO, NO_AUTO_CREATE_USER
230
    const SQL_MODE_TRADITIONAL          = 1622052;
231
232
    // -------------------------------------------------------------------------
233
    // Keyword.
234
235
    /**
236
     * Checks if the given string is a keyword.
237
     *
238
     * @param string $str        String to be checked.
239
     * @param bool   $isReserved Checks if the keyword is reserved.
240
     *
241
     * @return int
242
     */
243
    public static function isKeyword($str, $isReserved = false)
244
    {
245
        $str = strtoupper($str);
246
247
        if (isset(static::$KEYWORDS[$str])) {
248
            if ($isReserved) {
249
                if (!(static::$KEYWORDS[$str] & Token::FLAG_KEYWORD_RESERVED)) {
250
                    return null;
251
                }
252
            }
253
            return static::$KEYWORDS[$str];
254
        }
255
256
        return null;
257
    }
258
259
    // -------------------------------------------------------------------------
260
    // Operator.
261
262
    /**
263
     * Checks if the given string is an operator.
264
     *
265
     * @param string $str String to be checked.
266
     *
267
     * @return int The appropriate flag for the operator.
268
     */
269
    public static function isOperator($str)
270
    {
271
        if (!isset(static::$OPERATORS[$str])) {
272
            return null;
273
        }
274
        return static::$OPERATORS[$str];
275
    }
276
277
    // -------------------------------------------------------------------------
278
    // Whitespace.
279
280
    /**
281
     * Checks if the given character is a whitespace.
282
     *
283
     * @param string $str String to be checked.
284
     *
285
     * @return bool
286
     */
287
    public static function isWhitespace($str)
288
    {
289
        return ($str === ' ') || ($str === "\r") || ($str === "\n") || ($str === "\t");
290
    }
291
292
    // -------------------------------------------------------------------------
293
    // Comment.
294
295
    /**
296
     * Checks if the given string is the beginning of a whitespace.
297
     *
298
     * @param string $str String to be checked.
299
     *
300
     * @return int The appropriate flag for the comment type.
301
     */
302
    public static function isComment($str)
303
    {
304
        $len = strlen($str);
305
        if ($str[0] === '#') {
306
            return Token::FLAG_COMMENT_BASH;
307
        } elseif (($len > 1) && ($str[0] === '/') && ($str[1] === '*')) {
308
            return (($len > 2) && ($str[2] == '!')) ?
309
                Token::FLAG_COMMENT_MYSQL_CMD : Token::FLAG_COMMENT_C;
310
        } elseif (($len > 1) && ($str[0] === '*') && ($str[1] === '/')) {
311
            return Token::FLAG_COMMENT_C;
312
        } elseif (($len > 2) && ($str[0] === '-')
313
            && ($str[1] === '-') && (static::isWhitespace($str[2]))
314
        ) {
315
            return Token::FLAG_COMMENT_SQL;
316
        }
317
        return null;
318
    }
319
320
    // -------------------------------------------------------------------------
321
    // Bool.
322
323
    /**
324
     * Checks if the given string is a boolean value.
325
     * This actually check only for `TRUE` and `FALSE` because `1` or `0` are
326
     * actually numbers and are parsed by specific methods.
327
     *
328
     * @param string $str String to be checked.
329
     *
330
     * @return bool
331
     */
332
    public static function isBool($str)
333
    {
334
        $str = strtoupper($str);
335
        return ($str === 'TRUE') || ($str === 'FALSE');
336
    }
337
338
    // -------------------------------------------------------------------------
339
    // Number.
340
341
    /**
342
     * Checks if the given character can be a part of a number.
343
     *
344
     * @param string $str String to be checked.
345
     *
346
     * @return bool
347
     */
348
    public static function isNumber($str)
349
    {
350
        return (($str >= '0') && ($str <= '9')) || ($str === '.')
351
            || ($str === '-') || ($str === '+') || ($str === 'e') || ($str === 'E');
352
    }
353
354
    // -------------------------------------------------------------------------
355
    // Symbol.
356
357
    /**
358
     * Checks if the given character is the beginning of a symbol. A symbol
359
     * can be either a variable or a field name.
360
     *
361
     * @param string $str String to be checked.
362
     *
363
     * @return int The appropriate flag for the symbol type.
364
     */
365 View Code Duplication
    public static function isSymbol($str)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
366
    {
367
        if ($str[0] === '@') {
368
            return Token::FLAG_SYMBOL_VARIABLE;
369
        } elseif ($str[0] === '`') {
370
            return Token::FLAG_SYMBOL_BACKTICK;
371
        }
372
        return null;
373
    }
374
375
    // -------------------------------------------------------------------------
376
    // String.
377
378
    /**
379
     * Checks if the given character is the beginning of a string.
380
     *
381
     * @param string $str String to be checked.
382
     *
383
     * @return int The appropriate flag for the string type.
384
     */
385 View Code Duplication
    public static function isString($str)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
386
    {
387
        if ($str[0] === '\'') {
388
            return Token::FLAG_STRING_SINGLE_QUOTES;
389
        } elseif ($str[0] === '"') {
390
            return Token::FLAG_STRING_DOUBLE_QUOTES;
391
        }
392
        return null;
393
    }
394
395
    // -------------------------------------------------------------------------
396
    // Delimiter.
397
398
    /**
399
     * Checks if the given character can be a separator for two lexeme.
400
     *
401
     * @param string $str String to be checked.
402
     *
403
     * @return bool
404
     */
405
    public static function isSeparator($str)
406
    {
407
        // NOTES:   Only non alphanumeric ASCII characters may be separators.
408
        //          `~` is the last printable ASCII character.
409
        return ($str <= '~') && ($str !== '_')
410
            && (($str < '0') || ($str > '9'))
411
            && (($str < 'a') || ($str > 'z'))
412
            && (($str < 'A') || ($str > 'Z'));
413
    }
414
415
    /**
416
     * Loads the specified context.
417
     *
418
     * Contexts may be used by accessing the context directly.
419
     *
420
     * @param string $context Name of the context or full class name that
421
     *                        defines the context.
422
     *
423
     * @throws \Exception If the specified context doesn't exist.
424
     *
425
     * @return void
426
     */
427
    public static function load($context = '')
428
    {
429
        if (empty($context)) {
430
            $context = self::$defaultContext;
431
        }
432
        if ($context[0] !== '\\') {
433
            // Short context name (must be formatted into class name).
434
            $context = self::$contextPrefix . $context;
435
        }
436
        if (!class_exists($context)) {
437
            throw new \Exception(
438
                'Specified context ("' . $context . '") does not exist.'
439
            );
440
        }
441
        self::$loadedContext = $context;
442
        self::$KEYWORDS = $context::$KEYWORDS;
443
    }
444
445
    /**
446
     * Loads the context with the closest version to the one specified.
447
     *
448
     * The closest context is found by replacing last digits with zero until one
449
     * is loaded successfully.
450
     *
451
     * @see Context::load()
452
     *
453
     * @param string $context Name of the context or full class name that
454
     *                        defines the context.
455
     *
456
     * @return string The loaded context. `null` if no context was loaded.
457
     */
458
    public static function loadClosest($context = '')
459
    {
460
        /**
461
         * The number of replaces done by `preg_replace`.
462
         * This actually represents whether a new context was generated or not.
463
         *
464
         * @var int $count
465
         */
466
        $count = 0;
467
468
        // As long as a new context can be generated, we try to load it.
469
        do {
470
            try {
471
                // Trying to load the new context.
472
                static::load($context);
473
            } catch (\Exception $e) {
474
                // If it didn't work, we are looking for a new one and skipping
475
                // over to the next generation that will try the new context.
476
                $context = preg_replace(
477
                    '/[1-9](0*)$/',
478
                    '0$1',
479
                    $context,
480
                    -1,
481
                    $count
482
                );
483
                continue;
484
            }
485
486
            // Last generated context was valid (did not throw any exceptions).
487
            // So we return it, to let the user know what context was loaded.
488
            return $context;
489
        } while ($count !== 0);
490
491
        return null;
492
    }
493
494
    /**
495
     * Sets the SQL mode.
496
     *
497
     * @param string $mode The list of modes. If empty, the mode is reset.
498
     *
499
     * @return void
500
     */
501
    public static function setMode($mode = '')
502
    {
503
        static::$MODE = 0;
504
        if (empty($mode)) {
505
            return;
506
        }
507
        $mode = explode(',', $mode);
508
        foreach ($mode as $m) {
509
            static::$MODE |= constant('static::' . $m);
510
        }
511
    }
512
513
    /**
514
     * Escapes the symbol by adding surrounding backticks.
515
     *
516
     * @param array|string $str   The string to be escaped.
517
     * @param string       $quote Quote to be used when escaping.
518
     *
519
     * @return string
520
     */
521
    public static function escape($str, $quote = '`')
522
    {
523
        if (is_array($str)) {
524
            foreach ($str as $key => $value) {
525
                $str[$key] = static::escape($value);
526
            }
527
            return $str;
528
        }
529
530
        if ((static::$MODE & Context::NO_ENCLOSING_QUOTES)
531
            && (!static::isKeyword($str, true))
532
        ) {
533
            return $str;
534
        }
535
536
        if (static::$MODE & Context::ANSI_QUOTES) {
537
            $quote = '"';
538
        }
539
540
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
541
    }
542
}
543
544
// Initializing the default context.
545
Context::load();
546