SQLParserUtils.php 9.71 KB
Newer Older
1 2 3 4
<?php

namespace Doctrine\DBAL;

5 6 7 8 9 10 11 12 13 14 15 16
use const PREG_OFFSET_CAPTURE;
use function array_fill;
use function array_key_exists;
use function array_merge;
use function array_slice;
use function array_values;
use function count;
use function implode;
use function is_int;
use function key;
use function ksort;
use function preg_match_all;
17
use function sprintf;
18 19 20 21
use function strlen;
use function strpos;
use function substr;

22 23 24
/**
 * Utility class that parses sql statements with regard to types and parameters.
 */
25 26
class SQLParserUtils
{
Sergei Morozov's avatar
Sergei Morozov committed
27 28 29 30
    /**#@+
     *
     * @deprecated Will be removed as internal implementation details.
     */
31 32
    public const POSITIONAL_TOKEN = '\?';
    public const NAMED_TOKEN      = '(?<!:):[a-zA-Z_][a-zA-Z0-9_]*';
Sergei Morozov's avatar
Sergei Morozov committed
33
    /**#@-*/
34 35

    // Quote characters within string literals can be preceded by a backslash.
36 37 38
    public const ESCAPED_SINGLE_QUOTED_TEXT   = "(?:'(?:\\\\\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')";
    public const ESCAPED_DOUBLE_QUOTED_TEXT   = '(?:"(?:\\\\\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")';
    public const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)';
39
    private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
40

41
    /**
Benjamin Morel's avatar
Benjamin Morel committed
42
     * Gets an array of the placeholders in an sql statements as keys and their positions in the query string.
43
     *
Sergei Morozov's avatar
Sergei Morozov committed
44 45
     * For a statement with positional parameters, returns a zero-indexed list of placeholder position.
     * For a statement with named parameters, returns a map of placeholder positions to their parameter names.
46
     *
Sergei Morozov's avatar
Sergei Morozov committed
47 48
     * @deprecated Will be removed as internal implementation detail.
     *
49 50
     * @param string $statement
     * @param bool   $isPositional
Benjamin Morel's avatar
Benjamin Morel committed
51
     *
Sergei Morozov's avatar
Sergei Morozov committed
52
     * @return int[]|string[]
53
     */
54
    public static function getPlaceholderPositions($statement, $isPositional = true)
55
    {
Sergei Morozov's avatar
Sergei Morozov committed
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
        return $isPositional
            ? self::getPositionalPlaceholderPositions($statement)
            : self::getNamedPlaceholderPositions($statement);
    }

    /**
     * Returns a zero-indexed list of placeholder position.
     *
     * @return int[]
     */
    private static function getPositionalPlaceholderPositions(string $statement) : array
    {
        return self::collectPlaceholders(
            $statement,
            '?',
            self::POSITIONAL_TOKEN,
            static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
                $carry[] = $placeholderPosition + $fragmentPosition;
            }
        );
    }

    /**
     * Returns a map of placeholder positions to their parameter names.
     *
     * @return string[]
     */
    private static function getNamedPlaceholderPositions(string $statement) : array
    {
        return self::collectPlaceholders(
            $statement,
            ':',
            self::NAMED_TOKEN,
            static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
            }
        );
    }

    /**
     * @return mixed[]
     */
    private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
    {
100
        if (strpos($statement, $match) === false) {
101
            return [];
102
        }
103

Sergei Morozov's avatar
Sergei Morozov committed
104
        $carry = [];
105 106

        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
107
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
108
            foreach ($matches[0] as $placeholder) {
Sergei Morozov's avatar
Sergei Morozov committed
109
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
110 111 112
            }
        }

Sergei Morozov's avatar
Sergei Morozov committed
113
        return $carry;
114
    }
115

116
    /**
117
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
118
     *
119 120 121
     * @param string         $query  The SQL query to execute.
     * @param mixed[]        $params The parameters to bind to the query.
     * @param int[]|string[] $types  The types the previous parameters are in.
122
     *
123
     * @return mixed[]
Benjamin Morel's avatar
Benjamin Morel committed
124 125
     *
     * @throws SQLParserUtilsException
126
     */
127
    public static function expandListParameters($query, $params, $types)
128
    {
Fabio B. Silva's avatar
Fabio B. Silva committed
129
        $isPositional   = is_int(key($params));
130
        $arrayPositions = [];
Fabio B. Silva's avatar
Fabio B. Silva committed
131 132
        $bindIndex      = -1;

133 134 135 136 137
        if ($isPositional) {
            ksort($params);
            ksort($types);
        }

138
        foreach ($types as $name => $type) {
139
            ++$bindIndex;
140

Fabio B. Silva's avatar
Fabio B. Silva committed
141 142 143 144 145 146
            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
                continue;
            }

            if ($isPositional) {
                $name = $bindIndex;
147
            }
Fabio B. Silva's avatar
Fabio B. Silva committed
148 149

            $arrayPositions[$name] = false;
150
        }
151

152
        if (( ! $arrayPositions && $isPositional)) {
153
            return [$query, $params, $types];
154
        }
