Passed
Pull Request — 2.10.x (#3896)
by Sergei
12:54
created

SQLParserUtils::extractParam()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 20
ccs 9
cts 10
cp 0.9
rs 9.6111
c 0
b 0
f 0
cc 5
nc 5
nop 4
crap 5.025
1
<?php
2
3
namespace Doctrine\DBAL;
4
5
use const PREG_OFFSET_CAPTURE;
6
use function array_fill;
7
use function array_fill_keys;
8
use function array_key_exists;
9
use function array_keys;
10
use function array_merge;
11
use function array_slice;
12
use function array_values;
13
use function count;
14
use function implode;
15
use function is_int;
16
use function key;
17
use function ksort;
18
use function preg_match_all;
19
use function sprintf;
20
use function strlen;
21
use function strpos;
22
use function substr;
23
24
/**
25
 * Utility class that parses sql statements with regard to types and parameters.
26
 */
27
class SQLParserUtils
28
{
29
    /**#@+
30
     *
31
     * @deprecated Will be removed as internal implementation details.
32
     */
33
    public const POSITIONAL_TOKEN = '\?';
34
    public const NAMED_TOKEN      = '(?<!:):[a-zA-Z_][a-zA-Z0-9_]*';
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
    /**#@-*/
40
41
    private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
42
43
    /**
44
     * Gets an array of the placeholders in an sql statements as keys and their positions in the query string.
45
     *
46
     * For a statement with positional parameters, returns a zero-indexed list of placeholder position.
47
     * For a statement with named parameters, returns a map of placeholder positions to their parameter names.
48
     *
49
     * @deprecated Will be removed as internal implementation detail.
50
     *
51
     * @param string $statement
52
     * @param bool   $isPositional
53
     *
54
     * @return int[]|string[]
55
     */
56 1384
    public static function getPlaceholderPositions($statement, $isPositional = true)
57
    {
58 1384
        return $isPositional
59 1334
            ? self::getPositionalPlaceholderPositions($statement)
60 1384
            : self::getNamedPlaceholderPositions($statement);
61
    }
62
63
    /**
64
     * Returns a zero-indexed list of placeholder position.
65
     *
66
     * @return int[]
67
     */
68 3630
    private static function getPositionalPlaceholderPositions(string $statement) : array
69
    {
70 3630
        return self::collectPlaceholders(
71 70
            $statement,
72 3630
            '?',
73 3630
            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

73
            /** @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...
74
            static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
75 3612
                $carry[] = $placeholderPosition + $fragmentPosition;
76 3630
            }
77
        );
78
    }
79
80
    /**
81
     * Returns a map of placeholder positions to their parameter names.
82
     *
83
     * @return string[]
84
     */
85 3013
    private static function getNamedPlaceholderPositions(string $statement) : array
86
    {
87 3013
        return self::collectPlaceholders(
88 120
            $statement,
89 3013
            ':',
90 3013
            self::NAMED_TOKEN,
91
            static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
92 3009
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
93 3013
            }
94
        );
95
    }
96
97
    /**
98
     * @return mixed[]
99
     */
100 3750
    private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
101
    {
102 3750
        if (strpos($statement, $match) === false) {
103 1292
            return [];
104
        }
105
106 3746
        $carry = [];
107
108 3746
        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
109 3746
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
110 3746
            foreach ($matches[0] as $placeholder) {
111 3728
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
112
            }
113
        }
114
115 3746
        return $carry;
116
    }
117
118
    /**
119
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
120
     *
121
     * @param string         $query  The SQL query to execute.
122
     * @param mixed[]        $params The parameters to bind to the query.
123
     * @param int[]|string[] $types  The types the previous parameters are in.
124
     *
125
     * @return mixed[]
126
     *
127
     * @throws SQLParserUtilsException
128
     */
129 4425
    public static function expandListParameters($query, $params, $types)
130
    {
131 4425
        $isPositional   = is_int(key($params));
132 4425
        $arrayPositions = [];
133 4425
        $bindIndex      = -1;
134
135 4425
        if ($isPositional) {
136
            // make sure that $types has the same keys as $params
137
            // to allow omitting parameters with unspecified types
138 4357
            $types += array_fill_keys(array_keys($params), null);
139
140 4357
            ksort($params);
141 4357
            ksort($types);
142
        }
143
144 4425
        foreach ($types as $name => $type) {
145 4419
            ++$bindIndex;
146
147 4419
            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
148 4381
                continue;
149
            }
150
151 3628
            if ($isPositional) {
152 3584
                $name = $bindIndex;
153
            }
154
155 3628
            $arrayPositions[$name] = false;
156
        }
157
158 4425
        if (( ! $arrayPositions && $isPositional)) {
159 4335
            return [$query, $params, $types];
160
        }
161
162 3654
        if ($isPositional) {
163 3584
            $paramOffset = 0;
164 3584
            $queryOffset = 0;
165 3584
            $params      = array_values($params);
166 3584
            $types       = array_values($types);
167
168 3584
            $paramPos = self::getPositionalPlaceholderPositions($query);
169
170 3584
            foreach ($paramPos as $needle => $needlePos) {
171 3584
                if (! isset($arrayPositions[$needle])) {
172 794
                    continue;
173
                }
174
175 3584
                $needle    += $paramOffset;
176 3584
                $needlePos += $queryOffset;
177 3584
                $count      = count($params[$needle]);
178
179 3584
                $params = array_merge(
180 3584
                    array_slice($params, 0, $needle),
181 3584
                    $params[$needle],
182 3584
                    array_slice($params, $needle + 1)
183
                );
184
185 3584
                $types = array_merge(
186 3584
                    array_slice($types, 0, $needle),
187 3584
                    $count ?
188
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
189
                        // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
190 3578
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
191 3584
                        [],
192 3584
                    array_slice($types, $needle + 1)
193
                );
194
195 3584
                $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
196 3584
                $query     = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
197
198 3584
                $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle.
199 3584
                $queryOffset += (strlen($expandStr) - 1);
200
            }
201
202 3584
            return [$query, $params, $types];
203
        }
204
205 2963
        $queryOffset = 0;
206 2963
        $typesOrd    = [];
207 2963
        $paramsOrd   = [];
208
209 2963
        $paramPos = self::getNamedPlaceholderPositions($query);
210
211 2963
        foreach ($paramPos as $pos => $paramName) {
212 2963
            $paramLen = strlen($paramName) + 1;
213 2963
            $value    = static::extractParam($paramName, $params, true);
214
215 2951
            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
216 2937
                $pos         += $queryOffset;
217 2937
                $queryOffset -= ($paramLen - 1);
218 2937
                $paramsOrd[]  = $value;
219 2937
                $typesOrd[]   = static::extractParam($paramName, $types, false, ParameterType::STRING);
220 2937
                $query        = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
221
222 2937
                continue;
223
            }
224
225 2929
            $count     = count($value);
226 2929
            $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
227
228 2929
            foreach ($value as $val) {
229 2925
                $paramsOrd[] = $val;
230 2925
                $typesOrd[]  = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
231
            }
232
233 2929
            $pos         += $queryOffset;
234 2929
            $queryOffset += (strlen($expandStr) - $paramLen);
235 2929
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
236
        }
237
238 2951
        return [$query, $paramsOrd, $typesOrd];
239
    }
