Passed
Pull Request — master (#2685)
by COLE
12:27
created

SQLParserUtils::startsWith()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL;
21
22
/**
23
 * Utility class that parses sql statements with regard to types and parameters.
24
 *
25
 * @link   www.doctrine-project.org
26
 * @since  2.0
27
 * @author Benjamin Eberlei <[email protected]>
28
 */
29
class SQLParserUtils
30
{
31
    const POSITIONAL_TOKEN = '\?';
32
    const NAMED_TOKEN      = '(?<!:):[a-zA-Z_][a-zA-Z0-9_]*';
33
34
    // Quote characters within string literals can be preceded by a backslash.
35
    const ESCAPED_SINGLE_QUOTED_TEXT = "(?:'(?:\\\\\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')";
36
    const ESCAPED_DOUBLE_QUOTED_TEXT = '(?:"(?:\\\\\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")';
37
    const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)';
38
    const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\bARRAY)\[(?:[^\]])*\]';
39
40
    /**
41
     * Gets an array of the placeholders in an sql statements as keys and their positions in the query string.
42
     *
43
     * Returns an integer => integer pair (indexed from zero) for a positional statement
44
     * and a string => int[] pair for a named statement.
45
     *
46
     * @param string  $statement
47
     * @param boolean $isPositional
48
     *
49
     * @return array
50
     */
51 89
    public static function getPlaceholderPositions($statement, $isPositional = true)
52
    {
53 89
        $match = ($isPositional) ? '?' : ':';
54 89
        if (strpos($statement, $match) === false) {
55 2
            return [];
56
        }
57
58 87
        $token = ($isPositional) ? self::POSITIONAL_TOKEN : self::NAMED_TOKEN;
59 87
        $paramMap = [];
60
61 87
        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
62 87
            preg_match_all("/$token/", $fragment[0], $matches, PREG_OFFSET_CAPTURE);
63 87
            foreach ($matches[0] as $placeholder) {
64 78
                if ($isPositional) {
65 27
                    $paramMap[] = $placeholder[1] + $fragment[1];
66
                } else {
67 51
                    $pos = $placeholder[1] + $fragment[1];
68 87
                    $paramMap[$pos] = substr($placeholder[0], 1, strlen($placeholder[0]));
69
                }
70
            }
71
        }
72
73 87
        return $paramMap;
74
    }
75
76 45
    static private function startsWith($haystack, $needle)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
77
    {
78 45
        $length = strlen($needle);
79 45
        return (substr($haystack, 0, $length) === $needle);
80
    }
81
82 3
    static private function endsWith($haystack, $needle)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
83
    {
84 3
        $length = strlen($needle);
85 3
        if ($length == 0) {
86
            return true;
87
        }
88
89 3
        return (substr($haystack, -$length) === $needle);
90
    }
91
92 64
    static private function isCollectionType($type)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
93
    {
94 64
        if ($type === Connection::PARAM_INT_ARRAY || $type === Connection::PARAM_STR_ARRAY)
95 33
            return true;
96
97 45
        return static::startsWith($type, '[') && static::endsWith($type, ']');
98
    }
99
100 26
    static private function removeCollectionMarker($type)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
101
    {
102 26
        if (!static::isCollectionType($type))
103
            throw SQLParserUtilsException::notCollectionType($type);
104
        
105 26
        if (is_numeric($type))
106 24
            return $type - Connection::ARRAY_PARAM_OFFSET;
107
108
        // [my_custom_type] -> my_custom_type
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
109 3
        return substr($type, 1, strlen($type) - 2);
110
    }
111
112
    /**
113
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
114
     *
115
     * @param string $query  The SQL query to execute.
116
     * @param array  $params The parameters to bind to the query.
117
     * @param array  $types  The types the previous parameters are in.
118
     *
119
     * @return array
120
     *
121
     * @throws SQLParserUtilsException
122
     */
123 148
    public static function expandListParameters($query, $params, $types)