155

156 157 158
        if ($isPositional) {
            $paramOffset = 0;
            $queryOffset = 0;
159 160
            $params      = array_values($params);
            $types       = array_values($types);
Fabio B. Silva's avatar
Fabio B. Silva committed
161

Sergei Morozov's avatar
Sergei Morozov committed
162 163
            $paramPos = self::getPositionalPlaceholderPositions($query);

164
            foreach ($paramPos as $needle => $needlePos) {
165
                if (! isset($arrayPositions[$needle])) {
166 167
                    continue;
                }
168

Fabio B. Silva's avatar
Fabio B. Silva committed
169
                $needle    += $paramOffset;
170
                $needlePos += $queryOffset;
Fabio B. Silva's avatar
Fabio B. Silva committed
171
                $count      = count($params[$needle]);
172

173
                $params = array_merge(
174
                    array_slice($params, 0, $needle),
175
                    $params[$needle],
176
                    array_slice($params, $needle + 1)
177
                );
178

179
                $types = array_merge(
180
                    array_slice($types, 0, $needle),
181
                    $count ?
182 183 184
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
                        // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
185
                        [],
186
                    array_slice($types, $needle + 1)
187
                );
188

189 190
                $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
                $query     = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
191

Fabio B. Silva's avatar
Fabio B. Silva committed
192
                $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle.
193 194
                $queryOffset += (strlen($expandStr) - 1);
            }
195

196
            return [$query, $params, $types];
Fabio B. Silva's avatar
Fabio B. Silva committed
197
        }
198

Fabio B. Silva's avatar
Fabio B. Silva committed
199
        $queryOffset = 0;
200 201
        $typesOrd    = [];
        $paramsOrd   = [];
Fabio B. Silva's avatar
Fabio B. Silva committed
202

Sergei Morozov's avatar
Sergei Morozov committed
203 204
        $paramPos = self::getNamedPlaceholderPositions($query);

Fabio B. Silva's avatar
Fabio B. Silva committed
205
        foreach ($paramPos as $pos => $paramName) {
206 207
            $paramLen = strlen($paramName) + 1;
            $value    = static::extractParam($paramName, $params, true);
Fabio B. Silva's avatar
Fabio B. Silva committed
208

209
            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
Fabio B. Silva's avatar
Fabio B. Silva committed
210 211 212
                $pos         += $queryOffset;
                $queryOffset -= ($paramLen - 1);
                $paramsOrd[]  = $value;
213
                $typesOrd[]   = static::extractParam($paramName, $types, false, ParameterType::STRING);
Fabio B. Silva's avatar
Fabio B. Silva committed
214
                $query        = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen));
215

Fabio B. Silva's avatar
Fabio B. Silva committed
216 217 218
                continue;
            }

219 220
            $count     = count($value);
            $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
Fabio B. Silva's avatar
Fabio B. Silva committed
221 222 223

            foreach ($value as $val) {
                $paramsOrd[] = $val;
224
                $typesOrd[]  = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
225
            }
226

Fabio B. Silva's avatar
Fabio B. Silva committed
227 228 229
            $pos         += $queryOffset;
            $queryOffset += (strlen($expandStr) - $paramLen);
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen));
230
        }
231

232
        return [$query, $paramsOrd, $typesOrd];
233
    }
234 235 236 237 238 239 240 241 242 243

    /**
     * Slice the SQL statement around pairs of quotes and
     * return string fragments of SQL outside of quoted literals.
     * Each fragment is captured as a 2-element array:
     *
     * 0 => matched fragment string,
     * 1 => offset of fragment in $statement
     *
     * @param string $statement
244
     *
245
     * @return mixed[][]
246
     */
247
    private static function getUnquotedStatementFragments($statement)
248
    {
249 250 251 252 253 254 255
        $literal    = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
            self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
            self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
            self::ESCAPED_BRACKET_QUOTED_TEXT;
        $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);

        preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
256 257 258

        return $fragments[1];
    }
259 260

    /**
261
     * @param string $paramName     The name of the parameter (without a colon in front)
262
     * @param mixed  $paramsOrTypes A hash of parameters or types
263 264
     * @param bool   $isParam
     * @param mixed  $defaultValue  An optional default value. If omitted, an exception is thrown
265 266
     *
     * @return mixed
267 268
     *
     * @throws SQLParserUtilsException
269
     */
270
    private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
271
    {
272
        if (array_key_exists($paramName, $paramsOrTypes)) {
273 274 275 276
            return $paramsOrTypes[$paramName];
        }

        // Hash keys can be prefixed with a colon for compatibility
277
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
278 279 280
            return $paramsOrTypes[':' . $paramName];
        }

281
        if ($defaultValue !== null) {
282 283 284 285 286 287
            return $defaultValue;
        }

        if ($isParam) {
            throw SQLParserUtilsException::missingParam($paramName);
        }
Benjamin Eberlei's avatar
Benjamin Eberlei committed
288 289

        throw SQLParserUtilsException::missingType($paramName);
290
    }
291
}