Failed Conditions
Pull Request — develop (#3518)
by Michael
29:00 queued 25:29
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
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 3395
    private static function getPositionalPlaceholderPositions(string $statement) : array
44
    {
45 3395
        return self::collectPlaceholders(
46
            $statement,
47 3395
            '?',
48 3395
            self::POSITIONAL_TOKEN,
49
            static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
50 3395
                $carry[] = $placeholderPosition + $fragmentPosition;
51 3395
            }
52
        );
53
    }
54
55
    /**
56
     * Returns a map of placeholder positions to their parameter names.
57
     *
58
     * @return string[]
59
     */
60 2655
    private static function getNamedPlaceholderPositions(string $statement) : array
61
    {
62 2655
        return self::collectPlaceholders(
63
            $statement,
64 2655
            ':',
65 2655
            self::NAMED_TOKEN,
66
            static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
67 2655
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
68 2655
            }
69
        );
70
    }
71
72
    /**
73
     * @return mixed[]
74
     */
75 3395
    private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
76
    {
77 3395
        if (strpos($statement, $match) === false) {
78
            return [];
79
        }
80
81 3395
        $carry = [];
82
83 3395
        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
84 3395
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
85 3395
            foreach ($matches[0] as $placeholder) {
86 3395
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
87
            }
88
        }
89
90 3395
        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 4009
    public static function expandListParameters($query, $params, $types)
105
    {
106 4009
        $isPositional   = is_int(key($params));
107 4009
        $arrayPositions = [];
108 4009
        $bindIndex      = -1;
109
110 4009
        if ($isPositional) {
111 4009
            ksort($params);
112 4009
            ksort($types);
113
        }
114
115 4009
        foreach ($types as $name => $type) {
116 3931
            ++$bindIndex;
117
118 3931
            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
119 3931
                continue;
120
            }
121
122 3395
            if ($isPositional) {
123 3395
                $name = $bindIndex;
124
            }
125
126 3395
            $arrayPositions[$name] = false;
127
        }
128
129 4009
        if (( ! $arrayPositions && $isPositional)) {
130 4009
            return [$query, $params, $types];
131
        }
132
133 3395
        if ($isPositional) {
134 3395
            $paramOffset = 0;
135 3395
            $queryOffset = 0;
136 3395
            $params      = array_values($params);
137 3395
            $types       = array_values($types);
138
139 3395
            $paramPos = self::getPositionalPlaceholderPositions($query);
140
141 3395
            foreach ($paramPos as $needle => $needlePos) {
142 3395
                if (! isset($arrayPositions[$needle])) {
143 910
                    continue;
144
                }
145
146 3395
                $needle    += $paramOffset;
147 3395
                $needlePos += $queryOffset;
148 3395
                $count      = count($params[$needle]);
149
150 3395
                $params = array_merge(
151 3395
                    array_slice($params, 0, $needle),
152 3395
                    $params[$needle],
153 3395
                    array_slice($params, $needle + 1)
154
                );
155
156 3395
                $types = array_merge(
157 3395
                    array_slice($types, 0, $needle),
158 3395
                    $count ?
159
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
160
                        // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
161 3395
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
162 3395
                        [],
163 3395
                    array_slice($types, $needle + 1)
164
                );
165
166 3395
                $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
167 3395
                $query     = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
168
169 3395
                $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle.
170 3395
                $queryOffset += (strlen($expandStr) - 1);
171
            }
172
173 3395
            return [$query, $params, $types];
174
        }
175
176 2655
        $queryOffset = 0;
177 2655
        $typesOrd    = [];
178 2655
        $paramsOrd   = [];
179
180 2655
        $paramPos = self::getNamedPlaceholderPositions($query);
181
182 2655
        foreach ($paramPos as $pos => $paramName) {
183 2655
            $paramLen = strlen($paramName) + 1;
184 2655
            $value    = static::extractParam($paramName, $params, true);
185
186 2655
            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
187 2655
                $pos         += $queryOffset;
188 2655
                $queryOffset -= ($paramLen - 1);
189 2655
                $paramsOrd[]  = $value;
190 2655
                $typesOrd[]   = static::extractParam($paramName, $types, false, ParameterType::STRING);
191 2655
                $query        = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
192
193 2655
                continue;
194
            }
195
196 2655
            $count     = count($value);
197 2655
            $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
198
199 2655
            foreach ($value as $val) {
200 2655
                $paramsOrd[] = $val;
201 2655
                $typesOrd[]  = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
202
            }
203
204 2655
            $pos         += $queryOffset;
205 2655
            $queryOffset += (strlen($expandStr) - $paramLen);
206 2655
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
207
        }
208
209 2655
        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 3395
    private static function getUnquotedStatementFragments($statement)
225
    {
226 3395
        $literal    = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
227 3395
            self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
228 3395
            self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
229 3395
            self::ESCAPED_BRACKET_QUOTED_TEXT;
230 3395
        $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
231
232 3395
        preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
233
234 3395
        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 2655
    private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
248
    {
249 2655
        if (array_key_exists($paramName, $paramsOrTypes)) {
250 2655
            return $paramsOrTypes[$paramName];
251
        }
252
253
        // Hash keys can be prefixed with a colon for compatibility
254 416
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
255 364
            return $paramsOrTypes[':' . $paramName];
256
        }
257
258 416
        if ($defaultValue !== null) {
259 416
            return $defaultValue;
260
        }
261
262 182
        if ($isParam) {
263 182
            throw SQLParserUtilsException::missingParam($paramName);
264
        }
265
266
        throw SQLParserUtilsException::missingType($paramName);
267
    }
268
}
269