Failed Conditions
Push — master ( 94cec7...7f79d0 )
by Marco
25s queued 13s
created

SQLParserUtils::getNamedPlaceholderPositions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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,
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\SQLParserUtils::POSITIONAL_TOKEN has been deprecated: Will be removed as internal implementation details. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

71
            /** @scrutinizer ignore-deprecated */ self::POSITIONAL_TOKEN,

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.

Loading history...
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