1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Doctrine\DBAL; |
4
|
|
|
|
5
|
|
|
use const PREG_OFFSET_CAPTURE; |
6
|
|
|
use function array_fill; |
7
|
|
|
use function array_key_exists; |
8
|
|
|
use function array_merge; |
9
|
|
|
use function array_slice; |
10
|
|
|
use function array_values; |
11
|
|
|
use function count; |
12
|
|
|
use function implode; |
13
|
|
|
use function is_int; |
14
|
|
|
use function key; |
15
|
|
|
use function ksort; |
16
|
|
|
use function preg_match_all; |
17
|
|
|
use function sprintf; |
18
|
|
|
use function strlen; |
19
|
|
|
use function strpos; |
20
|
|
|
use function substr; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Utility class that parses sql statements with regard to types and parameters. |
24
|
|
|
*/ |
25
|
|
|
class SQLParserUtils |
26
|
|
|
{ |
27
|
|
|
/**#@+ |
28
|
|
|
* |
29
|
|
|
* @deprecated Will be removed as internal implementation details. |
30
|
|
|
*/ |
31
|
|
|
public const POSITIONAL_TOKEN = '\?'; |
32
|
|
|
public const NAMED_TOKEN = '(?<!:):[a-zA-Z_][a-zA-Z0-9_]*'; |
33
|
|
|
/**#@-*/ |
34
|
|
|
|
35
|
|
|
// Quote characters within string literals can be preceded by a backslash. |
36
|
|
|
public const ESCAPED_SINGLE_QUOTED_TEXT = "(?:'(?:\\\\\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')"; |
37
|
|
|
public const ESCAPED_DOUBLE_QUOTED_TEXT = '(?:"(?:\\\\\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")'; |
38
|
|
|
public const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)'; |
39
|
|
|
private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]'; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Gets an array of the placeholders in an sql statements as keys and their positions in the query string. |
43
|
|
|
* |
44
|
|
|
* For a statement with positional parameters, returns a zero-indexed list of placeholder position. |
45
|
|
|
* For a statement with named parameters, returns a map of placeholder positions to their parameter names. |
46
|
|
|
* |
47
|
2484 |
|
* @deprecated Will be removed as internal implementation detail. |
48
|
|
|
* |
49
|
2484 |
|
* @param string $statement |
50
|
2484 |
|
* @param bool $isPositional |
51
|
54 |
|
* |
52
|
|
|
* @return int[]|string[] |
53
|
|
|
*/ |
54
|
2430 |
|
public static function getPlaceholderPositions($statement, $isPositional = true) |
55
|
2430 |
|
{ |
56
|
|
|
return $isPositional |
57
|
2430 |
|
? self::getPositionalPlaceholderPositions($statement) |
58
|
2430 |
|
: self::getNamedPlaceholderPositions($statement); |
59
|
2430 |
|
} |
60
|
2187 |
|
|
61
|
675 |
|
/** |
62
|
|
|
* Returns a zero-indexed list of placeholder position. |
63
|
1512 |
|
* |
64
|
1580 |
|
* @return int[] |
65
|
|
|
*/ |
66
|
|
|
private static function getPositionalPlaceholderPositions(string $statement) : array |
67
|
|
|
{ |
68
|
|
|
return self::collectPlaceholders( |
69
|
2430 |
|
$statement, |
70
|
|
|
'?', |
71
|
|
|
self::POSITIONAL_TOKEN, |
|
|
|
|
72
|
|
|
static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void { |
73
|
|
|
$carry[] = $placeholderPosition + $fragmentPosition; |
74
|
|
|
} |
75
|
|
|
); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Returns a map of placeholder positions to their parameter names. |
80
|
|
|
* |
81
|
|
|
* @return string[] |
82
|
|
|
*/ |
83
|
4387 |
|
private static function getNamedPlaceholderPositions(string $statement) : array |
84
|
|
|
{ |
85
|
4387 |
|
return self::collectPlaceholders( |
86
|
4387 |
|
$statement, |
87
|
4387 |
|
':', |
88
|
|
|
self::NAMED_TOKEN, |
89
|
4387 |
|
static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void { |
90
|
3496 |
|
$carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1); |
91
|
3496 |
|
} |
92
|
|
|
); |
93
|
|
|
} |
94
|
4387 |
|
|
95
|
1753 |
|
/** |
96
|
|
|
* @return mixed[] |
97
|
1753 |
|
*/ |
98
|
1213 |
|
private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array |
99
|
|
|
{ |
100
|
|
|
if (strpos($statement, $match) === false) { |
101
|
891 |
|
return []; |
102
|
297 |
|
} |
103
|
|
|
|
104
|
|
|
$carry = []; |
105
|
891 |
|
|
106
|
|
|
foreach (self::getUnquotedStatementFragments($statement) as $fragment) { |
107
|
|
|
preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE); |
108
|
4387 |
|
foreach ($matches[0] as $placeholder) { |
109
|
3226 |
|
$collector($placeholder[0], $placeholder[1], $fragment[1], $carry); |
110
|
|
|
} |
111
|
|
|
} |
112
|
1215 |
|
|
113
|
|
|
return $carry; |
114
|
1215 |
|
} |
115
|
297 |
|
|
116
|
297 |
|
/** |
117
|
297 |
|
* For a positional query this method can rewrite the sql statement with regard to array parameters. |
118
|
297 |
|
* |
119
|
|
|
* @param string $query The SQL query to execute. |
120
|
297 |
|
* @param mixed[] $params The parameters to bind to the query. |
121
|
297 |
|
* @param int[]|string[] $types The types the previous parameters are in. |
122
|
135 |
|
* |
123
|
|
|
* @return mixed[] |
124
|
|
|
* |
125
|
297 |
|
* @throws SQLParserUtilsException |
126
|
297 |
|
*/ |
127
|
297 |
|
public static function expandListParameters($query, $params, $types) |
128
|
|
|
{ |
129
|
297 |
|
$isPositional = is_int(key($params)); |
130
|
297 |
|
$arrayPositions = []; |
131
|
297 |
|
$bindIndex = -1; |
132
|
297 |
|
|
133
|
|
|
if ($isPositional) { |
134
|
|
|
ksort($params); |
135
|
297 |
|
ksort($types); |
136
|
297 |
|
} |
137
|
297 |
|
|
138
|
|
|
foreach ($types as $name => $type) { |
139
|
|
|
++$bindIndex; |
140
|
216 |
|
|
141
|
297 |
|
if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) { |
142
|
297 |
|
continue; |
143
|
|
|
} |
144
|
|
|
|
145
|
297 |
|
if ($isPositional) { |
146
|
297 |
|
$name = $bindIndex; |
147
|
|
|
} |
148
|
297 |
|
|
149
|
297 |
|
$arrayPositions[$name] = false; |
150
|
|
|
} |
151
|
|
|
|
152
|
297 |
|
if (( ! $arrayPositions && $isPositional)) { |
153
|
|
|
return [$query, $params, $types]; |
154
|
|
|
} |
155
|
918 |
|
|
156
|
918 |
|
if ($isPositional) { |
157
|
918 |
|
$paramOffset = 0; |
158
|
|
|
$queryOffset = 0; |
159
|
918 |
|
$params = array_values($params); |
160
|
918 |
|
$types = array_values($types); |
161
|
918 |
|
|
162
|
|
|
$paramPos = self::getPositionalPlaceholderPositions($query); |
163
|
756 |
|
|
164
|
567 |
|
foreach ($paramPos as $needle => $needlePos) { |
165
|
567 |
|
if (! isset($arrayPositions[$needle])) { |
166
|
567 |
|
continue; |
167
|
567 |
|
} |
168
|
567 |
|
|
169
|
|
|
$needle += $paramOffset; |
170
|
567 |
|
$needlePos += $queryOffset; |
171
|
|
|
$count = count($params[$needle]); |
172
|
|
|
|
173
|
486 |
|
$params = array_merge( |
174
|
486 |
|
array_slice($params, 0, $needle), |
175
|
|
|
$params[$needle], |
176
|
486 |
|
array_slice($params, $needle + 1) |
177
|
432 |
|
); |
178
|
432 |
|
|
179
|
|
|
$types = array_merge( |
180
|
|
|
array_slice($types, 0, $needle), |
181
|
486 |
|
$count ? |
182
|
486 |
|
// array needles are at {@link \Doctrine\DBAL\ParameterType} constants |
183
|
486 |
|
// + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET} |
184
|
|
|
array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) : |
185
|
|
|
[], |
186
|
756 |
|
array_slice($types, $needle + 1) |
187
|
|
|
); |
188
|
|
|
|
189
|
|
|
$expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL'; |
190
|
|
|
$query = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1); |
191
|
|
|
|
192
|
|
|
$paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle. |
193
|
|
|
$queryOffset += (strlen($expandStr) - 1); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
return [$query, $params, $types]; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
$queryOffset = 0; |
200
|
|
|
$typesOrd = []; |
201
|
2430 |
|
$paramsOrd = []; |
202
|
|
|
|
203
|
2430 |
|
$paramPos = self::getNamedPlaceholderPositions($query); |
204
|
2430 |
|
|
205
|
2430 |
|
foreach ($paramPos as $pos => $paramName) { |
206
|
2430 |
|
$paramLen = strlen($paramName) + 1; |
207
|
2430 |
|
$value = static::extractParam($paramName, $params, true); |
208
|
|
|
|
209
|
2430 |
|
if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) { |
210
|
|
|
$pos += $queryOffset; |
211
|
2430 |
|
$queryOffset -= ($paramLen - 1); |
212
|
|
|
$paramsOrd[] = $value; |
213
|
|
|
$typesOrd[] = static::extractParam($paramName, $types, false, ParameterType::STRING); |
214
|
|
|
$query = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen)); |
215
|
|
|
|
216
|
|
|
continue; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
$count = count($value); |
220
|
|
|
$expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL'; |
221
|
|
|
|
222
|
|
|
foreach ($value as $val) { |
223
|
|
|
$paramsOrd[] = $val; |
224
|
918 |
|
$typesOrd[] = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET; |
225
|
|
|
} |
226
|
918 |
|
|
227
|
729 |
|
$pos += $queryOffset; |
228
|
|
|
$queryOffset += (strlen($expandStr) - $paramLen); |
229
|
|
|
$query = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen)); |
230
|
|
|
} |
231
|
378 |
|
|
232
|
162 |
|
return [$query, $paramsOrd, $typesOrd]; |
233
|
|
|
} |
234
|
|
|
|
235
|
297 |
|
/** |
236
|
135 |
|
* Slice the SQL statement around pairs of quotes and |
237
|
|
|
* return string fragments of SQL outside of quoted literals. |
238
|
|
|
* Each fragment is captured as a 2-element array: |
239
|
162 |
|
* |
240
|
162 |
|
* 0 => matched fragment string, |
241
|
|
|
* 1 => offset of fragment in $statement |
242
|
|
|
* |
243
|
|
|
* @param string $statement |
244
|
|
|
* |
245
|
|
|
* @return mixed[][] |
246
|
|
|
*/ |
247
|
|
|
private static function getUnquotedStatementFragments($statement) |
248
|
|
|
{ |
249
|
|
|
$literal = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' . |
250
|
|
|
self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' . |
251
|
|
|
self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' . |
252
|
|
|
self::ESCAPED_BRACKET_QUOTED_TEXT; |
253
|
|
|
$expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal); |
254
|
|
|
|
255
|
|
|
preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE); |
256
|
|
|
|
257
|
|
|
return $fragments[1]; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* @param string $paramName The name of the parameter (without a colon in front) |
262
|
|
|
* @param mixed $paramsOrTypes A hash of parameters or types |
263
|
|
|
* @param bool $isParam |
264
|
|
|
* @param mixed $defaultValue An optional default value. If omitted, an exception is thrown |
265
|
|
|
* |
266
|
|
|
* @return mixed |
267
|
|
|
* |
268
|
|
|
* @throws SQLParserUtilsException |
269
|
|
|
*/ |
270
|
|
|
private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null) |
271
|
|
|
{ |
272
|
|
|
if (array_key_exists($paramName, $paramsOrTypes)) { |
273
|
|
|
return $paramsOrTypes[$paramName]; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
// Hash keys can be prefixed with a colon for compatibility |
277
|
|
|
if (array_key_exists(':' . $paramName, $paramsOrTypes)) { |
278
|
|
|
return $paramsOrTypes[':' . $paramName]; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
if ($defaultValue !== null) { |
282
|
|
|
return $defaultValue; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
if ($isParam) { |
286
|
|
|
throw SQLParserUtilsException::missingParam($paramName); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
throw SQLParserUtilsException::missingType($paramName); |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
|
This class constant has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.