Completed
Push — master ( cc3868...bfc8bb )
by Marco
21s queued 15s
created

SQLParserUtils   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Test Coverage

Coverage 99.07%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 30
eloc 106
dl 0
loc 240
ccs 106
cts 107
cp 0.9907
rs 10
c 2
b 0
f 0

6 Methods

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