1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Doctrine\DBAL; |
6
|
|
|
|
7
|
|
|
use Doctrine\DBAL\Exception\MissingArrayParameter; |
8
|
|
|
use Doctrine\DBAL\Exception\MissingArrayParameterType; |
9
|
|
|
use const PREG_OFFSET_CAPTURE; |
10
|
|
|
use function array_fill; |
11
|
|
|
use function array_key_exists; |
12
|
|
|
use function array_merge; |
13
|
|
|
use function array_slice; |
14
|
|
|
use function array_values; |
15
|
|
|
use function count; |
16
|
|
|
use function implode; |
17
|
|
|
use function is_int; |
18
|
|
|
use function key; |
19
|
|
|
use function ksort; |
20
|
|
|
use function preg_match_all; |
21
|
|
|
use function sprintf; |
22
|
|
|
use function strlen; |
23
|
|
|
use function strpos; |
24
|
|
|
use function substr; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Utility class that parses sql statements with regard to types and parameters. |
28
|
|
|
*/ |
29
|
|
|
class SQLParserUtils |
30
|
|
|
{ |
31
|
|
|
private const POSITIONAL_TOKEN = '\?'; |
32
|
|
|
private 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
|
|
|
* |
37
|
|
|
* @deprecated Will be removed as internal implementation details. |
38
|
|
|
*/ |
39
|
|
|
public const ESCAPED_SINGLE_QUOTED_TEXT = "(?:'(?:\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')"; |
40
|
|
|
public const ESCAPED_DOUBLE_QUOTED_TEXT = '(?:"(?:\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")'; |
41
|
|
|
public const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)'; |
42
|
|
|
/**#@-*/ |
43
|
|
|
|
44
|
|
|
private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]'; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Returns a zero-indexed list of placeholder position. |
48
|
|
|
* |
49
|
|
|
* @return int[] |
50
|
|
|
*/ |
51
|
|
|
private static function getPositionalPlaceholderPositions(string $statement) : array |
52
|
|
|
{ |
53
|
|
|
return self::collectPlaceholders( |
54
|
1444 |
|
$statement, |
55
|
|
|
'?', |
56
|
1444 |
|
self::POSITIONAL_TOKEN, |
57
|
1396 |
|
static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void { |
58
|
1444 |
|
$carry[] = $placeholderPosition + $fragmentPosition; |
59
|
|
|
} |
60
|
|
|
); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Returns a map of placeholder positions to their parameter names. |
65
|
|
|
* |
66
|
3884 |
|
* @return string[] |
67
|
|
|
*/ |
68
|
3884 |
|
private static function getNamedPlaceholderPositions(string $statement) : array |
69
|
68 |
|
{ |
70
|
3884 |
|
return self::collectPlaceholders( |
71
|
3884 |
|
$statement, |
72
|
|
|
':', |
73
|
3866 |
|
self::NAMED_TOKEN, |
74
|
3884 |
|
static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void { |
75
|
|
|
$carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1); |
76
|
|
|
} |
77
|
|
|
); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @return mixed[] |
82
|
|
|
*/ |
83
|
3197 |
|
private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array |
84
|
|
|
{ |
85
|
3197 |
|
if (strpos($statement, $match) === false) { |
86
|
116 |
|
return []; |
87
|
3197 |
|
} |
88
|
3197 |
|
|
89
|
|
|
$carry = []; |
90
|
3193 |
|
|
91
|
3197 |
|
foreach (self::getUnquotedStatementFragments($statement) as $fragment) { |
92
|
|
|
preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE); |
93
|
|
|
foreach ($matches[0] as $placeholder) { |
94
|
|
|
$collector($placeholder[0], $placeholder[1], $fragment[1], $carry); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
4000 |
|
return $carry; |
99
|
|
|
} |
100
|
4000 |
|
|
101
|
1354 |
|
/** |
102
|
|
|
* For a positional query this method can rewrite the sql statement with regard to array parameters. |
103
|
|
|
* |
104
|
3996 |
|
* @param string $query The SQL query to execute. |
105
|
|
|
* @param mixed[] $params The parameters to bind to the query. |
106
|
3996 |
|
* @param int[]|string[] $types The types the previous parameters are in. |
107
|
3996 |
|
* |
108
|
3996 |
|
* @return mixed[] |
109
|
3996 |
|
* |
110
|
|
|
* @throws SQLParserUtilsException |
111
|
|
|
*/ |
112
|
|
|
public static function expandListParameters(string $query, array $params, array $types) : array |
113
|
3996 |
|
{ |
114
|
|
|
$isPositional = is_int(key($params)); |
115
|
|
|
$arrayPositions = []; |
116
|
|
|
$bindIndex = -1; |
117
|
|
|
|
118
|
|
|
if ($isPositional) { |
119
|
|
|
ksort($params); |
120
|
|
|
ksort($types); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
foreach ($types as $name => $type) { |
124
|
|
|
++$bindIndex; |
125
|
|
|
|
126
|
|
|
if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) { |
127
|
4713 |
|
continue; |
128
|
|
|
} |
129
|
4713 |
|
|
130
|
4713 |
|
if ($isPositional) { |
131
|
4713 |
|
$name = $bindIndex; |
132
|
|
|
} |
133
|
4713 |
|
|
134
|
4647 |
|
$arrayPositions[$name] = false; |
135
|
4647 |
|
} |
136
|
|
|
|
137
|
|
|
if (( ! $arrayPositions && $isPositional)) { |
|
|
|
|
138
|
4713 |
|
return [$query, $params, $types]; |
139
|
4455 |
|
} |
140
|
|
|
|
141
|
4455 |
|
if ($isPositional) { |
142
|
4415 |
|
$paramOffset = 0; |
143
|
|
|
$queryOffset = 0; |
144
|
|
|
$params = array_values($params); |
145
|
3882 |
|
$types = array_values($types); |
146
|
3838 |
|
|
147
|
|
|
$paramPos = self::getPositionalPlaceholderPositions($query); |
148
|
|
|
|
149
|
3882 |
|
foreach ($paramPos as $needle => $needlePos) { |
150
|
|
|
if (! isset($arrayPositions[$needle])) { |
151
|
|
|
continue; |
152
|
4713 |
|
} |
153
|
4627 |
|
|
154
|
|
|
$needle += $paramOffset; |
155
|
|
|
$needlePos += $queryOffset; |
156
|
3906 |
|
$count = count($params[$needle]); |
157
|
3838 |
|
|
158
|
3838 |
|
$params = array_merge( |
159
|
3838 |
|
array_slice($params, 0, $needle), |
160
|
3838 |
|
$params[$needle], |
161
|
|
|
array_slice($params, $needle + 1) |
162
|
3838 |
|
); |
163
|
|
|
|
164
|
3838 |
|
$types = array_merge( |
165
|
3838 |
|
array_slice($types, 0, $needle), |
166
|
835 |
|
$count ? |
167
|
|
|
// array needles are at {@link \Doctrine\DBAL\ParameterType} constants |
168
|
|
|
// + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET} |
169
|
3838 |
|
array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) : |
170
|
3838 |
|
[], |
171
|
3838 |
|
array_slice($types, $needle + 1) |
172
|
|
|
); |
173
|
3838 |
|
|
174
|
3838 |
|
$expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL'; |
175
|
3838 |
|
$query = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1); |
176
|
3838 |
|
|
177
|
|
|
$paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle. |
178
|
|
|
$queryOffset += (strlen($expandStr) - 1); |
179
|
3838 |
|
} |
180
|
3838 |
|
|
181
|
3838 |
|
return [$query, $params, $types]; |
182
|
|
|
} |
183
|
|
|
|
184
|
3832 |
|
$queryOffset = 0; |
185
|
3838 |
|
$typesOrd = []; |
186
|
3838 |
|
$paramsOrd = []; |
187
|
|
|
|
188
|
|
|
$paramPos = self::getNamedPlaceholderPositions($query); |
189
|
3838 |
|
|
190
|
3838 |
|
foreach ($paramPos as $pos => $paramName) { |
191
|
|
|
$paramLen = strlen($paramName) + 1; |
192
|
3838 |
|
$value = static::extractParam($paramName, $params, true); |
|
|
|
|
193
|
3838 |
|
|
194
|
|
|
if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) { |
195
|
|
|
$pos += $queryOffset; |
196
|
3838 |
|
$queryOffset -= ($paramLen - 1); |
197
|
|
|
$paramsOrd[] = $value; |
198
|
|
|
$typesOrd[] = static::extractParam($paramName, $types, false, ParameterType::STRING); |
|
|
|
|
199
|
3149 |
|
$query = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen)); |
200
|
3149 |
|
|
201
|
3149 |
|
continue; |
202
|
|
|
} |
203
|
3149 |
|
|
204
|
|
|
$count = count($value); |
205
|
3149 |
|
$expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL'; |
206
|
3149 |
|
|
207
|
3149 |
|
foreach ($value as $val) { |
208
|
|
|
$paramsOrd[] = $val; |
209
|
3137 |
|
$typesOrd[] = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET; |
|
|
|
|
210
|
3123 |
|
} |
211
|
3123 |
|
|
212
|
3123 |
|
$pos += $queryOffset; |
213
|
3123 |
|
$queryOffset += (strlen($expandStr) - $paramLen); |
214
|
3123 |
|
$query = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen)); |
215
|
|
|
} |
216
|
3123 |
|
|
217
|
|
|
return [$query, $paramsOrd, $typesOrd]; |
218
|
|
|
} |
219
|
3117 |
|
|
220
|
3117 |
|
/** |
221
|
|
|
* Slice the SQL statement around pairs of quotes and |
222
|
3117 |
|
* return string fragments of SQL outside of quoted literals. |
223
|
3113 |
|
* Each fragment is captured as a 2-element array: |
224
|
3113 |
|
* |
225
|
|
|
* 0 => matched fragment string, |
226
|
|
|
* 1 => offset of fragment in $statement |
227
|
3117 |
|
* |
228
|
3117 |
|
* @return mixed[][] |
229
|
3117 |
|
*/ |
230
|
|
|
private static function getUnquotedStatementFragments(string $statement) : array |
231
|
|
|
{ |
232
|
3137 |
|
$literal = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' . |
|
|
|
|
233
|
|
|
self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' . |
234
|
|
|
self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' . |
235
|
|
|
self::ESCAPED_BRACKET_QUOTED_TEXT; |
236
|
|
|
$expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal); |
237
|
|
|
|
238
|
|
|
preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE); |
239
|
|
|
|
240
|
|
|
return $fragments[1]; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* @param string $paramName The name of the parameter (without a colon in front) |
245
|
|
|
* @param mixed $paramsOrTypes A hash of parameters or types |
246
|
|
|
* @param mixed $defaultValue An optional default value. If omitted, an exception is thrown |
247
|
3996 |
|
* |
248
|
|
|
* @return mixed |
249
|
3996 |
|
* |
250
|
3996 |
|
* @throws SQLParserUtilsException |
251
|
3996 |
|
*/ |
252
|
3996 |
|
private static function extractParam(string $paramName, $paramsOrTypes, bool $isParam, $defaultValue = null) |
253
|
3996 |
|
{ |
254
|
|
|
if (array_key_exists($paramName, $paramsOrTypes)) { |
255
|
3996 |
|
return $paramsOrTypes[$paramName]; |
256
|
|
|
} |
257
|
3996 |
|
|
258
|
|
|
// Hash keys can be prefixed with a colon for compatibility |
259
|
|
|
if (array_key_exists(':' . $paramName, $paramsOrTypes)) { |
260
|
|
|
return $paramsOrTypes[':' . $paramName]; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
if ($defaultValue !== null) { |
264
|
|
|
return $defaultValue; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
if ($isParam) { |
268
|
|
|
throw MissingArrayParameter::new($paramName); |
269
|
|
|
} |
270
|
3149 |
|
|
271
|
|
|
throw MissingArrayParameterType::new($paramName); |
272
|
3149 |
|
} |
273
|
|
|
} |
274
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.