OCI8Statement.php 13.7 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 PDO;
24 25 26 27 28 29 30

/**
 * The OCI8 implementation of the Statement interface.
 *
 * @since 2.0
 * @author Roman Borschel <roman@code-factory.org>
 */
31
class OCI8Statement implements \IteratorAggregate, Statement
32
{
Benjamin Morel's avatar
Benjamin Morel committed
33 34 35
    /**
     * @var resource
     */
36
    protected $_dbh;
Benjamin Morel's avatar
Benjamin Morel committed
37 38 39 40

    /**
     * @var resource
     */
41
    protected $_sth;
Benjamin Morel's avatar
Benjamin Morel committed
42 43 44 45

    /**
     * @var \Doctrine\DBAL\Driver\OCI8\OCI8Connection
     */
46
    protected $_conn;
Benjamin Morel's avatar
Benjamin Morel committed
47 48 49 50

    /**
     * @var string
     */
51
    protected static $_PARAM = ':param';
Benjamin Morel's avatar
Benjamin Morel committed
52 53 54 55

    /**
     * @var array
     */
56
    protected static $fetchModeMap = [
57 58
        PDO::FETCH_BOTH => OCI_BOTH,
        PDO::FETCH_ASSOC => OCI_ASSOC,
59
        PDO::FETCH_NUM => OCI_NUM,
60
        PDO::FETCH_COLUMN => OCI_NUM,
61
    ];
Benjamin Morel's avatar
Benjamin Morel committed
62 63 64 65

    /**
     * @var integer
     */
66
    protected $_defaultFetchMode = PDO::FETCH_BOTH;
Benjamin Morel's avatar
Benjamin Morel committed
67 68 69 70

    /**
     * @var array
     */
71
    protected $_paramMap = [];
72

73 74 75 76 77 78 79
    /**
     * 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
     */
80
    private $boundValues = [];
81

82 83 84 85 86 87 88
    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

89 90 91
    /**
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
     *
Benjamin Morel's avatar
Benjamin Morel committed
92 93 94
     * @param resource                                  $dbh       The connection handle.
     * @param string                                    $statement The SQL statement.
     * @param \Doctrine\DBAL\Driver\OCI8\OCI8Connection $conn
95
     */
96
    public function __construct($dbh, $statement, OCI8Connection $conn)
97
    {
98 99
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
        $this->_sth = oci_parse($dbh, $statement);
100
        $this->_dbh = $dbh;
101
        $this->_paramMap = $paramMap;
102
        $this->_conn = $conn;
103
    }
104 105

    /**
Benjamin Morel's avatar
Benjamin Morel committed
106
     * Converts positional (?) into named placeholders (:param<num>).
107
     *
108 109 110 111 112
     * 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.
     *
113 114 115 116 117
     * 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
118
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
Benjamin Morel's avatar
Benjamin Morel committed
119
     *
120
     * @param string $statement The SQL statement to convert.
Benjamin Morel's avatar
Benjamin Morel committed
121
     *
122
     * @return string
123
     * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
124
     */
125
    public static function convertPositionalToNamedPlaceholders($statement)
126
    {
Sergei Morozov's avatar
Sergei Morozov committed
127
        $fragmentOffset = $tokenOffset = 0;
128
        $fragments = $paramMap = [];
Sergei Morozov's avatar
Sergei Morozov committed
129
        $currentLiteralDelimiter = null;
130 131

        do {
Sergei Morozov's avatar
Sergei Morozov committed
132 133
            if (!$currentLiteralDelimiter) {
                $result = self::findPlaceholderOrOpeningQuote(
134
                    $statement,
Sergei Morozov's avatar
Sergei Morozov committed
135 136 137 138 139 140
                    $tokenOffset,
                    $fragmentOffset,
                    $fragments,
                    $currentLiteralDelimiter,
                    $paramMap
                );
141
            } else {
Sergei Morozov's avatar
Sergei Morozov committed
142
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
143 144 145
            }
        } while ($result);

Sergei Morozov's avatar
Sergei Morozov committed
146
        if ($currentLiteralDelimiter) {
147 148
            throw new OCI8Exception(sprintf(
                'The statement contains non-terminated string literal starting at offset %d',
Sergei Morozov's avatar
Sergei Morozov committed
149
                $tokenOffset - 1
150
            ));
151
        }
152

153
        $fragments[] = substr($statement, $fragmentOffset);
154 155
        $statement = implode('', $fragments);

156
        return [$statement, $paramMap];
157 158
    }

Sergei Morozov's avatar
Sergei Morozov committed
159 160 161 162 163 164
    /**
     * Finds next placeholder or opening quote.
     *
     * @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
165 166 167 168
     * @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
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
     * @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;
        }

185
        if ($token === '?') {
Sergei Morozov's avatar
Sergei Morozov committed
186 187 188 189 190
            $position = count($paramMap) + 1;
            $param = ':param' . $position;
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
            $fragments[] = $param;
            $paramMap[$position] = $param;
191 192 193 194
            $tokenOffset += 1;
            $fragmentOffset = $tokenOffset;

            return true;
Sergei Morozov's avatar
Sergei Morozov committed
195 196
        }

197 198 199
        $currentLiteralDelimiter = $token;
        ++$tokenOffset;

Sergei Morozov's avatar
Sergei Morozov committed
200 201 202 203 204 205 206 207
        return true;
    }

    /**
     * Finds closing quote
     *
     * @param string $statement The SQL statement to parse
     * @param string $tokenOffset The offset to start searching from
208 209
     * @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
210 211 212 213 214 215 216
     * @return bool Whether the token was found
     */
    private static function findClosingQuote(
        $statement,
        &$tokenOffset,
        &$currentLiteralDelimiter
    ) {
217 218 219
        $token = self::findToken(
            $statement,
            $tokenOffset,
220
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
221
        );
Sergei Morozov's avatar
Sergei Morozov committed
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251

        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
     * @param string $offset The offset to start searching from
     * @param string $regex The regex containing token pattern
     * @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;
    }

252 253 254 255 256
    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = null)
    {
257
        return $this->bindParam($param, $value, $type, null);
258 259 260 261 262
    }

    /**
     * {@inheritdoc}
     */