240
241
    /**
242
     * Slice the SQL statement around pairs of quotes and
243
     * return string fragments of SQL outside of quoted literals.
244
     * Each fragment is captured as a 2-element array:
245
     *
246
     * 0 => matched fragment string,
247
     * 1 => offset of fragment in $statement
248
     *
249
     * @param string $statement
250
     *
251
     * @return mixed[][]
252
     */
253 3746
    private static function getUnquotedStatementFragments($statement)
254
    {
255 3746
        $literal    = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
256 3746
            self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
257 3746
            self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
258 3746
            self::ESCAPED_BRACKET_QUOTED_TEXT;
259 3746
        $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
260
261 3746
        preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
262
263 3746
        return $fragments[1];
264
    }
265
266
    /**
267
     * @param string $paramName     The name of the parameter (without a colon in front)
268
     * @param mixed  $paramsOrTypes A hash of parameters or types
269
     * @param bool   $isParam
270
     * @param mixed  $defaultValue  An optional default value. If omitted, an exception is thrown
271
     *
272
     * @return mixed
273
     *
274
     * @throws SQLParserUtilsException
275
     */
276 2963
    private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
277
    {
278 2963
        if (array_key_exists($paramName, $paramsOrTypes)) {
279 2947
            return $paramsOrTypes[$paramName];
280
        }
281
282
        // Hash keys can be prefixed with a colon for compatibility
283 375
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
284 313
            return $paramsOrTypes[':' . $paramName];
285
        }
286
287 369
        if ($defaultValue !== null) {
288 357
            return $defaultValue;
289
        }
290
291 127
        if ($isParam) {
292 127
            throw SQLParserUtilsException::missingParam($paramName);
293
        }
294
295
        throw SQLParserUtilsException::missingType($paramName);
296
    }
297
}
298