Passed
Pull Request — master (#3149)
by Sergei
27:12
created

OCI8Statement::setFetchMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 3
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Driver\OCI8;
21
22
use Doctrine\DBAL\Driver\Statement;
23
use Doctrine\DBAL\Driver\StatementIterator;
24
use Doctrine\DBAL\FetchMode;
25
use Doctrine\DBAL\ParameterType;
26
use IteratorAggregate;
27
use const OCI_ASSOC;
28
use const OCI_B_BIN;
29
use const OCI_B_BLOB;
30
use const OCI_BOTH;
31
use const OCI_D_LOB;
32
use const OCI_FETCHSTATEMENT_BY_COLUMN;
33
use const OCI_FETCHSTATEMENT_BY_ROW;
34
use const OCI_NUM;
35
use const OCI_RETURN_LOBS;
36
use const OCI_RETURN_NULLS;
37
use const OCI_TEMP_BLOB;
38
use const PREG_OFFSET_CAPTURE;
39
use const SQLT_CHR;
40
use function array_key_exists;
41
use function count;
42
use function implode;
43
use function is_numeric;
44
use function oci_bind_by_name;
45
use function oci_cancel;
46
use function oci_error;
47
use function oci_execute;
48
use function oci_fetch_all;
49
use function oci_fetch_array;
50
use function oci_fetch_object;
51
use function oci_new_descriptor;
52
use function oci_num_fields;
53
use function oci_num_rows;
54
use function oci_parse;
55
use function preg_match;
56
use function preg_quote;
57
use function sprintf;
58
use function substr;
59
60
/**
61
 * The OCI8 implementation of the Statement interface.
62
 *
63
 * @since 2.0
64
 * @author Roman Borschel <[email protected]>
65
 */
66
class OCI8Statement implements IteratorAggregate, Statement
67
{
68
    /**
69
     * @var resource
70
     */
71
    protected $_dbh;
72
73
    /**
74
     * @var resource
75
     */
76
    protected $_sth;
77
78
    /**
79
     * @var \Doctrine\DBAL\Driver\OCI8\OCI8Connection
80
     */
81
    protected $_conn;
82
83
    /**
84
     * @var string
85
     */
86
    protected static $_PARAM = ':param';
87
88
    /**
89
     * @var array
90
     */
91
    protected static $fetchModeMap = [
92
        FetchMode::MIXED       => OCI_BOTH,
93
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
94
        FetchMode::NUMERIC     => OCI_NUM,
95
        FetchMode::COLUMN      => OCI_NUM,
96
    ];
97
98
    /**
99
     * @var int
100
     */
101
    protected $_defaultFetchMode = FetchMode::MIXED;
102
103
    /**
104
     * @var array
105
     */
106
    protected $_paramMap = [];
107
108
    /**
109
     * Holds references to bound parameter values.
110
     *
111
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
112
     *
113
     * @var array
114
     */
115
    private $boundValues = [];
116
117
    /**
118
     * Indicates whether the statement is in the state when fetching results is possible
119
     *
120
     * @var bool
121
     */
122
    private $result = false;
123
124
    /**
125
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
126
     *
127
     * @param resource                                  $dbh       The connection handle.
128
     * @param string                                    $statement The SQL statement.
129
     * @param \Doctrine\DBAL\Driver\OCI8\OCI8Connection $conn
130
     */
131 258
    public function __construct($dbh, $statement, OCI8Connection $conn)
132
    {
133 258
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
134 258
        $this->_sth = oci_parse($dbh, $statement);
135 258
        $this->_dbh = $dbh;
136 258
        $this->_paramMap = $paramMap;
137 258
        $this->_conn = $conn;
138 258
    }
139
140
    /**
141
     * Converts positional (?) into named placeholders (:param<num>).
142
     *
143
     * Oracle does not support positional parameters, hence this method converts all
144
     * positional parameters into artificially named parameters. Note that this conversion
145
     * is not perfect. All question marks (?) in the original statement are treated as
146
     * placeholders and converted to a named parameter.
147
     *
148
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
149
     * Question marks inside literal strings are therefore handled correctly by this method.
150
     * This comes at a cost, the whole sql statement has to be looped over.
151
     *
152
     * @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.
154
     *
155
     * @param string $statement The SQL statement to convert.
156
     *
157
     * @return array [0] => the statement value (string), [1] => the paramMap value (array).
158
     * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
159
     */
160 451
    public static function convertPositionalToNamedPlaceholders($statement)
161
    {
162 451
        $fragmentOffset = $tokenOffset = 0;
163 451
        $fragments = $paramMap = [];
164 451
        $currentLiteralDelimiter = null;
165
166
        do {
167 451
            if (!$currentLiteralDelimiter) {
168 451
                $result = self::findPlaceholderOrOpeningQuote(
169 451
                    $statement,
170 451
                    $tokenOffset,
171 451
                    $fragmentOffset,
172 451
                    $fragments,
173 451
                    $currentLiteralDelimiter,
174 451
                    $paramMap
175
                );
176
            } else {
177 269
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
0 ignored issues
show
Bug introduced by
$currentLiteralDelimiter of type void is incompatible with the type null|string expected by parameter $currentLiteralDelimiter of Doctrine\DBAL\Driver\OCI...ent::findClosingQuote(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

177
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
178
            }
179 451
        } while ($result);
180
181 451
        if ($currentLiteralDelimiter) {
182 3
            throw new OCI8Exception(sprintf(
183 3
                'The statement contains non-terminated string literal starting at offset %d',
184 3
                $tokenOffset - 1
185
            ));
186
        }
187
188 448
        $fragments[] = substr($statement, $fragmentOffset);
189 448
        $statement = implode('', $fragments);
190
191 448
        return [$statement, $paramMap];
192
    }
193
194
    /**
195
     * Finds next placeholder or opening quote.
196
     *
197
     * @param string             $statement               The SQL statement to parse
198
     * @param string             $tokenOffset             The offset to start searching from
199
     * @param int                $fragmentOffset          The offset to build the next fragment from
200
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
201
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
202
     *                                                    or NULL if not currently in a literal
203
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
204
     * @return bool Whether the token was found
205
     */
206 451
    private static function findPlaceholderOrOpeningQuote(
207
        $statement,
208
        &$tokenOffset,
209
        &$fragmentOffset,
210
        &$fragments,
211
        &$currentLiteralDelimiter,
212
        &$paramMap
213
    ) {
214 451
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
215
216 451
        if (!$token) {
217 448
            return false;
218
        }
219
220 416
        if ($token === '?') {
221 313
            $position = count($paramMap) + 1;
222 313
            $param = ':param' . $position;
223 313
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
224 313
            $fragments[] = $param;
225 313
            $paramMap[$position] = $param;
226 313
            $tokenOffset += 1;
227 313
            $fragmentOffset = $tokenOffset;
228
229 313
            return true;
230
        }
231
232 269
        $currentLiteralDelimiter = $token;
233 269
        ++$tokenOffset;
234
235 269
        return true;
236
    }
237
238
    /**
239
     * Finds closing quote
240
     *
241
     * @param string      $statement               The SQL statement to parse
242
     * @param string      $tokenOffset             The offset to start searching from
243
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
244
     *                                             or NULL if not currently in a literal
245
     * @return bool Whether the token was found
246
     */
247 269
    private static function findClosingQuote(
248
        $statement,
249
        &$tokenOffset,
250
        &$currentLiteralDelimiter
251
    ) {
252 269
        $token = self::findToken(
253 269
            $statement,
254 269
            $tokenOffset,
255 269
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
256
        );
257
258 269
        if (!$token) {
259 3
            return false;
260
        }
261
262 267
        $currentLiteralDelimiter = false;
263 267
        ++$tokenOffset;
264
265 267
        return true;
266
    }
267
268
    /**
269
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
270
     * where the token was found.
271
     *
272
     * @param string $statement The SQL statement to parse
273
     * @param string $offset    The offset to start searching from
274
     * @param string $regex     The regex containing token pattern
275
     * @return string|null Token or NULL if not found
276
     */
277 451
    private static function findToken($statement, &$offset, $regex)
278
    {
279 451
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
0 ignored issues
show
Bug introduced by
$offset of type string is incompatible with the type integer expected by parameter $offset of preg_match(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

279
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, /** @scrutinizer ignore-type */ $offset)) {
Loading history...
280 416
            $offset = $matches[0][1];
281 416
            return $matches[0][0];
282
        }
283
284 451
        return null;
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290 118
    public function bindValue($param, $value, $type = ParameterType::STRING)
291
    {
292 118
        return $this->bindParam($param, $value, $type, null);
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298 123
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
299
    {
300 123
        $column = $this->_paramMap[$column] ?? $column;
301
302 123
        if ($type === ParameterType::LARGE_OBJECT) {
303 4
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
304 4
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
305
306 4
            $variable =& $lob;
307
        }
308
309 123
        $this->boundValues[$column] =& $variable;
310
311 123
        return oci_bind_by_name(
312 123
            $this->_sth,
313 123
            $column,
314 123
            $variable,
315 123
            $length ?? -1,
316 123
            $this->convertParamType($type)
317
        );
318
    }
319
320
    /**
321
     * Converts DBAL parameter type to oci8 parameter type
322
     */
323 123
    private function convertParamType(int $type) : int
324
    {
325 123
        switch ($type) {
326
            case ParameterType::BINARY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
327 1
                return OCI_B_BIN;
328
329
            case ParameterType::LARGE_OBJECT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
330 4
                return OCI_B_BLOB;
331
332
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
333 121
                return SQLT_CHR;
334
        }
335
    }
336
337
    /**
338
     * {@inheritdoc}
339
     */
340 19
    public function closeCursor()
341
    {
342
        // not having the result means there's nothing to close
343 19
        if (!$this->result) {
344 4
            return true;
345
        }
346
347 15
        oci_cancel($this->_sth);
348
349 15
        $this->result = false;
350
351 15
        return true;
352
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357 4
    public function columnCount()
358
    {
359 4
        return oci_num_fields($this->_sth);
360
    }
361
362
    /**
363
     * {@inheritdoc}
364
     */
365
    public function errorCode()
366
    {
367
        $error = oci_error($this->_sth);
368
        if ($error !== false) {
369
            $error = $error['code'];
370
        }
371
372
        return $error;
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378 139
    public function errorInfo()
379
    {
380 139
        return oci_error($this->_sth);
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386 259
    public function execute($params = null)
387
    {
388 259
        if ($params) {
389 91
            $hasZeroIndex = array_key_exists(0, $params);
390 91
            foreach ($params as $key => $val) {
391 91
                if ($hasZeroIndex && is_numeric($key)) {
392 91
                    $this->bindValue($key + 1, $val);
393
                } else {
394 91
                    $this->bindValue($key, $val);
395
                }
396
            }
397
        }
398
399 255
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
400 255
        if ( ! $ret) {
401 141
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
402
        }
403
404 252
        $this->result = true;
405
406 252
        return $ret;
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     */
412 225
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
413
    {
414 225
        $this->_defaultFetchMode = $fetchMode;
415
416 225
        return true;
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422 3
    public function getIterator()
423
    {
424 3
        return new StatementIterator($this);
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     */
430 78
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
431
    {
432
        // do not try fetching from the statement if it's not expected to contain result
433
        // in order to prevent exceptional situation
434 78
        if (!$this->result) {
435 3
            return false;
436
        }
437
438 75
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
439
440 75
        if ($fetchMode === FetchMode::COLUMN) {
441 1
            return $this->fetchColumn();
442
        }
443
444 74
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
445 1
            return oci_fetch_object($this->_sth);
446
        }
447
448 73
        if (! isset(self::$fetchModeMap[$fetchMode])) {
449
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
450
        }
451
452 73
        return oci_fetch_array(
453 73
            $this->_sth,
454 73
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
455
        );
456
    }
457
458
    /**
459
     * {@inheritdoc}
460
     */
461 96
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
462
    {
463 96
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
464
465 96
        $result = [];
466
467 96
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
468 1
            while ($row = $this->fetch($fetchMode)) {
469 1
                $result[] = $row;
470
            }
471
472 1
            return $result;
473
        }
474
475 95
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
476
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
477
        }
478
479 95
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
480 1
            while ($row = $this->fetch($fetchMode)) {
481 1
                $result[] = $row;
482
            }
483
        } else {
484 94
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
485
486 94
            if ($fetchMode === FetchMode::COLUMN) {
487 4
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
488
            }
489
490
            // do not try fetching from the statement if it's not expected to contain result
491
            // in order to prevent exceptional situation
492 94
            if (!$this->result) {
493 3
                return [];
494
            }
495
496 91
            oci_fetch_all($this->_sth, $result, 0, -1,
497 91
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
498
499 91
            if ($fetchMode === FetchMode::COLUMN) {
500 4
                $result = $result[0];
501
            }
502
        }
503
504 92
        return $result;
505
    }
506
507
    /**
508
     * {@inheritdoc}
509
     */
510 51
    public function fetchColumn($columnIndex = 0)
511
    {
512
        // do not try fetching from the statement if it's not expected to contain result
513
        // in order to prevent exceptional situation
514 51
        if (!$this->result) {
515 3
            return false;
516
        }
517
518 48
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
519
520 48
        if (false === $row) {
521 3
            return false;
522
        }
523
524 45
        return $row[$columnIndex] ?? null;
525
    }
526
527
    /**
528
     * {@inheritdoc}
529
     */
530 174
    public function rowCount()
531
    {
532 174
        return oci_num_rows($this->_sth);
533
    }
534
}
535