Context   F
last analyzed

Complexity

Total Complexity 71

Size/Duplication

Total Lines 683
Duplicated Lines 0 %

Test Coverage

Coverage 98.62%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 197
dl 0
loc 683
ccs 143
cts 145
cp 0.9862
rs 2.7199
c 3
b 0
f 0
wmc 71

18 Methods

Rating   Name   Duplication   Size   Complexity  
A isBool() 0 5 2
A isKeyword() 0 12 4
A doesIdentifierRequireQuoting() 0 3 1
A escapeAll() 0 7 2
A isNumber() 0 3 1
A escape() 0 15 5
A getMode() 0 3 1
B isSeparator() 0 10 9
A setMode() 0 16 4
A isOperator() 0 3 1
A isString() 0 15 4
B loadClosest() 0 32 8
A load() 0 19 4
C isComment() 0 35 12
A isWhitespace() 0 3 4
A hasMode() 0 7 2
A isSymbol() 0 19 6
A getModeFromString() 0 34 1

How to fix   Complexity   

Complex Class

Complex classes like Context often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Context, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser;
6
7
use PhpMyAdmin\SqlParser\Contexts\ContextMySql50700;
8
9
use function class_exists;
10
use function explode;
11
use function in_array;
12
use function intval;
13
use function is_int;
14
use function is_numeric;
15
use function preg_match;
16
use function str_replace;
17
use function str_starts_with;
18
use function strlen;
19
use function strtoupper;
20
use function substr;
21
22
/**
23
 * Defines a context class that is later extended to define other contexts.
24
 *
25
 * A context is a collection of keywords, operators and functions used for parsing.
26
 *
27
 * Holds the configuration of the context that is currently used.
28
 */
