OCI8Statement.php 15.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
Benjamin Eberlei's avatar
Benjamin Eberlei committed
16
 * and is licensed under the MIT license. For more information, see
17 18 19 20 21
 * <http://www.doctrine-project.org>.
 */

namespace Doctrine\DBAL\Driver\OCI8;

22
use Doctrine\DBAL\Driver\Statement;
23
use Doctrine\DBAL\Driver\StatementIterator;
24 25
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
26
use IteratorAggregate;
27
use const OCI_ASSOC;
28
use const OCI_B_BIN;
29 30 31 32 33 34 35 36 37 38
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;
39
use const SQLT_CHR;
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
use function array_key_exists;
use function count;
use function implode;
use function is_numeric;
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;
59 60 61 62 63 64 65

/**
 * The OCI8 implementation of the Statement interface.
 *
 * @since 2.0
 * @author Roman Borschel <roman@code-factory.org>
 */
66
class OCI8Statement implements IteratorAggregate, Statement
67
{
Benjamin Morel's avatar
Benjamin Morel committed
68 69 70
    /**
     * @var resource
     */
71
    protected $_dbh;
Benjamin Morel's avatar
Benjamin Morel committed
72 73 74 75

    /**
     * @var resource
     */
76
    protected $_sth;
Benjamin Morel's avatar
Benjamin Morel committed
77 78 79 80

    /**
     * @var \Doctrine\DBAL\Driver\OCI8\OCI8Connection
     */
81
    protected $_conn;
Benjamin Morel's avatar
Benjamin Morel committed
82 83 84 85

    /**
     * @var string
     */
86
    protected static $_PARAM = ':param';
Benjamin Morel's avatar
Benjamin Morel committed
87 88 89 90

    /**
     * @var array
     */
91
    protected static $fetchModeMap = [
Sergei Morozov's avatar
Sergei Morozov committed
92
        FetchMode::MIXED       => OCI_BOTH,
93
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
Sergei Morozov's avatar
Sergei Morozov committed
94 95
        FetchMode::NUMERIC     => OCI_NUM,
        FetchMode::COLUMN      => OCI_NUM,
96
    ];
Benjamin Morel's avatar
Benjamin Morel committed
97 98

    /**
99
     * @var int
Benjamin Morel's avatar
Benjamin Morel committed
100
     */
101
    protected $_defaultFetchMode = FetchMode::MIXED;
Benjamin Morel's avatar
Benjamin Morel committed
102 103 104 105

    /**
     * @var array
     */
106
    protected $_paramMap = [];
107

108 109 110 111 112 113 114
    /**
     * Holds references to bound parameter values.
     *
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
     *
     * @var array
     */
115
    private $boundValues = [];
116

117 118 119 120 121 122 123
    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

124 125 126
    /**
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
     *
Benjamin Morel's avatar
Benjamin Morel committed
127 128 129
     * @param resource                                  $dbh       The connection handle.
     * @param string                                    $statement The SQL statement.
     * @param \Doctrine\DBAL\Driver\OCI8\OCI8Connection $conn
130
     */
131
    public function __construct($dbh, $statement, OCI8Connection $conn)
132
    {
133 134
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
        $this->_sth = oci_parse($dbh, $statement);
135
        $this->_dbh = $dbh;
136
        $this->_paramMap = $paramMap;
137
        $this->_conn = $conn;
138
    }
139 140

    /**
Benjamin Morel's avatar
Benjamin Morel committed
141
     * Converts positional (?) into named placeholders (:param<num>).
142
     *
143 144 145 146 147
     * 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.
     *
148 149 150 151 152
     * 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.
     *
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
153
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
Benjamin Morel's avatar
Benjamin Morel committed
154
     *
155
     * @param string $statement The SQL statement to convert.
Benjamin Morel's avatar
Benjamin Morel committed
156
     *
157
     * @return array [0] => the statement value (string), [1] => the paramMap value (array).
158
     * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
159
     */
160
    public static function convertPositionalToNamedPlaceholders($statement)
161
    {
Sergei Morozov's avatar
Sergei Morozov committed
162
        $fragmentOffset = $tokenOffset = 0;
163
        $fragments = $paramMap = [];
Sergei Morozov's avatar
Sergei Morozov committed
164
        $currentLiteralDelimiter = null;
165 166

        do {
Sergei Morozov's avatar
Sergei Morozov committed
167 168
            if (!$currentLiteralDelimiter) {
                $result = self::findPlaceholderOrOpeningQuote(
169
                    $statement,
Sergei Morozov's avatar
Sergei Morozov committed
170 171 172 173 174 175
                    $tokenOffset,
                    $fragmentOffset,
                    $fragments,
                    $currentLiteralDelimiter,
                    $paramMap
                );
176
            } else {
Sergei Morozov's avatar
Sergei Morozov committed
177
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
178 179 180
            }
        } while ($result);

Sergei Morozov's avatar
Sergei Morozov committed
181
        if ($currentLiteralDelimiter) {
182 183
            throw new OCI8Exception(sprintf(
                'The statement contains non-terminated string literal starting at offset %d',
Sergei Morozov's avatar
Sergei Morozov committed
184
                $tokenOffset - 1
185
            ));
186
        }
187

188
        $fragments[] = substr($statement, $fragmentOffset);
189 190
        $statement = implode('', $fragments);

191
        return [$statement, $paramMap];
192 193
    }

Sergei Morozov's avatar
Sergei Morozov committed
194 195 196
    /**
     * Finds next placeholder or opening quote.
     *
197 198 199 200 201 202 203
     * @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
Sergei Morozov's avatar
Sergei Morozov committed
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
     * @return bool Whether the token was found
     */
    private static function findPlaceholderOrOpeningQuote(
        $statement,
        &$tokenOffset,
        &$fragmentOffset,
        &$fragments,
        &$currentLiteralDelimiter,
        &$paramMap
    ) {
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');

        if (!$token) {
            return false;
        }

220
        if ($token === '?') {
Sergei Morozov's avatar
Sergei Morozov committed
221 222 223 224 225
            $position = count($paramMap) + 1;
            $param = ':param' . $position;
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
            $fragments[] = $param;
            $paramMap[$position] = $param;
226 227 228 229
            $tokenOffset += 1;
            $fragmentOffset = $tokenOffset;

            return true;
Sergei Morozov's avatar
Sergei Morozov committed
230 231
        }

232 233 234
        $currentLiteralDelimiter = $token;
        ++$tokenOffset;

Sergei Morozov's avatar
Sergei Morozov committed
235 236 237 238 239 240
        return true;
    }

    /**
     * Finds closing quote
     *
241 242
     * @param string      $statement               The SQL statement to parse
     * @param string      $tokenOffset             The offset to start searching from
243 244
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
     *                                             or NULL if not currently in a literal
Sergei Morozov's avatar
Sergei Morozov committed
245 246 247 248 249 250 251
     * @return bool Whether the token was found
     */
    private static function findClosingQuote(
        $statement,
        &$tokenOffset,
        &$currentLiteralDelimiter
    ) {
252 253 254
        $token = self::findToken(
            $statement,
            $tokenOffset,
255
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
256
        );
Sergei Morozov's avatar
Sergei Morozov committed
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272

        if (!$token) {
            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
273 274
     * @param string $offset    The offset to start searching from
     * @param string $regex     The regex containing token pattern
Sergei Morozov's avatar
Sergei Morozov committed
275 276 277 278 279 280 281 282 283 284 285 286
     * @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];
            return $matches[0][0];
        }

        return null;
    }