Benjamin Eberlei's avatar
Benjamin Eberlei committed
263
    public function bindParam($column, &$variable, $type = null, $length = null)
264 265
    {
        $column = isset($this->_paramMap[$column]) ? $this->_paramMap[$column] : $column;
266 267 268 269 270

        if ($type == \PDO::PARAM_LOB) {
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);

271 272
            $this->boundValues[$column] =& $lob;

273
            return oci_bind_by_name($this->_sth, $column, $lob, -1, OCI_B_BLOB);
Steve Müller's avatar
Steve Müller committed
274
        } elseif ($length !== null) {
275 276
            $this->boundValues[$column] =& $variable;

Benjamin Eberlei's avatar
Benjamin Eberlei committed
277
            return oci_bind_by_name($this->_sth, $column, $variable, $length);
278
        }
Benjamin Eberlei's avatar
Benjamin Eberlei committed
279

280 281
        $this->boundValues[$column] =& $variable;

Benjamin Eberlei's avatar
Benjamin Eberlei committed
282
        return oci_bind_by_name($this->_sth, $column, $variable);
283 284 285
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
286
     * {@inheritdoc}
287 288 289
     */
    public function closeCursor()
    {
290 291 292 293 294
        // not having the result means there's nothing to close
        if (!$this->result) {
            return true;
        }

295
        oci_cancel($this->_sth);
296

297 298
        $this->result = false;

299
        return true;
300 301
    }

302
    /**
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
     * {@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
319

320 321
        return $error;
    }
322

323 324 325 326 327 328 329 330 331 332 333
    /**
     * {@inheritdoc}
     */
    public function errorInfo()
    {
        return oci_error($this->_sth);
    }

    /**
     * {@inheritdoc}
     */
334
    public function execute($params = null)
335
    {
336
        if ($params) {
337
            $hasZeroIndex = array_key_exists(0, $params);
338 339 340 341 342 343
            foreach ($params as $key => $val) {
                if ($hasZeroIndex && is_numeric($key)) {
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
344 345
            }
        }
346

347
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
348
        if ( ! $ret) {
349 350
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
        }
Benjamin Morel's avatar
Benjamin Morel committed
351

352 353
        $this->result = true;

354
        return $ret;
355 356
    }

357 358 359
    /**
     * {@inheritdoc}
     */
360
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
361
    {
362
        $this->_defaultFetchMode = $fetchMode;
Benjamin Morel's avatar
Benjamin Morel committed
363 364

        return true;
365 366 367 368 369 370 371
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
372
        $data = $this->fetchAll();
Benjamin Morel's avatar
Benjamin Morel committed
373

374 375 376
        return new \ArrayIterator($data);
    }

377 378 379
    /**
     * {@inheritdoc}
     */
380
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
381
    {
382 383 384 385 386 387
        // 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;
        }

388
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
389 390 391 392 393 394

        if (PDO::FETCH_OBJ == $fetchMode) {
            return oci_fetch_object($this->_sth);
        }

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

398 399 400 401
        return oci_fetch_array(
            $this->_sth,
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
        );
402 403 404 405 406
    }

    /**
     * {@inheritdoc}
     */
407
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
408
    {
409
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
410

411
        $result = [];
412 413 414 415 416 417 418 419 420

        if (PDO::FETCH_OBJ == $fetchMode) {
            while ($row = $this->fetch($fetchMode)) {
                $result[] = $row;
            }

            return $result;
        }

421 422
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
423
        }
424

425 426
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
            while ($row = $this->fetch($fetchMode)) {
427 428 429
                $result[] = $row;
            }
        } else {
430
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
431
            if ($fetchMode == PDO::FETCH_COLUMN) {
432 433
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
            }
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) {
438
                return [];
439 440
            }

441
            oci_fetch_all($this->_sth, $result, 0, -1,
442
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
443

444
            if ($fetchMode == PDO::FETCH_COLUMN) {
445 446
                $result = $result[0];
            }
447
        }
448

449 450 451 452 453 454 455 456
        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchColumn($columnIndex = 0)
    {
457 458 459 460 461 462
        // 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;
        }

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

465 466 467 468 469
        if (false === $row) {
            return false;
        }

        return isset($row[$columnIndex]) ? $row[$columnIndex] : null;
470 471 472 473 474 475 476 477
    }

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