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

namespace Doctrine\DBAL\Driver\OCI8;

5
use Doctrine\DBAL\Driver\Statement;
6
use Doctrine\DBAL\Driver\StatementIterator;
7 8
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
9
use InvalidArgumentException;
10
use IteratorAggregate;
11
use function array_key_exists;
Sergei Morozov's avatar
Sergei Morozov committed
12
use function assert;
13 14
use function count;
use function implode;
Sergei Morozov's avatar
Sergei Morozov committed
15 16
use function is_int;
use function is_resource;
17 18 19 20 21 22 23 24 25 26 27 28 29 30
use function oci_bind_by_name;
use function oci_cancel;
use function oci_error;
use function oci_execute;
use function oci_fetch_all;
use function oci_fetch_array;
use function oci_new_descriptor;
use function oci_num_fields;
use function oci_num_rows;
use function oci_parse;
use function preg_match;
use function preg_quote;
use function sprintf;
use function substr;
Grégoire Paris's avatar
Grégoire Paris committed
31 32 33 34 35 36 37 38 39 40 41 42 43
use const OCI_ASSOC;
use const OCI_B_BIN;
use const OCI_B_BLOB;
use const OCI_BOTH;
use const OCI_D_LOB;
use const OCI_FETCHSTATEMENT_BY_COLUMN;
use const OCI_FETCHSTATEMENT_BY_ROW;
use const OCI_NUM;
use const OCI_RETURN_LOBS;
use const OCI_RETURN_NULLS;
use const OCI_TEMP_BLOB;
use const PREG_OFFSET_CAPTURE;
use const SQLT_CHR;
44 45 46 47

/**
 * The OCI8 implementation of the Statement interface.
 */
