Completed
Push — develop ( 72ba3e...de019a )
by Marco
25s queued 12s
created

OCI8Statement::fetchAll()   B

Complexity

Conditions 10
Paths 11

Size

Total Lines 49
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 10.0056

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 49
ccs 25
cts 26
cp 0.9615
rs 7.6666
c 0
b 0
f 0
cc 10
nc 11
nop 2
crap 10.0056

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Doctrine\DBAL\Driver\OCI8;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Driver\Statement;
7
use Doctrine\DBAL\Driver\StatementIterator;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\ParameterType;
10
use InvalidArgumentException;
11
use IteratorAggregate;
12
use const OCI_ASSOC;
13
use const OCI_B_BIN;
14
use const OCI_B_BLOB;
15
use const OCI_BOTH;
16
use const OCI_D_LOB;
17
use const OCI_FETCHSTATEMENT_BY_COLUMN;
18
use const OCI_FETCHSTATEMENT_BY_ROW;
19
use const OCI_NUM;
20
use const OCI_RETURN_LOBS;
21
use const OCI_RETURN_NULLS;
22
use const OCI_TEMP_BLOB;
23
use const PREG_OFFSET_CAPTURE;
24
use const SQLT_CHR;
25
use function array_key_exists;
26
use function assert;
27
use function count;
28
use function implode;
29
use function is_int;
30
use function is_resource;
31
use function oci_bind_by_name;
32
use function oci_cancel;
33
use function oci_error;
34
use function oci_execute;
35
use function oci_fetch_all;
36
use function oci_fetch_array;
37
use function oci_fetch_object;
38
use function oci_new_descriptor;
39
use function oci_num_fields;
40
use function oci_num_rows;
41
use function oci_parse;
42
use function preg_match;
43
use function preg_quote;
44
use function sprintf;
45
use function substr;
46
47
/**
48
 * The OCI8 implementation of the Statement interface.
49
 */
