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

namespace Doctrine\DBAL\Driver\OCI8;

5
use Doctrine\DBAL\Driver\FetchUtils;
6
use Doctrine\DBAL\Driver\Statement;
7
use Doctrine\DBAL\Driver\StatementIterator;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\ForwardCompatibility\Driver\ResultStatement as ForwardCompatibleResultStatement;
10
use Doctrine\DBAL\ParameterType;
11
use InvalidArgumentException;
12
use IteratorAggregate;
13
use PDO;
14
use function array_key_exists;
Sergei Morozov's avatar
Sergei Morozov committed
15
use function assert;
16 17
use function count;
use function implode;
Sergei Morozov's avatar
Sergei Morozov committed
18 19
use function is_int;
use function is_resource;
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
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_fetch_object;
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
35 36 37 38 39 40 41 42 43 44 45 46 47
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;
48 49 50 51

/**
 * The OCI8 implementation of the Statement interface.
 */
52
class OCI8Statement implements IteratorAggregate, Statement, ForwardCompatibleResultStatement
53
{
54
    /** @var resource */
55
    protected $_dbh;
Benjamin Morel's avatar
Benjamin Morel committed
56

57
    /** @var resource */
58
    protected $_sth;
Benjamin Morel's avatar
Benjamin Morel committed
59

60
    /** @var OCI8Connection */
61
    protected $_conn;
Benjamin Morel's avatar
Benjamin Morel committed
62

63 64 65 66 67
    /**
     * @deprecated
     *
     * @var string
     */
68
    protected static $_PARAM = ':param';
Benjamin Morel's avatar
Benjamin Morel committed
69

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

78
    /** @var int */
79
    protected $_defaultFetchMode = FetchMode::MIXED;
Benjamin Morel's avatar
Benjamin Morel committed
80

81
    /** @var string[] */
82
    protected $_paramMap = [];
83

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

93 94 95 96 97 98 99
    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

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

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

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

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

168
        $fragments[] = substr($statement, $fragmentOffset);
169
        $statement   = implode('', $fragments);
170

171
        return [$statement, $paramMap];
172 173
    }

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

197
        if (! $token) {
Sergei Morozov's avatar
Sergei Morozov committed
198 199 200
            return false;
        }

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

            return true;
Sergei Morozov's avatar
Sergei Morozov committed
211 212
        }

213 214 215
        $currentLiteralDelimiter = $token;
        ++$tokenOffset;

Sergei Morozov's avatar
Sergei Morozov committed
216 217 218 219 220 221
        return true;
    }

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

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

Sergei Morozov's avatar
Sergei Morozov committed
264 265 266 267 268 269
            return $matches[0][0];
        }

        return null;
    }

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

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

Sergei Morozov's avatar
Sergei Morozov committed
291
        if ($type === ParameterType::LARGE_OBJECT) {
292
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
Sergei Morozov's avatar
Sergei Morozov committed
293 294 295 296

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

297 298
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);

299
            $variable =& $lob;
300
        }
Benjamin Eberlei's avatar
Benjamin Eberlei committed
301

302
        $this->boundValues[$param] =& $variable;
303

304 305
        return oci_bind_by_name(
            $this->_sth,
306
            $param,
307 308 309 310 311 312 313 314 315 316 317 318 319 320
            $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;
321

322 323
            case ParameterType::LARGE_OBJECT:
                return OCI_B_BLOB;
324

325 326 327
            default:
                return SQLT_CHR;
        }
328 329 330
    }

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

340
        oci_cancel($this->_sth);
341

342 343
        $this->result = false;

344
        return true;
345 346
    }

347
    /**
348 349 350 351
     * {@inheritdoc}
     */
    public function columnCount()
    {
Sergei Morozov's avatar
Sergei Morozov committed
352
        return oci_num_fields($this->_sth) ?: 0;
353 354 355 356
    }

    /**
     * {@inheritdoc}
357 358
     *
     * @deprecated The error information is available via exceptions.
359 360 361 362 363 364 365
     */
    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
    /**
     * {@inheritdoc}
372 373
     *
     * @deprecated The error information is available via exceptions.
374 375 376
     */
    public function errorInfo()
    {
Sergei Morozov's avatar
Sergei Morozov committed
377 378 379 380 381 382 383
        $error = oci_error($this->_sth);

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

        return $error;
384 385 386 387 388
    }

    /**
     * {@inheritdoc}
     */