124
    {
125 148
        $isPositional   = is_int(key($params));
126 148
        $arrayPositions = [];
127 148
        $bindIndex      = -1;
128
129 148
        if ($isPositional) {
130 115
            ksort($params);
131 115
            ksort($types);
132
        }
133
134 148
        foreach ($types as $name => $type) {
135 64
            ++$bindIndex;
136
137 64
            if (!static::isCollectionType($type)) {
138 42
                continue;
139
            }
140
141 35
            if ($isPositional) {
142 13
                $name = $bindIndex;
143
            }
144
145 35
            $arrayPositions[$name] = false;
146
        }
147
148 148
        if (( ! $arrayPositions && $isPositional)) {
149 103
            return [$query, $params, $types];
150
        }
151
152 47
        $paramPos = self::getPlaceholderPositions($query, $isPositional);
153
154 47
        if ($isPositional) {
155 13
            $paramOffset = 0;
156 13
            $queryOffset = 0;
157 13
            $params      = array_values($params);
158 13
            $types       = array_values($types);
159
160 13
            foreach ($paramPos as $needle => $needlePos) {
161 13
                if ( ! isset($arrayPositions[$needle])) {
162 5
                    continue;
163
                }
164
165 13
                $needle    += $paramOffset;
166 13
                $needlePos += $queryOffset;
167 13
                $count      = count($params[$needle]);
168
169 13
                $params = array_merge(
170 13
                    array_slice($params, 0, $needle),
171 13
                    $params[$needle],
172 13
                    array_slice($params, $needle + 1)
173
                );
174
175 13
                $types = array_merge(
176 13
                    array_slice($types, 0, $needle),
177 13
                    $count ?
178 10
                        array_fill(0, $count, static::removeCollectionMarker($types[$needle])) :
179 13
                        [],
180 13
                    array_slice($types, $needle + 1)
181
                );
182
183 13
                $expandStr  = $count ? implode(", ", array_fill(0, $count, "?")) : 'NULL';
184 13
                $query      = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
0 ignored issues
show
Bug introduced by
Are you sure substr($query, $needlePos + 1) of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

184
                $query      = substr($query, 0, $needlePos) . $expandStr . /** @scrutinizer ignore-type */ substr($query, $needlePos + 1);
Loading history...
Bug introduced by
Are you sure substr($query, 0, $needlePos) of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

184
                $query      = /** @scrutinizer ignore-type */ substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
Loading history...
185
186 13
                $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle.
187 13
                $queryOffset += (strlen($expandStr) - 1);
188
            }
189
190 13
            return [$query, $params, $types];
191
        }
192
193 34
        $queryOffset = 0;
194 34
        $typesOrd    = [];
195 34
        $paramsOrd   = [];
196
197 34
        foreach ($paramPos as $pos => $paramName) {
198 34
            $paramLen = strlen($paramName) + 1;
199 34
            $value    = static::extractParam($paramName, $params, true);
200
201 28
            if ( ! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
202 21
                $pos         += $queryOffset;
203 21
                $queryOffset -= ($paramLen - 1);
204 21
                $paramsOrd[]  = $value;
205 21
                $typesOrd[]   = static::extractParam($paramName, $types, false, \PDO::PARAM_STR);
206 21
                $query        = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
0 ignored issues
show
Bug introduced by
Are you sure substr($query, 0, $pos) of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

206
                $query        = /** @scrutinizer ignore-type */ substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
Loading history...
Bug introduced by
Are you sure substr($query, $pos + $paramLen) of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

206
                $query        = substr($query, 0, $pos) . '?' . /** @scrutinizer ignore-type */ substr($query, ($pos + $paramLen));
Loading history...
207
208 21
                continue;
209
            }
210
211 18
            $count      = count($value);
212 18
            $expandStr  = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
213
214 18
            foreach ($value as $val) {
215 16
                $paramsOrd[] = $val;
216 16
                $typesOrd[]  = static::removeCollectionMarker(static::extractParam($paramName, $types, false));
217
            }
218
219 18
            $pos         += $queryOffset;
220 18
            $queryOffset += (strlen($expandStr) - $paramLen);
221 18
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
222
        }
223
224 28
        return [$query, $paramsOrd, $typesOrd];
225
    }
226
227
    /**
228
     * Slice the SQL statement around pairs of quotes and
229
     * return string fragments of SQL outside of quoted literals.
230
     * Each fragment is captured as a 2-element array:
231
     *
232
     * 0 => matched fragment string,
233
     * 1 => offset of fragment in $statement
234
     *
235
     * @param string $statement
236
     * @return array
237
     */
238 87
    private static function getUnquotedStatementFragments($statement)
239
    {
240 87
        $literal = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
241 87
                   self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
242 87
                   self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
243 87
                   self::ESCAPED_BRACKET_QUOTED_TEXT;
244 87
        preg_match_all("/([^'\"`\[]+)(?:$literal)?/s", $statement, $fragments, PREG_OFFSET_CAPTURE);
245
246 87
        return $fragments[1];
247
    }
248
249
    /**
250
     * @param string    $paramName      The name of the parameter (without a colon in front)
251
     * @param array     $paramsOrTypes  A hash of parameters or types
252
     * @param bool      $isParam
253
     * @param mixed     $defaultValue   An optional default value. If omitted, an exception is thrown
254
     *
255
     * @throws SQLParserUtilsException
256
     * @return mixed
257
     */
258 34
    private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
259
    {
260 34
        if (array_key_exists($paramName, $paramsOrTypes)) {
261 27
            return $paramsOrTypes[$paramName];
262
        }
263
264
        // Hash keys can be prefixed with a colon for compatibility
265 14
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
266 6
            return $paramsOrTypes[':' . $paramName];
267
        }
268
269 11
        if (null !== $defaultValue) {
270 5
            return $defaultValue;
271
        }
272
273 6
        if ($isParam) {
274 6
            throw SQLParserUtilsException::missingParam($paramName);
275
        }
276
277
        throw SQLParserUtilsException::missingType($paramName);
278
    }
279
}
280