50
class OCI8Statement implements IteratorAggregate, Statement
51
{
52
    /** @var resource */
53
    protected $_dbh;
54
55
    /** @var resource */
56
    protected $_sth;
57
58
    /** @var OCI8Connection */
59
    protected $_conn;
60
61
    /** @var string */
62
    protected static $_PARAM = ':param';
63
64
    /** @var int[] */
65
    protected static $fetchModeMap = [
66
        FetchMode::MIXED       => OCI_BOTH,
67
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
68
        FetchMode::NUMERIC     => OCI_NUM,
69
        FetchMode::COLUMN      => OCI_NUM,
70
    ];
71
72
    /** @var int */
73
    protected $_defaultFetchMode = FetchMode::MIXED;
74
75
    /** @var string[] */
76
    protected $_paramMap = [];
77
78
    /**
79
     * Holds references to bound parameter values.
80
     *
81
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
82
     *
83
     * @var mixed[]
84
     */
85
    private $boundValues = [];
86
87
    /**
88
     * Indicates whether the statement is in the state when fetching results is possible
89
     *
90
     * @var bool
91
     */
92
    private $result = false;
93
94
    /**
95
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
96
     *
97
     * @param resource $dbh   The connection handle.
98
     * @param string   $query The SQL query.
99
     */
100 263
    public function __construct($dbh, $query, OCI8Connection $conn)
101
    {
102 263
        [$query, $paramMap] = self::convertPositionalToNamedPlaceholders($query);
103
104 263
        $stmt = oci_parse($dbh, $query);
105 263
        assert(is_resource($stmt));
106
107 263
        $this->_sth      = $stmt;
108 263
        $this->_dbh      = $dbh;
109 263
        $this->_paramMap = $paramMap;
110 263
        $this->_conn     = $conn;
111 263
    }
112
113
    /**
114
     * Converts positional (?) into named placeholders (:param<num>).
115
     *
116
     * Oracle does not support positional parameters, hence this method converts all
117
     * positional parameters into artificially named parameters. Note that this conversion
118
     * is not perfect. All question marks (?) in the original statement are treated as
119
     * placeholders and converted to a named parameter.
120
     *
121
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
122
     * Question marks inside literal strings are therefore handled correctly by this method.
123
     * This comes at a cost, the whole sql statement has to be looped over.
124
     *
125
     * @param string $statement The SQL statement to convert.
126
     *
127
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
128
     *
129
     * @throws OCI8Exception
130
     *
131
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
132
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
133
     */
134 444
    public static function convertPositionalToNamedPlaceholders($statement)
135
    {
136 444
        $fragmentOffset          = $tokenOffset = 0;
137 444
        $fragments               = $paramMap = [];
138 444
        $currentLiteralDelimiter = null;
139
140
        do {
141 444
            if (! $currentLiteralDelimiter) {
142 444
                $result = self::findPlaceholderOrOpeningQuote(
143 437
                    $statement,
144 276
                    $tokenOffset,
145 276
                    $fragmentOffset,
146 276
                    $fragments,
147 276
                    $currentLiteralDelimiter,
148 283
                    $paramMap
149
                );
150
            } else {
151 263
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
0 ignored issues
show
Bug introduced by
$currentLiteralDelimiter of type void is incompatible with the type 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

151
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
152
            }
153 444
        } while ($result);
154
155 444
        if ($currentLiteralDelimiter) {
156 3
            throw new OCI8Exception(sprintf(
157 3
                'The statement contains non-terminated string literal starting at offset %d',
158 3
                $tokenOffset - 1
159
            ));
160
        }
161
162 441
        $fragments[] = substr($statement, $fragmentOffset);
163 441
        $statement   = implode('', $fragments);
164
165 441
        return [$statement, $paramMap];
166
    }
167
168
    /**
169
     * Finds next placeholder or opening quote.
170
     *
171
     * @param string             $statement               The SQL statement to parse
172
     * @param string             $tokenOffset             The offset to start searching from
173
     * @param int                $fragmentOffset          The offset to build the next fragment from
174
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
175
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
176
     *                                                    or NULL if not currently in a literal
177
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
178
     *
179
     * @return bool Whether the token was found
180
     */
181 444
    private static function findPlaceholderOrOpeningQuote(
182
        $statement,
183
        &$tokenOffset,
184
        &$fragmentOffset,
185
        &$fragments,
186
        &$currentLiteralDelimiter,
187
        &$paramMap
188
    ) {
189 444
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
0 ignored issues
show
Bug introduced by
$tokenOffset of type string is incompatible with the type integer expected by parameter $offset of Doctrine\DBAL\Driver\OCI...8Statement::findToken(). ( Ignorable by Annotation )

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

189
        $token = self::findToken($statement, /** @scrutinizer ignore-type */ $tokenOffset, '/[?\'"]/');
Loading history...
190
191 444
        if (! $token) {
192 441
            return false;
193
        }
194
195 408
        if ($token === '?') {
196 301
            $position            = count($paramMap) + 1;
197 301
            $param               = ':param' . $position;
198 301
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
199 301
            $fragments[]         = $param;
200 301
            $paramMap[$position] = $param;
201 301
            $tokenOffset        += 1;
202 301
            $fragmentOffset      = $tokenOffset;
203
204 301
            return true;
205
        }
206
207 263
        $currentLiteralDelimiter = $token;
208 263
        ++$tokenOffset;
209
210 263
        return true;
211
    }
212
213
    /**
214
     * Finds closing quote
215
     *
216
     * @param string $statement               The SQL statement to parse
217
     * @param string $tokenOffset             The offset to start searching from
218
     * @param string $currentLiteralDelimiter The delimiter of the current string literal
219
     *
220
     * @return bool Whether the token was found
221
     */
222 263
    private static function findClosingQuote(
223
        $statement,
224
        &$tokenOffset,
225
        &$currentLiteralDelimiter
226
    ) {
227 263
        $token = self::findToken(
228 143
            $statement,
229 143
            $tokenOffset,
0 ignored issues
show
Bug introduced by
$tokenOffset of type string is incompatible with the type integer expected by parameter $offset of Doctrine\DBAL\Driver\OCI...8Statement::findToken(). ( Ignorable by Annotation )

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

229
            /** @scrutinizer ignore-type */ $tokenOffset,
Loading history...
230 263
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
231
        );
232
233 263
        if (! $token) {
234 3
            return false;
235
        }
236
237 261
        $currentLiteralDelimiter = false;
238 261
        ++$tokenOffset;
239
240 261
        return true;
241
    }
242
243
    /**
244
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
245
     * where the token was found.
246
     *
247
     * @param string $statement The SQL statement to parse
248
     * @param int    $offset    The offset to start searching from
249
     * @param string $regex     The regex containing token pattern
250
     *
251
     * @return string|null Token or NULL if not found
252
     */
253 444
    private static function findToken($statement, &$offset, $regex)
254
    {
255 444
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
256 408
            $offset = $matches[0][1];
257 408
            return $matches[0][0];
258
        }
259
260 444
        return null;
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266 119
    public function bindValue($param, $value, $type = ParameterType::STRING) : void
267
    {
268 119
        $this->bindParam($param, $value, $type, null);
269 118
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274 124
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) : void
275
    {
276 124
        $column = $this->_paramMap[$column];
277
278 124
        if ($type === ParameterType::LARGE_OBJECT) {
279 4
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
280
281 4
            $class = 'OCI-Lob';
282 4
            assert($lob instanceof $class);
283
284 4
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
285
286 4
            $variable =& $lob;
287
        }
288
289 124
        $this->boundValues[$column] =& $variable;
290
291 124
        if (! oci_bind_by_name(
292 124
            $this->_sth,
293 124
            $column,
294 124
            $variable,
295 124
            $length ?? -1,
296 124
            $this->convertParameterType($type)
297
        )) {
298 1
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
299
        }
300 123
    }
301
302
    /**
303
     * Converts DBAL parameter type to oci8 parameter type
304
     */
305 124
    private function convertParameterType(int $type) : int
306
    {
307 124
        switch ($type) {
308
            case ParameterType::BINARY:
309 1
                return OCI_B_BIN;
310
311
            case ParameterType::LARGE_OBJECT:
312 4
                return OCI_B_BLOB;
313
314
            default:
315 122
                return SQLT_CHR;
316
        }
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322 18
    public function closeCursor() : void
323
    {
324
        // not having the result means there's nothing to close
325 18
        if (! $this->result) {
326 3
            return;
327
        }
328
329 15
        oci_cancel($this->_sth);
330
331 15
        $this->result = false;
332 15
    }
333
334
    /**
335
     * {@inheritdoc}
336
     */
337 4
    public function columnCount()
338
    {
339 4
        return oci_num_fields($this->_sth) ?: 0;
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345
    public function errorCode()
346
    {
347
        $error = oci_error($this->_sth);
348
        if ($error !== false) {
349
            $error = $error['code'];
350
        }
351
352
        return $error;
353
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358 144
    public function errorInfo()
359
    {
360 144
        $error = oci_error($this->_sth);
361
362 144
        if ($error === false) {
363
            return [];
364
        }
365
366 144
        return $error;
367
    }
368
369
    /**
370
     * {@inheritdoc}
371
     */
372 264
    public function execute($params = null) : void
373
    {
374 264
        if ($params) {
375 92
            $hasZeroIndex = array_key_exists(0, $params);
376
377 92
            foreach ($params as $key => $val) {
378 92
                if ($hasZeroIndex && is_int($key)) {
379 92
                    $param = $key + 1;
380
                } else {
381
                    $param = $key;
382
                }
383
384 92
                $this->bindValue($param, $val);
385
            }
386
        }
387
388 260
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
389 260
        if (! $ret) {
390 146
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
391
        }
392
393 257
        $this->result = true;
394 257
    }
395
396
    /**
397
     * {@inheritdoc}
398
     */
399 230
    public function setFetchMode($fetchMode, ...$args) : void
400
    {
401 230
        $this->_defaultFetchMode = $fetchMode;
402 230
    }
403
404
    /**
405
     * {@inheritdoc}
406
     */
407 3
    public function getIterator()
408
    {
409 3
        return new StatementIterator($this);
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415 77
    public function fetch($fetchMode = null, ...$args)
416
    {
417
        // do not try fetching from the statement if it's not expected to contain result
418
        // in order to prevent exceptional situation
419 77
        if (! $this->result) {
420 3
            return false;
421
        }
422
423 74
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
424
425 74
        if ($fetchMode === FetchMode::COLUMN) {
426 1
            return $this->fetchColumn();
427
        }
428
429 73
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
430 1
            return oci_fetch_object($this->_sth);
431
        }
432
433 72
        if (! isset(self::$fetchModeMap[$fetchMode])) {
434
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
435
        }
436
437 72
        return oci_fetch_array(
438 72
            $this->_sth,
439 72
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
440
        );
441
    }
442
443
    /**
444
     * {@inheritdoc}
445
     */
446 99
    public function fetchAll($fetchMode = null, ...$args)
447
    {
448 99
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
449
450 99
        $result = [];
451
452 99
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
453 1
            while ($row = $this->fetch($fetchMode)) {
454 1
                $result[] = $row;
455
            }
456
457 1
            return $result;
458
        }
459
460 98
        if (! isset(self::$fetchModeMap[$fetchMode])) {
461
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
462
        }
463
464 98
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
465 1
            while ($row = $this->fetch($fetchMode)) {
466 1
                $result[] = $row;
467
            }
468
        } else {
469 97
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
470
471 97
            if ($fetchMode === FetchMode::COLUMN) {
472 6
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
473
            }
474
475
            // do not try fetching from the statement if it's not expected to contain result
476
            // in order to prevent exceptional situation
477 97
            if (! $this->result) {
478 3
                return [];
479
            }
480
481 94
            oci_fetch_all(
482 94
                $this->_sth,
483 94
                $result,
484 94
                0,
485 94
                -1,
486 94
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
487
            );
488
489 94
            if ($fetchMode === FetchMode::COLUMN) {
490 6
                $result = $result[0];
491
            }
492
        }
493
494 95
        return $result;
495
    }
496
497
    /**
498
     * {@inheritdoc}
499
     */
500 54
    public function fetchColumn($columnIndex = 0)
501
    {
502
        // do not try fetching from the statement if it's not expected to contain result
503
        // in order to prevent exceptional situation
504 54
        if (! $this->result) {
505 3
            return false;
506
        }
507
508 51
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
509
510 51
        if ($row === false) {
511 3
            return false;
512
        }
513
514 48
        if (! array_key_exists($columnIndex, $row)) {
515 2
            throw DBALException::invalidColumnIndex($columnIndex, count($row));
516
        }
517
518 46
        return $row[$columnIndex];
519
    }
520
521
    /**
522
     * {@inheritdoc}
523
     */
524 172
    public function rowCount() : int
525
    {
526 172
        return oci_num_rows($this->_sth) ?: 0;
527
    }
528
}
529