389
    public function execute($params = null)
390
    {
391
        if ($params) {
392
            $hasZeroIndex = array_key_exists(0, $params);
Sergei Morozov's avatar
Sergei Morozov committed
393

394
            foreach ($params as $key => $val) {
Sergei Morozov's avatar
Sergei Morozov committed
395
                if ($hasZeroIndex && is_int($key)) {
396 397 398 399
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
400 401
            }
        }
402

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

408 409
        $this->result = true;

410
        return $ret;
411 412
    }

413 414
    /**
     * {@inheritdoc}
415 416
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
417
     */
418
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
419
    {
420
        $this->_defaultFetchMode = $fetchMode;
Benjamin Morel's avatar
Benjamin Morel committed
421 422

        return true;
423 424 425 426
    }

    /**
     * {@inheritdoc}
427 428
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
429 430 431
     */
    public function getIterator()
    {
432
        return new StatementIterator($this);
433 434
    }

435 436
    /**
     * {@inheritdoc}
437 438
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
439
     */
440
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
441
    {
442 443
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
444
        if (! $this->result) {
445 446 447
            return false;
        }

448
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
449

450
        if ($fetchMode === FetchMode::COLUMN) {
451 452 453
            return $this->fetchColumn();
        }

454
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
455 456 457 458
            return oci_fetch_object($this->_sth);
        }

        if (! isset(self::$fetchModeMap[$fetchMode])) {
459
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
460
        }
461

462 463 464 465
        return oci_fetch_array(
            $this->_sth,
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
        );
466 467 468 469
    }

    /**
     * {@inheritdoc}
470 471
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchColumn() instead.
472
     */
473
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
474
    {
475
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
476

477
        $result = [];
478

Sergei Morozov's avatar
Sergei Morozov committed
479
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
480 481 482 483 484 485 486
            while ($row = $this->fetch($fetchMode)) {
                $result[] = $row;
            }

            return $result;
        }

487 488
        if (! isset(self::$fetchModeMap[$fetchMode])) {
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
489
        }
490

491 492
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
            while ($row = $this->fetch($fetchMode)) {
493 494 495
                $result[] = $row;
            }
        } else {
496
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
497

Sergei Morozov's avatar
Sergei Morozov committed
498
            if ($fetchMode === FetchMode::COLUMN) {
499 500
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
            }
501

502 503
            // do not try fetching from the statement if it's not expected to contain result
            // in order to prevent exceptional situation
504
            if (! $this->result) {
505
                return [];
506 507
            }

508 509 510 511 512 513 514
            oci_fetch_all(
                $this->_sth,
                $result,
                0,
                -1,
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
            );
515

Sergei Morozov's avatar
Sergei Morozov committed
516
            if ($fetchMode === FetchMode::COLUMN) {
517 518
                $result = $result[0];
            }
519
        }
520

521 522 523 524 525
        return $result;
    }

    /**
     * {@inheritdoc}
526 527
     *
     * @deprecated Use fetchOne() instead.
528 529 530
     */
    public function fetchColumn($columnIndex = 0)
    {
531 532
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
533
        if (! $this->result) {
534 535 536
            return false;
        }

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

539
        if ($row === false) {
540 541 542
            return false;
        }

543
        return $row[$columnIndex] ?? null;
544 545 546 547 548 549 550
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
Sergei Morozov's avatar
Sergei Morozov committed
551
        return oci_num_rows($this->_sth) ?: 0;
552
    }
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631

    /**
     * {@inheritdoc}
     */
    public function fetchNumeric()
    {
        return $this->doFetch(OCI_NUM);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        return $this->doFetch(OCI_ASSOC);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric() : array
    {
        return $this->doFetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_ROW);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative() : array
    {
        return $this->doFetchAll(OCI_ASSOC, OCI_FETCHSTATEMENT_BY_ROW);
    }

    /**
     * @return mixed|false
     */
    private function doFetch(int $mode)
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        return oci_fetch_array(
            $this->_sth,
            $mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS
        );
    }

    /**
     * @return array<mixed>
     */
    private function doFetchAll(int $mode, int $fetchStructure) : array
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return [];
        }

        oci_fetch_all(
            $this->_sth,
            $result,
            0,
            -1,
            $mode | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
        );

        return $result;
    }
632
}