Completed
Push — master ( bfc8bb...04db0e )
by Marco
20s queued 15s
created

ConvertPositionalToNamedPlaceholders::__invoke()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 20
dl 0
loc 29
rs 9.6
c 1
b 0
f 0
cc 4
nc 4
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\OCI8;
6
7
use const PREG_OFFSET_CAPTURE;
8
use function count;
9
use function implode;
10
use function preg_match;
11
use function preg_quote;
12
use function substr;
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
    public function __invoke(string $statement) : array
34
    {
35
        $fragmentOffset          = $tokenOffset = 0;
36
        $fragments               = $paramMap = [];
37
        $currentLiteralDelimiter = null;
38
39
        do {
40
            if ($currentLiteralDelimiter === null) {
41
                $result = $this->findPlaceholderOrOpeningQuote(
42
                    $statement,
43
                    $tokenOffset,
44
                    $fragmentOffset,
45
                    $fragments,
46
                    $currentLiteralDelimiter,
47
                    $paramMap
48
                );
49
            } else {
50
                $result = $this->findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
0 ignored issues
show
Bug introduced by
$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
        } while ($result);
53
54
        if ($currentLiteralDelimiter) {
55
            throw NonTerminatedStringLiteral::new($tokenOffset - 1);
56
        }
57
58
        $fragments[] = substr($statement, $fragmentOffset);
59
        $statement   = implode('', $fragments);
60
61
        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
    private function findPlaceholderOrOpeningQuote(
78
        string $statement,
79
        int &$tokenOffset,
80
        int &$fragmentOffset,
81
        array &$fragments,
82
        ?string &$currentLiteralDelimiter,
83
        array &$paramMap
84
    ) : bool {
85
        $token = $this->findToken($statement, $tokenOffset, '/[?\'"]/');
86
87
        if (! $token) {
88
            return false;
89
        }
90
91
        if ($token === '?') {
92
            $position            = count($paramMap) + 1;
93
            $param               = ':param' . $position;
94
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
95
            $fragments[]         = $param;
96
            $paramMap[$position] = $param;
97
            $tokenOffset        += 1;
98
            $fragmentOffset      = $tokenOffset;
99
100
            return true;
101
        }
102
103
        $currentLiteralDelimiter = $token;
104
        ++$tokenOffset;
105
106
        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
    private function findClosingQuote(
119
        string $statement,
120
        int &$tokenOffset,
121
        string &$currentLiteralDelimiter
122
    ) : bool {
123
        $token = $this->findToken(
124
            $statement,
125
            $tokenOffset,
126
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
127
        );
128
129
        if (! $token) {
130
            return false;
131
        }
132
133
        $currentLiteralDelimiter = null;
134
        ++$tokenOffset;
135
136
        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
    private function findToken(string $statement, int &$offset, string $regex) : ?string
150
    {
151
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
152
            $offset = $matches[0][1];
153
154
            return $matches[0][0];
155
        }
156
157
        return null;
158
    }
159
}
160