Failed Conditions
Push — develop ( 152bc9...e39bc0 )
by Sergei
102:42 queued 37:39
created

SQLParserUtils::getPlaceholderPositions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2
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 2116
     * @deprecated Will be removed as internal implementation detail.
48
     *
49 2116
     * @param string $statement
50 2116
     * @param bool   $isPositional
51 46
     *
52
     * @return int[]|string[]
53
     */
54 2070
    public static function getPlaceholderPositions($statement, $isPositional = true)
55 2070
    {
56
        return $isPositional
57 2070
            ? self::getPositionalPlaceholderPositions($statement)
58 2070
            : self::getNamedPlaceholderPositions($statement);
59 2070
    }
60 1863
61 575
    /**
62
     * Returns a zero-indexed list of placeholder position.
63 1288
     *
64 2070
     * @return int[]
65
     */
66
    private static function getPositionalPlaceholderPositions(string $statement) : array
67
    {
68
        return self::collectPlaceholders(
69 2070
            $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 3724
    private static function getNamedPlaceholderPositions(string $statement) : array
84
    {
85 3724
        return self::collectPlaceholders(
86 3724
            $statement,
87 3724
            ':',
88
            self::NAMED_TOKEN,
89 3724
            static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
90 2965
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
91 2965
            }
92
        );
93
    }
94 3724
95 1499
    /**
96
     * @return mixed[]
97 1499
     */
98 1039
    private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
99
    {
100
        if (strpos($statement, $match) === false) {
101 759
            return [];
102 253
        }
103
104
        $carry = [];
105 759
106
        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
107
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
108 3724
            foreach ($matches[0] as $placeholder) {
109 2735
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
110
            }
111
        }
112 1035
113
        return $carry;
114 1035
    }
115 253
116 253
    /**
117 253
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
118 253
     *
119
     * @param string         $query  The SQL query to execute.
120 253
     * @param mixed[]        $params The parameters to bind to the query.
121 253
     * @param int[]|string[] $types  The types the previous parameters are in.
122 115
     *
123
     * @return mixed[]
124
     *
125 253
     * @throws SQLParserUtilsException
126 253
     */
127 253
    public static function expandListParameters($query, $params, $types)
128
    {
129 253
        $isPositional   = is_int(key($params));
130 253
        $arrayPositions = [];
131 253
        $bindIndex      = -1;
132 253
133
        if ($isPositional) {
134
            ksort($params);
135 253
            ksort($types);
136 253
        }
137 253
138
        foreach ($types as $name => $type) {
139
            ++$bindIndex;
140 184
141 253
            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
142 253
                continue;
143
            }
144
145 253
            if ($isPositional) {
146 253
                $name = $bindIndex;
147
            }
148 253
149 253
            $arrayPositions[$name] = false;
150
        }
151
152 253
        if (( ! $arrayPositions && $isPositional)) {
153
            return [$query, $params, $types];
154
        }
155 782
156 782
        if ($isPositional) {
157 782
            $paramOffset = 0;
158
            $queryOffset = 0;
159 782
            $params      = array_values($params);
160 782
            $types       = array_values($types);
161 782
162
            $paramPos = self::getPositionalPlaceholderPositions($query);
163 644
164 483
            foreach ($paramPos as $needle => $needlePos) {
165 483
                if (! isset($arrayPositions[$needle])) {
166 483
                    continue;
167 483
                }
168 483
169
                $needle    += $paramOffset;
170 483
                $needlePos += $queryOffset;
171
                $count      = count($params[$needle]);
172
173 414
                $params = array_merge(
174 414
                    array_slice($params, 0, $needle),
175
                    $params[$needle],
176 414
                    array_slice($params, $needle + 1)
177 368
                );
178 368
179
                $types = array_merge(
180
                    array_slice($types, 0, $needle),
181 414
                    $count ?
182 414
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
183 414
                        // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
184
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
185
                        [],
186 644
                    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 2070
        $paramsOrd   = [];
202
203 2070
        $paramPos = self::getNamedPlaceholderPositions($query);
204 2070
205 2070
        foreach ($paramPos as $pos => $paramName) {
206 2070
            $paramLen = strlen($paramName) + 1;
207 2070
            $value    = static::extractParam($paramName, $params, true);
208
209 2070
            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
210
                $pos         += $queryOffset;
211 2070
                $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 782
                $typesOrd[]  = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
225
            }
226 782
227 621
            $pos         += $queryOffset;
228
            $queryOffset += (strlen($expandStr) - $paramLen);
229
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
230
        }
231 322
232 138
        return [$query, $paramsOrd, $typesOrd];
233
    }
234
235 253
    /**
236 115
     * 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 138
     *
240 138
     * 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