Completed
Pull Request — master (#3610)
by Sergei
03:03
created

SQLParserUtils   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 0
dl 0
loc 245
ccs 104
cts 104
cp 1
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getPositionalPlaceholderPositions() 0 11 1
A getNamedPlaceholderPositions() 0 11 1
A collectPlaceholders() 0 17 4
F expandListParameters() 0 107 18
A getUnquotedStatementFragments() 0 12 1
A extractParam() 0 21 5
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
     * @deprecated Will be removed as internal implementation details.
38
     */
39
    public const ESCAPED_SINGLE_QUOTED_TEXT   = "(?:'(?:\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')";
40
    public const ESCAPED_DOUBLE_QUOTED_TEXT   = '(?:"(?:\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")';
41
    public const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)';
42
    /**#@-*/
43
44
    private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
45
46
    /**
47
     * Returns a zero-indexed list of placeholder position.
48
     *
49
     * @return int[]
50
     */
51
    private static function getPositionalPlaceholderPositions(string $statement) : array
52
    {
53
        return self::collectPlaceholders(
54 1444
            $statement,
55
            '?',
56 1444
            self::POSITIONAL_TOKEN,
57 1396
            static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
58 1444
                $carry[] = $placeholderPosition + $fragmentPosition;
59
            }
60
        );
61
    }
62
63
    /**
64
     * Returns a map of placeholder positions to their parameter names.
65
     *
66 3884
     * @return string[]
67
     */
68 3884
    private static function getNamedPlaceholderPositions(string $statement) : array
69 68
    {
70 3884
        return self::collectPlaceholders(
71 3884
            $statement,
72
            ':',
73 3866
            self::NAMED_TOKEN,
74 3884
            static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
75
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
76
            }
77
        );
78
    }
79
80
    /**
81
     * @return mixed[]
82
     */
83 3197
    private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
84
    {
85 3197
        if (strpos($statement, $match) === false) {
86 116
            return [];
87 3197
        }
88 3197
89
        $carry = [];
90 3193
91 3197
        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
92
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
93
            foreach ($matches[0] as $placeholder) {
94
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
95
            }
96
        }
97
98 4000
        return $carry;
99
    }
100 4000
101 1354
    /**
102
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
103
     *
104 3996
     * @param string         $query  The SQL query to execute.
105
     * @param mixed[]        $params The parameters to bind to the query.
106 3996
     * @param int[]|string[] $types  The types the previous parameters are in.
107 3996
     *
108 3996
     * @return mixed[]
109 3996
     *
110
     * @throws SQLParserUtilsException
111
     */
112
    public static function expandListParameters(string $query, array $params, array $types) : array
113 3996
    {
114
        $isPositional   = is_int(key($params));
115
        $arrayPositions = [];
116
        $bindIndex      = -1;
117
118
        if ($isPositional) {
119
            ksort($params);
120
            ksort($types);
121
        }
122
123
        foreach ($types as $name => $type) {
124
            ++$bindIndex;
125
126
            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
127 4713
                continue;
128
            }
129 4713
130 4713
            if ($isPositional) {
131 4713
                $name = $bindIndex;
132
            }
133 4713
134 4647
            $arrayPositions[$name] = false;
135 4647
        }
136
137
        if (( ! $arrayPositions && $isPositional)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $arrayPositions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
138 4713
            return [$query, $params, $types];
139 4455
        }
140
141 4455
        if ($isPositional) {
142 4415
            $paramOffset = 0;
143
            $queryOffset = 0;
144
            $params      = array_values($params);
145 3882
            $types       = array_values($types);
146 3838
147
            $paramPos = self::getPositionalPlaceholderPositions($query);
148
149 3882
            foreach ($paramPos as $needle => $needlePos) {
150
                if (! isset($arrayPositions[$needle])) {
151
                    continue;
152 4713
                }
153 4627
154
                $needle    += $paramOffset;
155
                $needlePos += $queryOffset;
156 3906
                $count      = count($params[$needle]);
157 3838
158 3838
                $params = array_merge(
159 3838
                    array_slice($params, 0, $needle),
160 3838
                    $params[$needle],
161
                    array_slice($params, $needle + 1)
162 3838
                );
163
164 3838
                $types = array_merge(
165 3838
                    array_slice($types, 0, $needle),
166 835
                    $count ?
167
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
168
                        // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
169 3838
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
170 3838
                        [],
171 3838
                    array_slice($types, $needle + 1)
172
                );
173 3838
174 3838
                $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
175 3838
                $query     = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
176 3838
177
                $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle.
178
                $queryOffset += (strlen($expandStr) - 1);
179 3838
            }
180 3838
181 3838
            return [$query, $params, $types];
182
        }
