Issues (201)

OCI8/ConvertPositionalToNamedPlaceholders.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\OCI8;
6
7
use function count;
8
use function implode;
9
use function preg_match;
10
use function preg_quote;
11
use function substr;
12
use const PREG_OFFSET_CAPTURE;
13
14
/**
15
 * Converts positional (?) into named placeholders (:param<num>).
16
 *
17
 * Oracle does not support positional parameters, hence this method converts all
18
 * positional parameters into artificially named parameters. Note that this conversion
19
 * is not perfect. All question marks (?) in the original statement are treated as
20
 * placeholders and converted to a named parameter.
21
 *
22
 * @internal This class is not covered by the backward compatibility promise
23
 */
24
final class ConvertPositionalToNamedPlaceholders
25
{
26
    /**
27
     * @param string $statement The SQL statement to convert.
28
     *
29
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
30
     *
31
     * @throws OCI8Exception
32
     */
33 639
    public function __invoke(string $statement) : array
34
    {
35 639
        $fragmentOffset          = $tokenOffset = 0;
36 639
        $fragments               = $paramMap = [];
37 639
        $currentLiteralDelimiter = null;
38
39
        do {
40 639
            if ($currentLiteralDelimiter === null) {
41 639
                $result = $this->findPlaceholderOrOpeningQuote(
42 639
                    $statement,
43
                    $tokenOffset,
44
                    $fragmentOffset,
45
                    $fragments,
46
                    $currentLiteralDelimiter,
47
                    $paramMap
48
                );
49
            } else {
50 414
                $result = $this->findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
0 ignored issues
show
$currentLiteralDelimiter of type null is incompatible with the type string expected by parameter $currentLiteralDelimiter of Doctrine\DBAL\Driver\OCI...ers::findClosingQuote(). ( Ignorable by Annotation )

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

50
                $result = $this->findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
51
            }
52 639
        } while ($result);
53
54 639
        if ($currentLiteralDelimiter) {
55 66
            throw NonTerminatedStringLiteral::new($tokenOffset - 1);
56
        }
57
58 573
        $fragments[] = substr($statement, $fragmentOffset);
59 573
        $statement   = implode('', $fragments);
60
61 573
        return [$statement, $paramMap];
62
    }
63
64
    /**
65
     * Finds next placeholder or opening quote.
66
     *
67
     * @param string      $statement               The SQL statement to parse
68
     * @param int         $tokenOffset             The offset to start searching from
69
     * @param int         $fragmentOffset          The offset to build the next fragment from
70
     * @param string[]    $fragments               Fragments of the original statement not containing placeholders
71
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
72
     *                                             or NULL if not currently in a literal
73
     * @param string[]    $paramMap                Mapping of the original parameter positions to their named replacements
74
     *
75
     * @return bool Whether the token was found
76
     */
77 639
    private function findPlaceholderOrOpeningQuote(
78
        string $statement,
79
        int &$tokenOffset,
80
        int &$fragmentOffset,
81
        array &$fragments,
82
        ?string &$currentLiteralDelimiter,
83
        array &$paramMap
84
    ) : bool {
85 639
        $token = $this->findToken($statement, $tokenOffset, '/[?\'"]/');
86
87 639
        if ($token === null) {
88 573
            return false;
89
        }
90
91 558
        if ($token === '?') {
92 367
            $position            = count($paramMap) + 1;
93 367
            $param               = ':param' . $position;
94 367
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
95 367
            $fragments[]         = $param;
96 367
            $paramMap[$position] = $param;
97 367
            $tokenOffset        += 1;
98 367
            $fragmentOffset      = $tokenOffset;
99
100 367
            return true;
101
        }
102
103 414
        $currentLiteralDelimiter = $token;
104 414
        ++$tokenOffset;
105
106 414
        return true;
107
    }
108
109
    /**
110
     * Finds closing quote
111
     *
112
     * @param string $statement               The SQL statement to parse
113
     * @param int    $tokenOffset             The offset to start searching from
114
     * @param string $currentLiteralDelimiter The delimiter of the current string literal
115
     *
116
     * @return bool Whether the token was found
117
     */
118 414
    private function findClosingQuote(
119
        string $statement,
120
        int &$tokenOffset,
121
        string &$currentLiteralDelimiter
122
    ) : bool {
123 414
        $token = $this->findToken(
124 183
            $statement,
125
            $tokenOffset,
126 414
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
127
        );
128
129 414
        if ($token === null) {
130 66
            return false;
131
        }
132
133 370
        $currentLiteralDelimiter = null;
134 370
        ++$tokenOffset;
135
136 370
        return true;
137
    }
138
139
    /**
140
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
141
     * where the token was found.
142
     *
143
     * @param string $statement The SQL statement to parse
144
     * @param int    $offset    The offset to start searching from
145
     * @param string $regex     The regex containing token pattern
146
     *
147
     * @return string|null Token or NULL if not found
148
     */
149 639
    private function findToken(string $statement, int &$offset, string $regex) : ?string
150
    {
151 639
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset) === 1) {
152 558
            $offset = $matches[0][1];
153
154 558
            return $matches[0][0];
155
        }
156
157 639
        return null;
158
    }
159
}
160