OCI8Statement.php 9.33 KB
Newer Older
1 2 3 4
<?php

namespace Doctrine\DBAL\Driver\OCI8;

5
use Doctrine\DBAL\Driver\Result as ResultInterface;
6
use Doctrine\DBAL\Driver\Statement;
7
use Doctrine\DBAL\ParameterType;
8

Sergei Morozov's avatar
Sergei Morozov committed
9
use function assert;
10 11
use function count;
use function implode;
Sergei Morozov's avatar
Sergei Morozov committed
12 13
use function is_int;
use function is_resource;
14 15 16 17 18 19 20 21 22
use function oci_bind_by_name;
use function oci_error;
use function oci_execute;
use function oci_new_descriptor;
use function oci_parse;
use function preg_match;
use function preg_quote;
use function sprintf;
use function substr;
23

Grégoire Paris's avatar
Grégoire Paris committed
24 25 26 27 28 29
use const OCI_B_BIN;
use const OCI_B_BLOB;
use const OCI_D_LOB;
use const OCI_TEMP_BLOB;
use const PREG_OFFSET_CAPTURE;
use const SQLT_CHR;
30 31 32 33

/**
 * The OCI8 implementation of the Statement interface.
 */
34
class OCI8Statement implements Statement
35
{
36
    /** @var resource */
37
    protected $_dbh;
Benjamin Morel's avatar
Benjamin Morel committed
38

39
    /** @var resource */
40
    protected $_sth;
Benjamin Morel's avatar
Benjamin Morel committed
41

42
    /** @var OCI8Connection */
43
    protected $_conn;
Benjamin Morel's avatar
Benjamin Morel committed
44

45 46 47 48 49
    /**
     * @deprecated
     *
     * @var string
     */
50
    protected static $_PARAM = ':param';
Benjamin Morel's avatar
Benjamin Morel committed
51

52
    /** @var string[] */
53
    protected $_paramMap = [];
54

55 56 57 58 59
    /**
     * Holds references to bound parameter values.
     *
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
     *
60
     * @var mixed[]
61
     */
62
    private $boundValues = [];
63

64 65 66
    /**
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
     *
Sergei Morozov's avatar
Sergei Morozov committed
67 68
     * @param resource $dbh   The connection handle.
     * @param string   $query The SQL query.
69
     */
Sergei Morozov's avatar
Sergei Morozov committed
70
    public function __construct($dbh, $query, OCI8Connection $conn)
71
    {
Sergei Morozov's avatar
Sergei Morozov committed
72 73 74 75 76 77 78 79 80
        [$query, $paramMap] = self::convertPositionalToNamedPlaceholders($query);

        $stmt = oci_parse($dbh, $query);
        assert(is_resource($stmt));

        $this->_sth      = $stmt;
        $this->_dbh      = $dbh;
        $this->_paramMap = $paramMap;
        $this->_conn     = $conn;
81
    }
82 83

    /**
Benjamin Morel's avatar
Benjamin Morel committed
84
     * Converts positional (?) into named placeholders (:param<num>).
85
     *
86 87 88 89 90
     * Oracle does not support positional parameters, hence this method converts all
     * positional parameters into artificially named parameters. Note that this conversion
     * is not perfect. All question marks (?) in the original statement are treated as
     * placeholders and converted to a named parameter.
     *
91 92 93 94 95
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
     * Question marks inside literal strings are therefore handled correctly by this method.
     * This comes at a cost, the whole sql statement has to be looped over.
     *
     * @param string $statement The SQL statement to convert.
Benjamin Morel's avatar
Benjamin Morel committed
96
     *
97
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
98 99 100 101 102
     *
     * @throws OCI8Exception
     *
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
103
     */
104
    public static function convertPositionalToNamedPlaceholders($statement)
105
    {
106 107
        $fragmentOffset          = $tokenOffset = 0;
        $fragments               = $paramMap = [];
Sergei Morozov's avatar
Sergei Morozov committed
108
        $currentLiteralDelimiter = null;
109 110

        do {
111
            if (! $currentLiteralDelimiter) {
Sergei Morozov's avatar
Sergei Morozov committed
112
                $result = self::findPlaceholderOrOpeningQuote(
113
                    $statement,
Sergei Morozov's avatar
Sergei Morozov committed
114 115 116 117 118 119
                    $tokenOffset,
                    $fragmentOffset,
                    $fragments,
                    $currentLiteralDelimiter,
                    $paramMap
                );
120
            } else {
Sergei Morozov's avatar
Sergei Morozov committed
121
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
122 123 124
            }
        } while ($result);

Sergei Morozov's avatar
Sergei Morozov committed
125
        if ($currentLiteralDelimiter) {
126 127
            throw new OCI8Exception(sprintf(
                'The statement contains non-terminated string literal starting at offset %d',
Sergei Morozov's avatar
Sergei Morozov committed
128
                $tokenOffset - 1
129
            ));
130
        }
131

132
        $fragments[] = substr($statement, $fragmentOffset);
133
        $statement   = implode('', $fragments);
134

135
        return [$statement, $paramMap];
136 137
    }

Sergei Morozov's avatar
Sergei Morozov committed
138 139 140
    /**
     * Finds next placeholder or opening quote.
     *
141 142 143 144 145 146 147
     * @param string             $statement               The SQL statement to parse
     * @param string             $tokenOffset             The offset to start searching from
     * @param int                $fragmentOffset          The offset to build the next fragment from
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
     *                                                    or NULL if not currently in a literal
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
148
     *
Sergei Morozov's avatar
Sergei Morozov committed
149 150 151 152 153 154 155 156 157 158 159 160
     * @return bool Whether the token was found
     */
    private static function findPlaceholderOrOpeningQuote(
        $statement,
        &$tokenOffset,
        &$fragmentOffset,
        &$fragments,
        &$currentLiteralDelimiter,
        &$paramMap
    ) {
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');

161
        if ($token === null) {
Sergei Morozov's avatar
Sergei Morozov committed
162 163 164
            return false;
        }

165
        if ($token === '?') {
166 167 168 169
            $position            = count($paramMap) + 1;
            $param               = ':param' . $position;
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
            $fragments[]         = $param;
Sergei Morozov's avatar
Sergei Morozov committed
170
            $paramMap[$position] = $param;
171 172
            $tokenOffset        += 1;
            $fragmentOffset      = $tokenOffset;
173 174

            return true;
Sergei Morozov's avatar
Sergei Morozov committed
175 176
        }

177 178 179
        $currentLiteralDelimiter = $token;
        ++$tokenOffset;

Sergei Morozov's avatar
Sergei Morozov committed
180 181 182 183 184 185
        return true;
    }

    /**
     * Finds closing quote
     *
Sergei Morozov's avatar
Sergei Morozov committed
186 187 188
     * @param string $statement               The SQL statement to parse
     * @param string $tokenOffset             The offset to start searching from
     * @param string $currentLiteralDelimiter The delimiter of the current string literal
189
     *
Sergei Morozov's avatar
Sergei Morozov committed
190 191 192 193 194 195 196
     * @return bool Whether the token was found
     */
    private static function findClosingQuote(
        $statement,
        &$tokenOffset,
        &$currentLiteralDelimiter
    ) {
197 198 199
        $token = self::findToken(
            $statement,
            $tokenOffset,
200
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
201
        );
Sergei Morozov's avatar
Sergei Morozov committed
202

203
        if ($token === null) {
Sergei Morozov's avatar
Sergei Morozov committed
204 205 206 207 208 209 210 211 212 213 214 215 216 217
            return false;
        }

        $currentLiteralDelimiter = false;
        ++$tokenOffset;

        return true;
    }

    /**
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
     * where the token was found.
     *
     * @param string $statement The SQL statement to parse
Sergei Morozov's avatar
Sergei Morozov committed
218
     * @param int    $offset    The offset to start searching from
219
     * @param string $regex     The regex containing token pattern
220
     *
Sergei Morozov's avatar
Sergei Morozov committed
221 222 223 224
     * @return string|null Token or NULL if not found
     */
    private static function findToken($statement, &$offset, $regex)
    {
225
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset) === 1) {
Sergei Morozov's avatar
Sergei Morozov committed
226
            $offset = $matches[0][1];
227

Sergei Morozov's avatar
Sergei Morozov committed
228 229 230 231 232 233
            return $matches[0][0];
        }

        return null;
    }

