1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Yiisoft\Db\Syntax; |
6
|
|
|
|
7
|
|
|
use function strlen; |
8
|
|
|
use function substr; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* SQL parser. |
12
|
|
|
* |
13
|
|
|
* This class provides methods to parse SQL statements and extract placeholders from them. |
14
|
|
|
*/ |
15
|
|
|
class SqlParser |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* @var int Length of SQL statement. |
19
|
|
|
*/ |
20
|
|
|
protected int $length; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var int Current position in SQL statement. |
24
|
|
|
*/ |
25
|
|
|
protected int $position = 0; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @param string $sql SQL statement to parse. |
29
|
|
|
*/ |
30
|
|
|
public function __construct(protected string $sql) |
31
|
|
|
{ |
32
|
|
|
$this->length = strlen($sql); |
33
|
|
|
} |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Returns the next placeholder from the current position in SQL statement. |
37
|
|
|
* |
38
|
|
|
* @param int|null $position Position of the placeholder in SQL statement. |
39
|
|
|
* |
40
|
|
|
* @return string|null The next placeholder or null if it is not found. |
41
|
|
|
*/ |
42
|
|
|
public function getNextPlaceholder(int|null &$position = null): string|null |
43
|
|
|
{ |
44
|
|
|
$result = null; |
45
|
|
|
$length = $this->length - 1; |
46
|
|
|
|
47
|
|
|
while ($this->position < $length) { |
48
|
|
|
$pos = $this->position++; |
49
|
|
|
|
50
|
|
|
match ($this->sql[$pos]) { |
51
|
|
|
':' => ($word = $this->parseWord()) === '' |
52
|
|
|
? $this->skipChars(':') |
|
|
|
|
53
|
|
|
: $result = ':' . $word, |
54
|
|
|
'"', "'" => $this->skipQuotedWithoutEscape($this->sql[$pos]), |
|
|
|
|
55
|
|
|
'-' => $this->sql[$this->position] === '-' |
56
|
|
|
? ++$this->position && $this->skipToAfterChar("\n") |
|
|
|
|
57
|
|
|
: null, |
58
|
|
|
'/' => $this->sql[$this->position] === '*' |
59
|
|
|
? ++$this->position && $this->skipToAfterString('*/') |
|
|
|
|
60
|
|
|
: null, |
61
|
|
|
default => null, |
62
|
|
|
}; |
63
|
|
|
|
64
|
|
|
if ($result !== null) { |
65
|
|
|
$position = $pos; |
66
|
|
|
|
67
|
|
|
return $result; |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
return null; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Parses and returns word symbols. Equals to `\w+` in regular expressions. |
76
|
|
|
* |
77
|
|
|
* @return string Parsed word symbols. |
78
|
|
|
*/ |
79
|
|
|
final protected function parseWord(): string |
80
|
|
|
{ |
81
|
|
|
$word = ''; |
82
|
|
|
$continue = true; |
83
|
|
|
|
84
|
|
|
while ($continue && $this->position < $this->length) { |
85
|
|
|
match ($this->sql[$this->position]) { |
86
|
|
|
'_', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', |
87
|
|
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', |
88
|
|
|
'v', 'w', 'x', 'y', 'z', |
89
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', |
90
|
|
|
'V', 'W', 'X', 'Y', 'Z' => $word .= $this->sql[$this->position++], |
91
|
|
|
default => $continue = false, |
92
|
|
|
}; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
return $word; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Parses and returns identifier. Equals to `[_a-zA-Z]\w+` in regular expressions. |
100
|
|
|
* |
101
|
|
|
* @return string Parsed identifier. |
102
|
|
|
*/ |
103
|
|
|
protected function parseIdentifier(): string |
104
|
|
|
{ |
105
|
|
|
return match ($this->sql[$this->position]) { |
106
|
|
|
'_', |
107
|
|
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', |
108
|
|
|
'v', 'w', 'x', 'y', 'z', |
109
|
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', |
110
|
|
|
'V', 'W', 'X', 'Y', 'Z' => $this->sql[$this->position++] . $this->parseWord(), |
111
|
|
|
default => '', |
112
|
|
|
}; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Skips quoted string without escape characters. |
117
|
|
|
*/ |
118
|
|
|
final protected function skipQuotedWithoutEscape(string $endChar): void |
119
|
|
|
{ |
120
|
|
|
do { |
121
|
|
|
$this->skipToAfterChar($endChar); |
122
|
|
|
} while ($this->sql[$this->position] === $endChar && ++$this->position); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Skips quoted string with escape characters. |
127
|
|
|
*/ |
128
|
|
|
final protected function skipQuotedWithEscape(string $endChar): void |
129
|
|
|
{ |
130
|
|
|
for (; $this->position < $this->length; ++$this->position) { |
131
|
|
|
if ($this->sql[$this->position] === $endChar) { |
132
|
|
|
++$this->position; |
133
|
|
|
return; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
if ($this->sql[$this->position] === '\\') { |
137
|
|
|
++$this->position; |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Skips all specified characters. |
144
|
|
|
*/ |
145
|
|
|
final protected function skipChars(string $char): void |
146
|
|
|
{ |
147
|
|
|
while ($this->position < $this->length && $this->sql[$this->position] === $char) { |
148
|
|
|
++$this->position; |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Skips to the character after the specified character. |
154
|
|
|
*/ |
155
|
|
|
final protected function skipToAfterChar(string $char): void |
156
|
|
|
{ |
157
|
|
|
for (; $this->position < $this->length; ++$this->position) { |
158
|
|
|
if ($this->sql[$this->position] === $char) { |
159
|
|
|
++$this->position; |
160
|
|
|
return; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Skips to the character after the specified string. |
167
|
|
|
*/ |
168
|
|
|
final protected function skipToAfterString(string $string): void |
169
|
|
|
{ |
170
|
|
|
$firstChar = $string[0]; |
171
|
|
|
$subString = substr($string, 1); |
172
|
|
|
$length = strlen($subString); |
173
|
|
|
|
174
|
|
|
do { |
175
|
|
|
$this->skipToAfterChar($firstChar); |
176
|
|
|
|
177
|
|
|
if (substr($this->sql, $this->position, $length) === $subString) { |
178
|
|
|
$this->position += $length; |
179
|
|
|
return; |
180
|
|
|
} |
181
|
|
|
} while ($this->position + $length < $this->length); |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.