29
abstract class Context
30
{
31
    /**
32
     * The maximum length of a keyword.
33
     */
34
    public const KEYWORD_MAX_LENGTH = 30;
35
36
    /**
37
     * The maximum length of a label.
38
     *
39
     * Ref: https://dev.mysql.com/doc/refman/5.7/en/statement-labels.html
40
     */
41
    public const LABEL_MAX_LENGTH = 16;
42
43
    /**
44
     * The maximum length of an operator.
45
     */
46
    public const OPERATOR_MAX_LENGTH = 4;
47
48
    /**
49
     * The name of the loaded context.
50
     */
51
    public static string $loadedContext = ContextMySql50700::class;
52
53
    /**
54
     * The prefix concatenated to the context name when an incomplete class name
55
     * is specified.
56
     */
57
    public static string $contextPrefix = 'PhpMyAdmin\\SqlParser\\Contexts\\Context';
58
59
    /**
60
     * List of keywords.
61
     *
62
     * Because, PHP's associative arrays are basically hash tables, it is more
63
     * efficient to store keywords as keys instead of values.
64
     *
65
     * The value associated to each keyword represents its flags.
66
     *
67
     * @see Token::FLAG_KEYWORD_RESERVED Token::FLAG_KEYWORD_COMPOSED
68
     *      Token::FLAG_KEYWORD_DATA_TYPE Token::FLAG_KEYWORD_KEY
69
     *      Token::FLAG_KEYWORD_FUNCTION
70
     *
71
     * Elements are sorted by flags, length and keyword.
72
     *
73
     * @var array<string,int>
74
     * @psalm-var non-empty-array<string,Token::FLAG_KEYWORD_*|int>
75
     * @phpstan-var non-empty-array<non-empty-string,Token::FLAG_KEYWORD_*|int>
76
     */
77
    public static array $keywords = [];
78
79
    /**
80
     * List of operators and their flags.
81
     *
82
     * @var array<string, int>
83
     */
84
    public static array $operators = [
85
        // Some operators (*, =) may have ambiguous flags, because they depend on
86
        // the context they are being used in.
87
        // For example: 1. SELECT * FROM table; # SQL specific (wildcard)
88
        //                 SELECT 2 * 3;        # arithmetic
89
        //              2. SELECT * FROM table WHERE foo = 'bar';
90
        //                 SET @i = 0;
91
92
        // @see Token::FLAG_OPERATOR_ARITHMETIC
93
        '%' => 1,
94
        '*' => 1,
95
        '+' => 1,
96
        '-' => 1,
97
        '/' => 1,
98
99
        // @see Token::FLAG_OPERATOR_LOGICAL
100
        '!' => 2,
101
        '!=' => 2,
102
        '&&' => 2,
103
        '<' => 2,
104
        '<=' => 2,
105
        '<=>' => 2,
106
        '<>' => 2,
107
        '=' => 2,
108
        '>' => 2,
109
        '>=' => 2,
110
        '||' => 2,
111
112
        // @see Token::FLAG_OPERATOR_BITWISE
113
        '&' => 4,
114
        '<<' => 4,
115
        '>>' => 4,
116
        '^' => 4,
117
        '|' => 4,
118
        '~' => 4,
119
120
        // @see Token::FLAG_OPERATOR_ASSIGNMENT
121
        ':=' => 8,
122
123
        // @see Token::FLAG_OPERATOR_SQL
124
        '(' => 16,
125
        ')' => 16,
126
        '.' => 16,
127
        ',' => 16,
128
        ';' => 16,
129
    ];
130
131
    /**
132
     * The mode of the MySQL server that will be used in lexing, parsing and building the statements.
133
     *
134
     * @internal use the {@see Context::getMode()} method instead.
135
     *
136
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html
137
     * @link https://mariadb.com/kb/en/sql-mode/
138
     */
139
    public static int $mode = self::SQL_MODE_NONE;
140
141
    public const SQL_MODE_NONE = 0;
142
143
    /**
144
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_allow_invalid_dates
145
     * @link https://mariadb.com/kb/en/sql-mode/#allow_invalid_dates
146
     */
147
    public const SQL_MODE_ALLOW_INVALID_DATES = 1;
148
149
    /**
150
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes
151
     * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes
152
     */
153
    public const SQL_MODE_ANSI_QUOTES = 2;
154
155
    /** Compatibility mode for Microsoft's SQL server. This is the equivalent of {@see SQL_MODE_ANSI_QUOTES}. */
156
    public const SQL_MODE_COMPAT_MYSQL = 2;
157
158
    /**
159
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_error_for_division_by_zero
160
     * @link https://mariadb.com/kb/en/sql-mode/#error_for_division_by_zero
161
     */
162
    public const SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO = 4;
163
164
    /**
165
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_high_not_precedence
166
     * @link https://mariadb.com/kb/en/sql-mode/#high_not_precedence
167
     */
168
    public const SQL_MODE_HIGH_NOT_PRECEDENCE = 8;
169
170
    /**
171
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ignore_space
172
     * @link https://mariadb.com/kb/en/sql-mode/#ignore_space
173
     */
174
    public const SQL_MODE_IGNORE_SPACE = 16;
175
176
    /**
177
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_auto_create_user
178
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_create_user
179
     */
180
    public const SQL_MODE_NO_AUTO_CREATE_USER = 32;
181
182
    /**
183
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_auto_value_on_zero
184
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_value_on_zero
185
     */
186
    public const SQL_MODE_NO_AUTO_VALUE_ON_ZERO = 64;
187
188
    /**
189
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_backslash_escapes
190
     * @link https://mariadb.com/kb/en/sql-mode/#no_backslash_escapes
191
     */
192
    public const SQL_MODE_NO_BACKSLASH_ESCAPES = 128;
193
194
    /**
195
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_dir_in_create
196
     * @link https://mariadb.com/kb/en/sql-mode/#no_dir_in_create
197
     */
198
    public const SQL_MODE_NO_DIR_IN_CREATE = 256;
199
200
    /**
201
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_engine_substitution
202
     * @link https://mariadb.com/kb/en/sql-mode/#no_engine_substitution
203
     */
204
    public const SQL_MODE_NO_ENGINE_SUBSTITUTION = 512;
205
206
    /**
207
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_field_options
208
     * @link https://mariadb.com/kb/en/sql-mode/#no_field_options
209
     */
210
    public const SQL_MODE_NO_FIELD_OPTIONS = 1024;
211
212
    /**
213
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_key_options
214
     * @link https://mariadb.com/kb/en/sql-mode/#no_key_options
215
     */
216
    public const SQL_MODE_NO_KEY_OPTIONS = 2048;
217
218
    /**
219
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_table_options
220
     * @link https://mariadb.com/kb/en/sql-mode/#no_table_options
221
     */
222
    public const SQL_MODE_NO_TABLE_OPTIONS = 4096;
223
224
    /**
225
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_unsigned_subtraction
226
     * @link https://mariadb.com/kb/en/sql-mode/#no_unsigned_subtraction
227
     */
228
    public const SQL_MODE_NO_UNSIGNED_SUBTRACTION = 8192;
229
230
    /**
231
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_date
232
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_date
233
     */
234
    public const SQL_MODE_NO_ZERO_DATE = 16384;
235
236
    /**
237
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_in_date
238
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_in_date
239
     */
240
    public const SQL_MODE_NO_ZERO_IN_DATE = 32768;
241
242
    /**
243
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_only_full_group_by
244
     * @link https://mariadb.com/kb/en/sql-mode/#only_full_group_by
245
     */
246
    public const SQL_MODE_ONLY_FULL_GROUP_BY = 65536;
247
248
    /**
249
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_pipes_as_concat
250
     * @link https://mariadb.com/kb/en/sql-mode/#pipes_as_concat
251
     */
252
    public const SQL_MODE_PIPES_AS_CONCAT = 131072;
253
254
    /**
255
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_real_as_float
256
     * @link https://mariadb.com/kb/en/sql-mode/#real_as_float
257
     */
258
    public const SQL_MODE_REAL_AS_FLOAT = 262144;
259
260
    /**
261
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_all_tables
262
     * @link https://mariadb.com/kb/en/sql-mode/#strict_all_tables
263
     */
264
    public const SQL_MODE_STRICT_ALL_TABLES = 524288;
265
266
    /**
267
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_trans_tables
268
     * @link https://mariadb.com/kb/en/sql-mode/#strict_trans_tables
269
     */
270
    public const SQL_MODE_STRICT_TRANS_TABLES = 1048576;
271
272
    /**
273
     * Custom mode.
274
     * The table and column names and any other field that must be escaped will not be.
275
     * Reserved keywords are being escaped regardless this mode is used or not.
276
     */
277
    public const SQL_MODE_NO_ENCLOSING_QUOTES = 1073741824;
278
279
    /**
280
     * Equivalent to {@see SQL_MODE_REAL_AS_FLOAT}, {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES},
281
     * {@see SQL_MODE_IGNORE_SPACE}.
282
     *
283
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi
284
     * @link https://mariadb.com/kb/en/sql-mode/#ansi
285
     */
286
    public const SQL_MODE_ANSI = 393234;
287
288
    /**
289
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
290
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
291
     *
292
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_db2
293
     * @link https://mariadb.com/kb/en/sql-mode/#db2
294
     */
295
    public const SQL_MODE_DB2 = 138258;
296
297
    /**
298
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
299
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
300
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
301
     *
302
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_maxdb
303
     * @link https://mariadb.com/kb/en/sql-mode/#maxdb
304
     */
305
    public const SQL_MODE_MAXDB = 138290;
306
307
    /**
308
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
309
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
310
     *
311
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_mssql
312
     * @link https://mariadb.com/kb/en/sql-mode/#mssql
313
     */
314
    public const SQL_MODE_MSSQL = 138258;
315
316
    /**
317
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
318
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
319
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
320
     *
321
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_oracle
322
     * @link https://mariadb.com/kb/en/sql-mode/#oracle
323
     */
324
    public const SQL_MODE_ORACLE = 138290;
325
326
    /**
327
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
328
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
329
     *
330
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_postgresql
331
     * @link https://mariadb.com/kb/en/sql-mode/#postgresql
332
     */
333
    public const SQL_MODE_POSTGRESQL = 138258;
334
335
    /**
336
     * Equivalent to {@see SQL_MODE_STRICT_TRANS_TABLES}, {@see SQL_MODE_STRICT_ALL_TABLES},
337
     * {@see SQL_MODE_NO_ZERO_IN_DATE}, {@see SQL_MODE_NO_ZERO_DATE}, {@see SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO},
338
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
339
     *
340
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_traditional
341
     * @link https://mariadb.com/kb/en/sql-mode/#traditional
342
     */
343
    public const SQL_MODE_TRADITIONAL = 1622052;
344
345
    /**
346
     * Checks if the given string is a keyword.
347
     *
348
     * @param bool $isReserved checks if the keyword is reserved
349
     */
350 1424
    public static function isKeyword(string $string, bool $isReserved = false): int|null
351
    {
352 1424
        $upperString = strtoupper($string);
353
354
        if (
355 1424
            ! isset(static::$keywords[$upperString])
356 1424
            || ($isReserved && ! (static::$keywords[$upperString] & Token::FLAG_KEYWORD_RESERVED))
357
        ) {
358 1424
            return null;
359
        }
360
361 1394
        return static::$keywords[$upperString];
362
    }
363
364
    /**
365
     * Checks if the given string is an operator and returns the appropriate flag for the operator.
366
     */
367 1440
    public static function isOperator(string $string): int|null
368
    {
369 1440
        return static::$operators[$string] ?? null;
370
    }
371
372
    /**
373
     * Checks if the given character is a whitespace.
374
     */
375 1448
    public static function isWhitespace(string $string): bool
376
    {
377 1448
        return $string === ' ' || $string === "\r" || $string === "\n" || $string === "\t";
378
    }
379
380
    /**
381
     * Checks if the given string is the beginning of a whitespace.
382
     *
383
     * @return int|null the appropriate flag for the comment type
384
     */
385 1440
    public static function isComment(string $string, bool $end = false): int|null
386
    {
387 1440
        if ($string === '') {
388 2
            return null;
389
        }
390
391
        // If comment is Bash style (#):
392 1440
        if (str_starts_with($string, '#')) {
393 8
            return Token::FLAG_COMMENT_BASH;
394
        }
395
396
        // If comment is a MySQL command
397 1440
        if (str_starts_with($string, '/*!')) {
398
            return Token::FLAG_COMMENT_MYSQL_CMD;
399
        }
400
401
        // If comment is opening C style (/*) or is closing C style (*/), warning, it could conflict
402
        // with wildcard and a real opening C style.
403
        // It would look like the following valid SQL statement: "SELECT */* comment */ FROM...".
404 1440
        if (str_starts_with($string, '/*') || str_starts_with($string, '*/')) {
405 102
            return Token::FLAG_COMMENT_C;
406
        }
407
408
        // If comment is SQL style (--\s?):
409
        if (
410 1440
            str_starts_with($string, '-- ')
411 1440
            || str_starts_with($string, "--\r")
412 1440
            || str_starts_with($string, "--\n")
413 1440
            || str_starts_with($string, "--\t")
414 1440
            || ($string === '--' && $end)
415
        ) {
416 72
            return Token::FLAG_COMMENT_SQL;
417
        }
418
419 1440
        return null;
420
    }
421
422
    /**
423
     * Checks if the given string is a boolean value.
424
     * This actually check only for `TRUE` and `FALSE` because `1` or `0` are
425
     * actually numbers and are parsed by specific methods.
426
     */
427 1424
    public static function isBool(string $string): bool
428
    {
429 1424
        $upperString = strtoupper($string);
430
431 1424
        return $upperString === 'TRUE' || $upperString === 'FALSE';
432
    }
433
434
    /**
435
     * Checks if the given character can be a part of a number.
436
     */
437 2
    public static function isNumber(string $string): bool
438
    {
439 2
        return in_array($string, ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', '+', 'e', 'E'], true);
440
    }
441
442
    /**
443
     * Checks if the given character is the beginning of a symbol. A symbol
444
     * can be either a variable or a field name.
445
     *
446
     * @return int|null the appropriate flag for the symbol type
447
     */
448 1424
    public static function isSymbol(string $string): int|null
449
    {
450 1424
        if ($string === '') {
451 2
            return null;
452
        }
453
454 1424
        if (str_starts_with($string, '@')) {
455 124
            return Token::FLAG_SYMBOL_VARIABLE;
456
        }
457
458 1422
        if (str_starts_with($string, '`')) {
459 370
            return Token::FLAG_SYMBOL_BACKTICK;
460
        }
461
462 1422
        if (str_starts_with($string, ':') || str_starts_with($string, '?')) {
463 18
            return Token::FLAG_SYMBOL_PARAMETER;
464
        }
465
466 1422
        return null;
467
    }
468
469
    /**
470
     * Checks if the given character is the beginning of a string.
471
     *
472
     * @param string $string string to be checked
473
     *
474
     * @return int|null the appropriate flag for the string type
475
     */
476 1424
    public static function isString(string $string): int|null
477
    {
478 1424
        if ($string === '') {
479 2
            return null;
480
        }
481
482 1424
        if (str_starts_with($string, '\'')) {
483 334
            return Token::FLAG_STRING_SINGLE_QUOTES;
484
        }
485
486 1424
        if (str_starts_with($string, '"')) {
487 160
            return Token::FLAG_STRING_DOUBLE_QUOTES;
488
        }
489
490 1424
        return null;
491
    }
492
493
    /**
494
     * Checks if the given character can be a separator for two lexeme.
495
     *
496
     * @param string $string string to be checked
497
     */
498 1424
    public static function isSeparator(string $string): bool
499
    {
500
        // NOTES:   Only non-alphanumeric ASCII characters may be separators.
501
        //          `~` is the last printable ASCII character.
502 1424
        return $string <= '~'
503 1424
            && $string !== '_'
504 1424
            && $string !== '$'
505 1424
            && ($string < '0' || $string > '9')
506 1424
            && ($string < 'a' || $string > 'z')
507 1424
            && ($string < 'A' || $string > 'Z');
508
    }
509
510
    /**
511
     * Loads the specified context.
512
     *
513
     * Contexts may be used by accessing the context directly.
514
     *
515
     * @param string $context name of the context or full class name that defines the context
516
     *
517
     * @return bool true if the context was loaded, false otherwise
518
     */
519 1768
    public static function load(string $context = ''): bool
520
    {
521 1768
        if ($context === '') {
522 1768
            $context = ContextMySql50700::class;
523
        }
524
525 1768
        if (! class_exists($context)) {
526 46
            if (! class_exists(self::$contextPrefix . $context)) {
527 12
                return false;
528
            }
529
530
            // Short context name (must be formatted into class name).
531 42
            $context = self::$contextPrefix . $context;
532
        }
533
534 1768
        self::$loadedContext = $context;
535 1768
        self::$keywords = $context::$keywords;
0 ignored issues
show
Bug introduced by
The property keywords does not exist on string.
Loading history...
536
537 1768
        return true;
538
    }
539
540
    /**
541
     * Loads the context with the closest version to the one specified.
542
     *
543
     * The closest context is found by replacing last digits with zero until one
544
     * is loaded successfully.
545
     *
546
     * @see Context::load()
547
     *
548
     * @param string $context name of the context or full class name that defines the context
549
     *
550
     * @return string|null The loaded context. `null` if no context was loaded.
551
     */
552 14
    public static function loadClosest(string $context = ''): string|null
553
    {
554 14
        $length = strlen($context);
555 14
        for ($i = $length; $i > 0;) {
556
            /* Trying to load the new context */
557 14
            if (static::load($context)) {
558 12
                return $context;
559
            }
560
561
            /* Replace last two non zero digits by zeroes */
562
            do {
563 10
                $i -= 2;
564 10
                $part = substr($context, $i, 2);
565
                /* No more numeric parts to strip */
566 10
                if (! is_numeric($part)) {
567 6
                    break 2;
568
                }
569 8
            } while (intval($part) === 0 && $i > 0);
570
571 8
            $context = substr($context, 0, $i) . '00' . substr($context, $i + 2);
572
        }
573
574
        /* Fallback to loading at least matching engine */
575 6
        if (str_starts_with($context, 'MariaDb')) {
576 2
            return static::loadClosest('MariaDb100300');
577
        }
578
579 4
        if (str_starts_with($context, 'MySql')) {
580 2
            return static::loadClosest('MySql50700');
581
        }
582
583 2
        return null;
584
    }
585
586
    /**
587
     * Gets the SQL mode.
588
     */
589 72
    public static function getMode(): int
590
    {
591 72
        return static::$mode;
592
    }
593
594
    /**
595
     * Sets the SQL mode.
596
     */
597 944
    public static function setMode(int|string $mode = self::SQL_MODE_NONE): void
598
    {
599 944
        if (is_int($mode)) {
600 878
            static::$mode = $mode;
601
602 878
            return;
603
        }
604
605 66
        static::$mode = self::SQL_MODE_NONE;
606 66
        if ($mode === '') {
607 4
            return;
608
        }
609
610 64
        $modes = explode(',', $mode);
611 64
        foreach ($modes as $sqlMode) {
612 64
            static::$mode |= self::getModeFromString($sqlMode);
613
        }
614
    }
615
616
    /** @psalm-suppress MixedReturnStatement, MixedInferredReturnType Is caused by the LSB of the constants */
617 64
    private static function getModeFromString(string $mode): int
618
    {
619 64
        return match ($mode) {
620 64
            'ALLOW_INVALID_DATES' => self::SQL_MODE_ALLOW_INVALID_DATES,
621 64
            'ANSI_QUOTES' => self::SQL_MODE_ANSI_QUOTES,
622 64
            'COMPAT_MYSQL' => self::SQL_MODE_COMPAT_MYSQL,
623 64
            'ERROR_FOR_DIVISION_BY_ZERO' => self::SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO,
624 64
            'HIGH_NOT_PRECEDENCE' => self::SQL_MODE_HIGH_NOT_PRECEDENCE,
625 64
            'IGNORE_SPACE' => self::SQL_MODE_IGNORE_SPACE,
626 64
            'NO_AUTO_CREATE_USER' => self::SQL_MODE_NO_AUTO_CREATE_USER,
627 64
            'NO_AUTO_VALUE_ON_ZERO' => self::SQL_MODE_NO_AUTO_VALUE_ON_ZERO,
628 64
            'NO_BACKSLASH_ESCAPES' => self::SQL_MODE_NO_BACKSLASH_ESCAPES,
629 64
            'NO_DIR_IN_CREATE' => self::SQL_MODE_NO_DIR_IN_CREATE,
630 64
            'NO_ENGINE_SUBSTITUTION' => self::SQL_MODE_NO_ENGINE_SUBSTITUTION,
631 64
            'NO_FIELD_OPTIONS' => self::SQL_MODE_NO_FIELD_OPTIONS,
632 64
            'NO_KEY_OPTIONS' => self::SQL_MODE_NO_KEY_OPTIONS,
633 64
            'NO_TABLE_OPTIONS' => self::SQL_MODE_NO_TABLE_OPTIONS,
634 64
            'NO_UNSIGNED_SUBTRACTION' => self::SQL_MODE_NO_UNSIGNED_SUBTRACTION,
635 64
            'NO_ZERO_DATE' => self::SQL_MODE_NO_ZERO_DATE,
636 64
            'NO_ZERO_IN_DATE' => self::SQL_MODE_NO_ZERO_IN_DATE,
637 64
            'ONLY_FULL_GROUP_BY' => self::SQL_MODE_ONLY_FULL_GROUP_BY,
638 64
            'PIPES_AS_CONCAT' => self::SQL_MODE_PIPES_AS_CONCAT,
639 64
            'REAL_AS_FLOAT' => self::SQL_MODE_REAL_AS_FLOAT,
640 64
            'STRICT_ALL_TABLES' => self::SQL_MODE_STRICT_ALL_TABLES,
641 64
            'STRICT_TRANS_TABLES' => self::SQL_MODE_STRICT_TRANS_TABLES,
642 64
            'NO_ENCLOSING_QUOTES' => self::SQL_MODE_NO_ENCLOSING_QUOTES,
643 64
            'ANSI' => self::SQL_MODE_ANSI,
644 64
            'DB2' => self::SQL_MODE_DB2,
645 64
            'MAXDB' => self::SQL_MODE_MAXDB,
646 64
            'MSSQL' => self::SQL_MODE_MSSQL,
647 64
            'ORACLE' => self::SQL_MODE_ORACLE,
648 64
            'POSTGRESQL' => self::SQL_MODE_POSTGRESQL,
649 64
            'TRADITIONAL' => self::SQL_MODE_TRADITIONAL,
650 64
            default => self::SQL_MODE_NONE,
651 64
        };
652
    }
653
654
    /**
655
     * Escapes the symbol by adding surrounding backticks.
656
     *
657
     * @param string $str   the string to be escaped
658
     * @param string $quote quote to be used when escaping
659
     */
660 142
    public static function escape(string $str, string $quote = '`'): string
661
    {
662
        if (
663 142
            (static::$mode & self::SQL_MODE_NO_ENCLOSING_QUOTES) && ! (
664 142
                static::isKeyword($str, true) || self::doesIdentifierRequireQuoting($str)
0 ignored issues
show
Bug Best Practice introduced by
The expression static::isKeyword($str, true) of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
665
            )
666
        ) {
667 2
            return $str;
668
        }
669
670 142
        if (static::$mode & self::SQL_MODE_ANSI_QUOTES) {
671 2
            $quote = '"';
672
        }
673
674 142
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
675
    }
676
677
    /**
678
     * Escapes the symbol by adding surrounding backticks.
679
     *
680
     * @param string[] $strings the string to be escaped
681
     *
682
     * @return string[]
683
     */
684 32
    public static function escapeAll(array $strings): array
685
    {
686 32
        foreach ($strings as $key => $value) {
687 32
            $strings[$key] = static::escape($value);
688
        }
689
690 32
        return $strings;
691
    }
692
693
    /**
694
     * Function verifies that given SQL Mode constant is currently set
695
     *
696
     * @param int $flag for example {@see Context::SQL_MODE_ANSI_QUOTES}
697
     *
698
     * @return bool false on empty param, true/false on given constant/int value
699
     */
700 4
    public static function hasMode(int|null $flag = null): bool
701
    {
702 4
        if (empty($flag)) {
703
            return false;
704
        }
705
706 4
        return (self::$mode & $flag) === $flag;
707
    }
708
709 2
    private static function doesIdentifierRequireQuoting(string $identifier): bool
710
    {
711 2
        return preg_match('/^[$]|^\d+$|[^0-9a-zA-Z$_\x80-\xffff]/', $identifier) === 1;
712
    }
713
}
714