183
184 3832
        $queryOffset = 0;
185 3838
        $typesOrd    = [];
186 3838
        $paramsOrd   = [];
187
188
        $paramPos = self::getNamedPlaceholderPositions($query);
189 3838
190 3838
        foreach ($paramPos as $pos => $paramName) {
191
            $paramLen = strlen($paramName) + 1;
192 3838
            $value    = static::extractParam($paramName, $params, true);
0 ignored issues
show
Bug introduced by
Since extractParam() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of extractParam() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
193 3838
194
            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
195
                $pos         += $queryOffset;
196 3838
                $queryOffset -= ($paramLen - 1);
197
                $paramsOrd[]  = $value;
198
                $typesOrd[]   = static::extractParam($paramName, $types, false, ParameterType::STRING);
0 ignored issues
show
Bug introduced by
Since extractParam() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of extractParam() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
199 3149
                $query        = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
200 3149
201 3149
                continue;
202
            }
203 3149
204
            $count     = count($value);
205 3149
            $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
206 3149
207 3149
            foreach ($value as $val) {
208
                $paramsOrd[] = $val;
209 3137
                $typesOrd[]  = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
0 ignored issues
show
Bug introduced by
Since extractParam() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of extractParam() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
210 3123
            }
211 3123
212 3123
            $pos         += $queryOffset;
213 3123
            $queryOffset += (strlen($expandStr) - $paramLen);
214 3123
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
215
        }
216 3123
217
        return [$query, $paramsOrd, $typesOrd];
218
    }
219 3117
220 3117
    /**
221
     * Slice the SQL statement around pairs of quotes and
222 3117
     * return string fragments of SQL outside of quoted literals.
223 3113
     * Each fragment is captured as a 2-element array:
224 3113
     *
225
     * 0 => matched fragment string,
226
     * 1 => offset of fragment in $statement
227 3117
     *
228 3117
     * @return mixed[][]
229 3117
     */
230
    private static function getUnquotedStatementFragments(string $statement) : array
231
    {
232 3137
        $literal    = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\SQLParserU...APED_SINGLE_QUOTED_TEXT has been deprecated with message: Will be removed as internal implementation details.

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...
233
            self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
234
            self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
235
            self::ESCAPED_BRACKET_QUOTED_TEXT;
236
        $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
237
238
        preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
239
240
        return $fragments[1];
241
    }
242
243
    /**
244
     * @param string $paramName     The name of the parameter (without a colon in front)
245
     * @param mixed  $paramsOrTypes A hash of parameters or types
246
     * @param mixed  $defaultValue  An optional default value. If omitted, an exception is thrown
247 3996
     *
248
     * @return mixed
249 3996
     *
250 3996
     * @throws SQLParserUtilsException
251 3996
     */
252 3996
    private static function extractParam(string $paramName, $paramsOrTypes, bool $isParam, $defaultValue = null)
253 3996
    {
254
        if (array_key_exists($paramName, $paramsOrTypes)) {
255 3996
            return $paramsOrTypes[$paramName];
256
        }
257 3996
258
        // Hash keys can be prefixed with a colon for compatibility
259
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
260
            return $paramsOrTypes[':' . $paramName];
261
        }
262
263
        if ($defaultValue !== null) {
264
            return $defaultValue;
265
        }
266
267
        if ($isParam) {
268
            throw MissingArrayParameter::new($paramName);
269
        }
270 3149
271
        throw MissingArrayParameterType::new($paramName);
272 3149
    }
273
}
274