48
class OCI8Statement implements IteratorAggregate, Statement
49
{
50
    /** @var resource */
51
    protected $_dbh;
Benjamin Morel's avatar
Benjamin Morel committed
52

53
    /** @var resource */
54
    protected $_sth;
Benjamin Morel's avatar
Benjamin Morel committed
55

56
    /** @var OCI8Connection */
57
    protected $_conn;
Benjamin Morel's avatar
Benjamin Morel committed
58

59 60 61 62 63
    /**
     * @deprecated
     *
     * @var string
     */
64
    protected static $_PARAM = ':param';
Benjamin Morel's avatar
Benjamin Morel committed
65

66
    /** @var int[] */
67
    protected static $fetchModeMap = [
Sergei Morozov's avatar
Sergei Morozov committed
68
        FetchMode::MIXED       => OCI_BOTH,
69
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
Sergei Morozov's avatar
Sergei Morozov committed
70 71
        FetchMode::NUMERIC     => OCI_NUM,
        FetchMode::COLUMN      => OCI_NUM,
72
    ];
Benjamin Morel's avatar
Benjamin Morel committed
73

74
    /** @var int */
75
    protected $_defaultFetchMode = FetchMode::MIXED;
Benjamin Morel's avatar
Benjamin Morel committed
76

77
    /** @var string[] */
78
    protected $_paramMap = [];
79

80 81 82 83 84
    /**
     * Holds references to bound parameter values.
     *
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
     *
85
     * @var mixed[]
86
     */
87
    private $boundValues = [];
88

89 90 91 92 93 94 95
    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

96 97 98
    /**
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
     *
Sergei Morozov's avatar
Sergei Morozov committed
99 100
     * @param resource $dbh   The connection handle.
     * @param string   $query The SQL query.
101
     */
Sergei Morozov's avatar
Sergei Morozov committed
102
    public function __construct($dbh, $query, OCI8Connection $conn)
103
    {
Sergei Morozov's avatar
Sergei Morozov committed
104 105 106 107 108 109 110 111 112
        [$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;
113
    }
114 115

    /**
Benjamin Morel's avatar
Benjamin Morel committed
116
     * Converts positional (?) into named placeholders (:param<num>).
117
     *
118 119 120 121 122
     * 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.
     *
123 124 125 126 127
     * 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
128
     *
129
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
130 131 132 133 134
     *
     * @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.
135
     */
136
    public static function convertPositionalToNamedPlaceholders($statement)
137
    {
138 139
        $fragmentOffset          = $tokenOffset = 0;
        $fragments               = $paramMap = [];
Sergei Morozov's avatar
Sergei Morozov committed
140
        $currentLiteralDelimiter = null;
141 142

        do {
143
            if (! $currentLiteralDelimiter) {
Sergei Morozov's avatar
Sergei Morozov committed
144
                $result = self::findPlaceholderOrOpeningQuote(
145
                    $statement,
Sergei Morozov's avatar
Sergei Morozov committed
146 147 148 149 150 151
                    $tokenOffset,
                    $fragmentOffset,
                    $fragments,
                    $currentLiteralDelimiter,
                    $paramMap
                );
152
            } else {
Sergei Morozov's avatar
Sergei Morozov committed
153
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
154 155 156
            }
        } while ($result);

Sergei Morozov's avatar
Sergei Morozov committed
157
        if ($currentLiteralDelimiter) {
158 159
            throw new OCI8Exception(sprintf(
                'The statement contains non-terminated string literal starting at offset %d',
Sergei Morozov's avatar
Sergei Morozov committed
160
                $tokenOffset - 1
161
            ));
162
        }
163

164
        $fragments[] = substr($statement, $fragmentOffset);
165
        $statement   = implode('', $fragments);
166

167
        return [$statement, $paramMap];
168 169
    }

Sergei Morozov's avatar
Sergei Morozov committed
170 171 172
    /**
     * Finds next placeholder or opening quote.
     *
173 174 175 176 177 178 179
     * @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
180
     *
Sergei Morozov's avatar
Sergei Morozov committed
181 182 183 184 185 186 187 188 189 190 191 192
     * @return bool Whether the token was found
     */
    private static function findPlaceholderOrOpeningQuote(
        $statement,
        &$tokenOffset,
        &$fragmentOffset,
        &$fragments,
        &$currentLiteralDelimiter,
        &$paramMap
    ) {
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');

193
        if ($token === null) {
Sergei Morozov's avatar
Sergei Morozov committed
194 195 196
            return false;
        }

197
        if ($token === '?') {
198 199 200 201
            $position            = count($paramMap) + 1;
            $param               = ':param' . $position;
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
            $fragments[]         = $param;
Sergei Morozov's avatar
Sergei Morozov committed
202
            $paramMap[$position] = $param;
203 204
            $tokenOffset        += 1;
            $fragmentOffset      = $tokenOffset;
205 206

            return true;
Sergei Morozov's avatar
Sergei Morozov committed
207 208
        }

209 210 211
        $currentLiteralDelimiter = $token;
        ++$tokenOffset;

Sergei Morozov's avatar
Sergei Morozov committed
212 213 214 215 216 217
        return true;
    }

    /**
     * Finds closing quote
     *
Sergei Morozov's avatar
Sergei Morozov committed
218 219 220
     * @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
221
     *
Sergei Morozov's avatar
Sergei Morozov committed
222 223 224 225 226 227 228
     * @return bool Whether the token was found
     */
    private static function findClosingQuote(
        $statement,
        &$tokenOffset,
        &$currentLiteralDelimiter
    ) {
229 230 231
        $token = self::findToken(
            $statement,
            $tokenOffset,
232
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
233
        );
Sergei Morozov's avatar
Sergei Morozov committed
234

235
        if ($token === null) {
Sergei Morozov's avatar
Sergei Morozov committed
236 237 238 239 240 241 242 243 244 245 246 247 248 249
            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
250
     * @param int    $offset    The offset to start searching from
251
     * @param string $regex     The regex containing token pattern
252
     *
Sergei Morozov's avatar
Sergei Morozov committed
253 254 255 256
     * @return string|null Token or NULL if not found
     */
    private static function findToken($statement, &$offset, $regex)
    {
257
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset) === 1) {
Sergei Morozov's avatar
Sergei Morozov committed
258
            $offset = $matches[0][1];
259

Sergei Morozov's avatar
Sergei Morozov committed
260 261 262 263 264 265
            return $matches[0][0];
        }

        return null;
    }

266 267 268
    /**
     * {@inheritdoc}
     */
269
    public function bindValue($param, $value, $type = ParameterType::STRING)
270
    {
271
        return $this->bindParam($param, $value, $type, null);
272 273 274 275 276
    }

    /**
     * {@inheritdoc}
     */
277
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
278
    {
279 280 281 282 283 284 285
        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];
        }
286

Sergei Morozov's avatar
Sergei Morozov committed
287
        if ($type === ParameterType::LARGE_OBJECT) {
288
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
Sergei Morozov's avatar
Sergei Morozov committed
289 290 291 292

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

293
            $lob->writetemporary($variable, OCI_TEMP_BLOB);
294

295
            $variable =& $lob;
296
        }
Benjamin Eberlei's avatar
Benjamin Eberlei committed
297

298
        $this->boundValues[$param] =& $variable;
299

300 301
        return oci_bind_by_name(
            $this->_sth,
302
            $param,
303 304 305 306 307 308 309 310 311 312 313 314 315 316
            $variable,
            $length ?? -1,
            $this->convertParameterType($type)
        );
    }

    /**
     * Converts DBAL parameter type to oci8 parameter type
     */
    private function convertParameterType(int $type) : int
    {
        switch ($type) {
            case ParameterType::BINARY:
                return OCI_B_BIN;
317

318 319
            case ParameterType::LARGE_OBJECT:
                return OCI_B_BLOB;
320

321 322 323
            default:
                return SQLT_CHR;
        }
324 325 326
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
327
     * {@inheritdoc}
328 329 330
     */
    public function closeCursor()
    {
331
        // not having the result means there's nothing to close
332
        if (! $this->result) {
333 334 335
            return true;
        }

336
        oci_cancel($this->_sth);
337

338 339
        $this->result = false;

340
        return true;
341 342
    }

343
    /**
344 345 346 347
     * {@inheritdoc}
     */
    public function columnCount()
    {
348 349 350 351 352 353 354
        $count = oci_num_fields($this->_sth);

        if ($count !== false) {
            return $count;
        }

        return 0;
355 356 357 358 359 360 361 362 363 364 365
    }

    /**
     * {@inheritdoc}
     */
    public function errorCode()
    {
        $error = oci_error($this->_sth);
        if ($error !== false) {
            $error = $error['code'];
        }
Benjamin Morel's avatar
Benjamin Morel committed
366

367 368
        return $error;
    }
369

370 371 372 373 374
    /**
     * {@inheritdoc}
     */
    public function errorInfo()
    {
Sergei Morozov's avatar
Sergei Morozov committed
375 376 377 378 379 380 381
        $error = oci_error($this->_sth);

        if ($error === false) {
            return [];
        }

        return $error;
382 383 384 385 386
    }

    /**
     * {@inheritdoc}
     */
387
    public function execute($params = null)
388
    {
389
        if ($params !== null) {
390
            $hasZeroIndex = array_key_exists(0, $params);
391
            foreach ($params as $key => $val) {
Sergei Morozov's avatar
Sergei Morozov committed
392
                if ($hasZeroIndex && is_int($key)) {
393 394 395 396
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
397 398
            }
        }
399

400
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
401
        if (! $ret) {
402 403
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
        }
Benjamin Morel's avatar
Benjamin Morel committed
404

405 406
        $this->result = true;

407
        return $ret;
408 409
    }

410 411 412
    /**
     * {@inheritdoc}
     */
413
    public function setFetchMode($fetchMode)
414
    {
415
        $this->_defaultFetchMode = $fetchMode;
Benjamin Morel's avatar
Benjamin Morel committed
416 417

        return true;
418 419 420 421 422 423 424
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
425
        return new StatementIterator($this);
426 427
    }

428 429 430
    /**
     * {@inheritdoc}
     */
431
    public function fetch($fetchMode = null)
432
    {
433 434
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
435
        if (! $this->result) {
436 437 438
            return false;
        }

439
        $fetchMode = $fetchMode ?? $this->_defaultFetchMode;
440

441
        if ($fetchMode === FetchMode::COLUMN) {
442 443 444
            return $this->fetchColumn();
        }

445
        if (! isset(self::$fetchModeMap[$fetchMode])) {
446
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
447
        }
448

449 450 451 452
        return oci_fetch_array(
            $this->_sth,
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
        );
453 454 455 456 457
    }

    /**
     * {@inheritdoc}
     */
458
    public function fetchAll($fetchMode = null)
459
    {
460
        $fetchMode = $fetchMode ?? $this->_defaultFetchMode;
461

462
        $result = [];
463

464 465
        if (! isset(self::$fetchModeMap[$fetchMode])) {
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
466
        }
467

468 469
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
            while ($row = $this->fetch($fetchMode)) {
470 471 472
                $result[] = $row;
            }
        } else {
473
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
474

Sergei Morozov's avatar
Sergei Morozov committed
475
            if ($fetchMode === FetchMode::COLUMN) {
476 477
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
            }
478

479 480
            // do not try fetching from the statement if it's not expected to contain result
            // in order to prevent exceptional situation
481
            if (! $this->result) {
482
                return [];
483 484
            }

485 486 487 488 489 490 491
            oci_fetch_all(
                $this->_sth,
                $result,
                0,
                -1,
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
            );
492

Sergei Morozov's avatar
Sergei Morozov committed
493
            if ($fetchMode === FetchMode::COLUMN) {
494 495
                $result = $result[0];
            }
496
        }
497

498 499 500 501 502 503
        return $result;
    }

    /**
     * {@inheritdoc}
     */
504
    public function fetchColumn()
505
    {
506 507
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
508
        if (! $this->result) {
509 510 511
            return false;
        }

512
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
Benjamin Morel's avatar
Benjamin Morel committed
513

514
        if ($row === false) {
515 516 517
            return false;
        }

518
        return $row[0] ?? null;
519 520
    }

521
    public function rowCount() : int
522
    {
523 524 525 526 527 528 529
        $count = oci_num_rows($this->_sth);

        if ($count !== false) {
            return $count;
        }

        return 0;
530
    }
531
}