Passed
Push — drop-deprecated ( db0b1f )
by Michael
27:00
created

SQLParserUtils::collectPlaceholders()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 16
ccs 8
cts 9
cp 0.8889
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 4
crap 4.0218
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL;
6
7
use const PREG_OFFSET_CAPTURE;
8
use function array_fill;
9
use function array_key_exists;
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
    private const POSITIONAL_TOKEN = '\?';
30
    private const NAMED_TOKEN      = '(?<!:):[a-zA-Z_][a-zA-Z0-9_]*';
31
32
    // Quote characters within string literals can be preceded by a backslash.
33
    public const ESCAPED_SINGLE_QUOTED_TEXT   = "(?:'(?:\\\\\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')";
34
    public const ESCAPED_DOUBLE_QUOTED_TEXT   = '(?:"(?:\\\\\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")';
35
    public const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)';
36
    private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
37
38
    /**
39
     * Returns a zero-indexed list of placeholder position.
40
     *
41
     * @return int[]
42
     */
43 3208
    private static function getPositionalPlaceholderPositions(string $statement) : array
44
    {
45 3208
        return self::collectPlaceholders(
46
            $statement,
47 3208
            '?',
48 3208
            self::POSITIONAL_TOKEN,
49
            static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
50 3208
                $carry[] = $placeholderPosition + $fragmentPosition;
51 3208
            }
52
        );
53
    }
54
55
    /**
56
     * Returns a map of placeholder positions to their parameter names.
57
     *
58
     * @return string[]
59
     */
60 2499
    private static function getNamedPlaceholderPositions(string $statement) : array
61
    {
62 2499
        return self::collectPlaceholders(
63
            $statement,
64 2499
            ':',
65 2499
            self::NAMED_TOKEN,
66
            static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
67 2499
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
68 2499
            }
69
        );
70
    }
71
72
    /**
73
     * @return mixed[]
74
     */
75 3208
    private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
76
    {
77 3208
        if (strpos($statement, $match) === false) {
78
            return [];
79
        }
80
81 3208
        $carry = [];
82
83 3208
        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
84 3208
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
85 3208
            foreach ($matches[0] as $placeholder) {
86 3208
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
87
            }
88
        }
89
90 3208
        return $carry;
91
    }
92
93
    /**
94
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
95
     *
96
     * @param string         $query  The SQL query to execute.
97
     * @param mixed[]        $params The parameters to bind to the query.
98
     * @param int[]|string[] $types  The types the previous parameters are in.
99
     *
100
     * @return mixed[]
101
     *
102
     * @throws SQLParserUtilsException
103
     */
104 3775
    public static function expandListParameters($query, $params, $types)
105
    {
106 3775
        $isPositional   = is_int(key($params));
107 3775
        $arrayPositions = [];
108 3775
        $bindIndex      = -1;
109
110 3775
        if ($isPositional) {
111 3775
            ksort($params);
112 3775
            ksort($types);
113
        }
114
115 3775
        foreach ($types as $name => $type) {
116 3703
            ++$bindIndex;
117
118 3703
            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
119 3703
                continue;
120
            }
121
122 3208
            if ($isPositional) {
123 3208
                $name = $bindIndex;
124
            }
125
126 3208
            $arrayPositions[$name] = false;
127
        }
128
129 3775
        if (( ! $arrayPositions && $isPositional)) {
130 3775
            return [$query, $params, $types];
131
        }