287 288 289
    /**
     * {@inheritdoc}
     */
290
    public function bindValue($param, $value, $type = ParameterType::STRING)
291
    {
292
        return $this->bindParam($param, $value, $type, null);
293 294 295 296 297
    }

    /**
     * {@inheritdoc}
     */
298
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
299
    {
300
        $column = $this->_paramMap[$column] ?? $column;
301

Sergei Morozov's avatar
Sergei Morozov committed
302
        if ($type === ParameterType::LARGE_OBJECT) {
303 304 305
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);

306
            $variable =& $lob;
307
        }
Benjamin Eberlei's avatar
Benjamin Eberlei committed
308

309 310
        $this->boundValues[$column] =& $variable;

311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
        return oci_bind_by_name(
            $this->_sth,
            $column,
            $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;

            case ParameterType::LARGE_OBJECT:
                return OCI_B_BLOB;

            default:
                return SQLT_CHR;
        }
335 336 337
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
338
     * {@inheritdoc}
339 340 341
     */
    public function closeCursor()
    {
342 343 344 345 346
        // not having the result means there's nothing to close
        if (!$this->result) {
            return true;
        }

347
        oci_cancel($this->_sth);
348

349 350
        $this->result = false;

351
        return true;
352 353
    }

354
    /**
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return oci_num_fields($this->_sth);
    }

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

372 373
        return $error;
    }
374

375 376 377 378 379 380 381 382 383 384 385
    /**
     * {@inheritdoc}
     */
    public function errorInfo()
    {
        return oci_error($this->_sth);
    }

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

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

404 405
        $this->result = true;

406
        return $ret;
407 408
    }

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

        return true;
417 418 419 420 421 422 423
    }

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

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

438
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
439

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

444
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
445 446 447 448
            return oci_fetch_object($this->_sth);
        }

        if (! isset(self::$fetchModeMap[$fetchMode])) {
449
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
450
        }
451

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

    /**
     * {@inheritdoc}
     */
461
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
462
    {
463
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
464

465
        $result = [];
466

Sergei Morozov's avatar
Sergei Morozov committed
467
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
468 469 470 471 472 473 474
            while ($row = $this->fetch($fetchMode)) {
                $result[] = $row;
            }

            return $result;
        }

475 476
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
477
        }
478

479 480
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
            while ($row = $this->fetch($fetchMode)) {
481 482 483
                $result[] = $row;
            }
        } else {
484
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
485

Sergei Morozov's avatar
Sergei Morozov committed
486
            if ($fetchMode === FetchMode::COLUMN) {
487 488
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
            }
489

490 491 492
            // do not try fetching from the statement if it's not expected to contain result
            // in order to prevent exceptional situation
            if (!$this->result) {
493
                return [];
494 495
            }

496
            oci_fetch_all($this->_sth, $result, 0, -1,
497
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
498

Sergei Morozov's avatar
Sergei Morozov committed
499
            if ($fetchMode === FetchMode::COLUMN) {
500 501
                $result = $result[0];
            }
502
        }
503

504 505 506 507 508 509 510 511
        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchColumn($columnIndex = 0)
    {
512 513 514 515 516 517
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
        if (!$this->result) {
            return false;
        }

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

520 521 522 523
        if (false === $row) {
            return false;
        }

524
        return $row[$columnIndex] ?? null;
525 526 527 528 529 530 531 532
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        return oci_num_rows($this->_sth);
533
    }
534
}