Passed
Push — type-registry ( f9a1df...0931f1 )
by Michael
24:09
created

SQLParserUtils::collectPlaceholders()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 4
crap 4
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
     * @deprecated Will be removed as internal implementation detail.
48
     *
49
     * @param string $statement
50
     * @param bool   $isPositional
51
     *
52
     * @return int[]|string[]
53
     */
54 1775
    public static function getPlaceholderPositions($statement, $isPositional = true)
55
    {
56 1775
        return $isPositional
57 1775
            ? self::getPositionalPlaceholderPositions($statement)
58 1775
            : self::getNamedPlaceholderPositions($statement);
59
    }
60
61
    /**
62
     * Returns a zero-indexed list of placeholder position.
63
     *
64
     * @return int[]
65
     */
66 4217
    private static function getPositionalPlaceholderPositions(string $statement) : array
67
    {
68 4217
        return self::collectPlaceholders(
69
            $statement,
70 4217
            '?',
71 4217
            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 4217
                $carry[] = $placeholderPosition + $fragmentPosition;
74 4217
            }
75
        );
76
    }
77
78
    /**
79
     * Returns a map of placeholder positions to their parameter names.
80
     *
81
     * @return string[]
82
     */
83 3482
    private static function getNamedPlaceholderPositions(string $statement) : array
84
    {
85 3482
        return self::collectPlaceholders(
86
            $statement,
87 3482
            ':',
88 3482
            self::NAMED_TOKEN,
89
            static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
90 3482
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
91 3482
            }
92
        );
93
    }
94
95
    /**
96
     * @return mixed[]
97
     */
98 4217
    private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
99
    {
100 4217
        if (strpos($statement, $match) === false) {
101 1775
            return [];
102
        }
103
104 4217
        $carry = [];
105
106 4217
        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
107 4217
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
108 4217
            foreach ($matches[0] as $placeholder) {
109 4217
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
110
            }
111
        }
112
113 4217
        return $carry;
114
    }
115
116
    /**
117
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
118
     *
119
     * @param string         $query  The SQL query to execute.
120
     * @param mixed[]        $params The parameters to bind to the query.
121
     * @param int[]|string[] $types  The types the previous parameters are in.
122
     *
123
     * @return mixed[]
124
     *
125
     * @throws SQLParserUtilsException
126
     */
127 4809
    public static function expandListParameters($query, $params, $types)
128
    {
129 4809
        $isPositional   = is_int(key($params));
130 4809
        $arrayPositions = [];
131 4809
        $bindIndex      = -1;
132
133 4809
        if ($isPositional) {
134 4809
            ksort($params);
135 4809
            ksort($types);
136
        }
137
138 4809
        foreach ($types as $name => $type) {
139 4734
            ++$bindIndex;
140
141 4734
            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
142 4734
                continue;
143
            }
144
145 4217
            if ($isPositional) {
146 4217
                $name = $bindIndex;
147
            }
148
149 4217
            $arrayPositions[$name] = false;
150
        }
151
152 4809
        if (( ! $arrayPositions && $isPositional)) {
153 4809
            return [$query, $params, $types];
154
        }
155
156 4217
        if ($isPositional) {
157 4217
            $paramOffset = 0;
158 4217
            $queryOffset = 0;
159 4217
            $params      = array_values($params);
160 4217
            $types       = array_values($types);
161
162 4217
            $paramPos = self::getPositionalPlaceholderPositions($query);
163
164 4217
            foreach ($paramPos as $needle => $needlePos) {
165 4217
                if (! isset($arrayPositions[$needle])) {
166 875
                    continue;
167
                }
168
169 4217
                $needle    += $paramOffset;
170 4217
                $needlePos += $queryOffset;
171 4217
                $count      = count($params[$needle]);
172
173 4217
                $params = array_merge(
174 4217
                    array_slice($params, 0, $needle),
175 4217
                    $params[$needle],
176 4217
                    array_slice($params, $needle + 1)
177
                );
178
179 4217
                $types = array_merge(
180 4217
                    array_slice($types, 0, $needle),
181 4217
                    $count ?
182
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
183
                        // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
184 4217
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
185 4217
                        [],
186 4217
                    array_slice($types, $needle + 1)
187
                );
188
189 4217
                $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
190 4217
                $query     = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
191
192 4217
                $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle.
193 4217
                $queryOffset += (strlen($expandStr) - 1);
194
            }
195
196 4217
            return [$query, $params, $types];
197
        }
198
199 3482
        $queryOffset = 0;
200 3482
        $typesOrd    = [];
201 3482
        $paramsOrd   = [];
202
203 3482
        $paramPos = self::getNamedPlaceholderPositions($query);
204
205 3482
        foreach ($paramPos as $pos => $paramName) {
206 3482
            $paramLen = strlen($paramName) + 1;
207 3482
            $value    = static::extractParam($paramName, $params, true);
208
209 3482
            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
210 3482
                $pos         += $queryOffset;
211 3482
                $queryOffset -= ($paramLen - 1);
212 3482
                $paramsOrd[]  = $value;
213 3482
                $typesOrd[]   = static::extractParam($paramName, $types, false, ParameterType::STRING);
214 3482
                $query        = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
215
216 3482
                continue;
217
            }
218
219 3482
            $count     = count($value);
220 3482
            $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
221
222 3482
            foreach ($value as $val) {
223 3482
                $paramsOrd[] = $val;
224 3482
                $typesOrd[]  = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
225
            }
226
227 3482
            $pos         += $queryOffset;
228 3482
            $queryOffset += (strlen($expandStr) - $paramLen);
229 3482
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
230
        }
231
232 3482
        return [$query, $paramsOrd, $typesOrd];
233
    }
234
235
    /**
236
     * 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
     *
240
     * 0 => matched fragment string,
241
     * 1 => offset of fragment in $statement
242
     *
243
     * @param string $statement
244
     *
245
     * @return mixed[][]
246
     */
247 4217
    private static function getUnquotedStatementFragments($statement)
248
    {
249 4217
        $literal    = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
250 4217
            self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
251 4217
            self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
252 4217
            self::ESCAPED_BRACKET_QUOTED_TEXT;
253 4217
        $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
254
255 4217
        preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
256
257 4217
        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 3482
    private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
271
    {
272 3482
        if (array_key_exists($paramName, $paramsOrTypes)) {
273 3482
            return $paramsOrTypes[$paramName];
274
        }
275
276
        // Hash keys can be prefixed with a colon for compatibility
277 400
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
278 350
            return $paramsOrTypes[':' . $paramName];
279
        }
280
281 400
        if ($defaultValue !== null) {
282 400
            return $defaultValue;
283
        }
284
285 175
        if ($isParam) {
286 175
            throw SQLParserUtilsException::missingParam($paramName);
287
        }
288
289
        throw SQLParserUtilsException::missingType($paramName);
290
    }
291
}
292