132
133 3208
        if ($isPositional) {
134 3208
            $paramOffset = 0;
135 3208
            $queryOffset = 0;
136 3208
            $params      = array_values($params);
137 3208
            $types       = array_values($types);
138
139 3208
            $paramPos = self::getPositionalPlaceholderPositions($query);
140
141 3208
            foreach ($paramPos as $needle => $needlePos) {
142 3208
                if (! isset($arrayPositions[$needle])) {
143 840
                    continue;
144
                }
145
146 3208
                $needle    += $paramOffset;
147 3208
                $needlePos += $queryOffset;
148 3208
                $count      = count($params[$needle]);
149
150 3208
                $params = array_merge(
151 3208
                    array_slice($params, 0, $needle),
152 3208
                    $params[$needle],
153 3208
                    array_slice($params, $needle + 1)
154
                );
155
156 3208
                $types = array_merge(
157 3208
                    array_slice($types, 0, $needle),
158 3208
                    $count ?
159
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
160
                        // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
161 3208
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
162 3208
                        [],
163 3208
                    array_slice($types, $needle + 1)
164
                );
165
166 3208
                $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
167 3208
                $query     = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
168
169 3208
                $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle.
170 3208
                $queryOffset += (strlen($expandStr) - 1);
171
            }
172
173 3208
            return [$query, $params, $types];
174
        }
175
176 2499
        $queryOffset = 0;
177 2499
        $typesOrd    = [];
178 2499
        $paramsOrd   = [];
179
180 2499
        $paramPos = self::getNamedPlaceholderPositions($query);
181
182 2499
        foreach ($paramPos as $pos => $paramName) {
183 2499
            $paramLen = strlen($paramName) + 1;
184 2499
            $value    = static::extractParam($paramName, $params, true);
185
186 2499
            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
187 2499
                $pos         += $queryOffset;
188 2499
                $queryOffset -= ($paramLen - 1);
189 2499
                $paramsOrd[]  = $value;
190 2499
                $typesOrd[]   = static::extractParam($paramName, $types, false, ParameterType::STRING);
191 2499
                $query        = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
192
193 2499
                continue;
194
            }
195
196 2499
            $count     = count($value);
197 2499
            $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
198
199 2499
            foreach ($value as $val) {
200 2499
                $paramsOrd[] = $val;
201 2499
                $typesOrd[]  = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
202
            }
203
204 2499
            $pos         += $queryOffset;
205 2499
            $queryOffset += (strlen($expandStr) - $paramLen);
206 2499
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
207
        }
208
209 2499
        return [$query, $paramsOrd, $typesOrd];
210
    }
211
212
    /**
213
     * Slice the SQL statement around pairs of quotes and
214
     * return string fragments of SQL outside of quoted literals.
215
     * Each fragment is captured as a 2-element array:
216
     *
217
     * 0 => matched fragment string,
218
     * 1 => offset of fragment in $statement
219
     *
220
     * @param string $statement
221
     *
222
     * @return mixed[][]
223
     */
224 3208
    private static function getUnquotedStatementFragments($statement)
225
    {
226 3208
        $literal    = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
227 3208
            self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
228 3208
            self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
229 3208
            self::ESCAPED_BRACKET_QUOTED_TEXT;
230 3208
        $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
231
232 3208
        preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
233
234 3208
        return $fragments[1];
235
    }
236
237
    /**
238
     * @param string $paramName     The name of the parameter (without a colon in front)
239
     * @param mixed  $paramsOrTypes A hash of parameters or types
240
     * @param bool   $isParam
241
     * @param mixed  $defaultValue  An optional default value. If omitted, an exception is thrown
242
     *
243
     * @return mixed
244
     *
245
     * @throws SQLParserUtilsException
246
     */
247 2499
    private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
248
    {
249 2499
        if (array_key_exists($paramName, $paramsOrTypes)) {
250 2499
            return $paramsOrTypes[$paramName];
251
        }
252
253
        // Hash keys can be prefixed with a colon for compatibility
254 384
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
255 336
            return $paramsOrTypes[':' . $paramName];
256
        }
257
258 384
        if ($defaultValue !== null) {
259 384
            return $defaultValue;
260
        }
261
262 168
        if ($isParam) {
263 168
            throw SQLParserUtilsException::missingParam($paramName);
264
        }
265
266
        throw SQLParserUtilsException::missingType($paramName);
267
    }
268
}
269