234 235 236
    /**
     * {@inheritdoc}
     */
237
    public function bindValue($param, $value, $type = ParameterType::STRING)
238
    {
239
        return $this->bindParam($param, $value, $type, null);
240 241 242 243 244
    }

    /**
     * {@inheritdoc}
     */
245
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
246
    {
247 248 249 250 251 252 253
        if (is_int($param)) {
            if (! isset($this->_paramMap[$param])) {
                throw new OCI8Exception(sprintf('Could not find variable mapping with index %d, in the SQL statement', $param));
            }

            $param = $this->_paramMap[$param];
        }
254

Sergei Morozov's avatar
Sergei Morozov committed
255
        if ($type === ParameterType::LARGE_OBJECT) {
256
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
Sergei Morozov's avatar
Sergei Morozov committed
257 258 259 260

            $class = 'OCI-Lob';
            assert($lob instanceof $class);

261
            $lob->writetemporary($variable, OCI_TEMP_BLOB);
262

263
            $variable =& $lob;
264
        }
Benjamin Eberlei's avatar
Benjamin Eberlei committed
265

266
        $this->boundValues[$param] =& $variable;
267

268 269
        return oci_bind_by_name(
            $this->_sth,
270
            $param,
271 272 273 274 275 276 277 278 279
            $variable,
            $length ?? -1,
            $this->convertParameterType($type)
        );
    }

    /**
     * Converts DBAL parameter type to oci8 parameter type
     */
280
    private function convertParameterType(int $type): int
281 282 283 284
    {
        switch ($type) {
            case ParameterType::BINARY:
                return OCI_B_BIN;
285

286 287
            case ParameterType::LARGE_OBJECT:
                return OCI_B_BLOB;
288

289 290 291
            default:
                return SQLT_CHR;
        }
292 293 294
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
295
     * {@inheritdoc}
296
     */
297
    public function execute($params = null): ResultInterface
298
    {
299
        if ($params !== null) {
300
            foreach ($params as $key => $val) {
301
                if (is_int($key)) {
302 303 304 305
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
306 307
            }
        }
308

309
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
310
        if (! $ret) {
311
            throw OCI8Exception::fromErrorInfo(oci_error($this->_sth));
312
        }
Benjamin Morel's avatar
Benjamin Morel committed
313

314
        return new Result($this->_sth);
315
    }
316
}