Failed Conditions
Pull Request — master (#3018)
by Gabriel
64:18
created

OCI8Statement::fetchAll()   C

Complexity

Conditions 10
Paths 11

Size

Total Lines 49
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 49
ccs 0
cts 16
cp 0
rs 5.5471
c 0
b 0
f 0
cc 10
eloc 24
nc 11
nop 3
crap 110

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

172
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
173
            }
174 10
        } while ($result);
175
176
        if ($currentLiteralDelimiter) {
177
            throw new OCI8Exception(sprintf(
178
                'The statement contains non-terminated string literal starting at offset %d',
179
                $tokenOffset - 1
180
            ));
181
        }
182 10
183
        $fragments[] = substr($statement, $fragmentOffset);
184 10
        $statement   = implode('', $fragments);
185 10
186
        return [$statement, $paramMap];
187
    }
188 10
189 10
    /**
190 10
     * Finds next placeholder or opening quote.
191 10
     *
192 10
     * @param string             $statement               The SQL statement to parse
193 10
     * @param string             $tokenOffset             The offset to start searching from
194 10
     * @param int                $fragmentOffset          The offset to build the next fragment from
195 10
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
196
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
197 10
     *                                                    or NULL if not currently in a literal
198
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
199
     * @return bool Whether the token was found
200 8
     */
201 8
    private static function findPlaceholderOrOpeningQuote(
202
        $statement,
203 8
        &$tokenOffset,
204
        &$fragmentOffset,
205
        &$fragments,
206
        &$currentLiteralDelimiter,
207
        &$paramMap
208
    ) {
209
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
210
211
        if (! $token) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
212
            return false;
213
        }
214
215 8
        if ($token === '?') {
216
            $position            = count($paramMap) + 1;
217
            $param               = ':param' . $position;
218
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
219
            $fragments[]         = $param;
220 8
            $paramMap[$position] = $param;
221 8
            $tokenOffset        += 1;
222 8
            $fragmentOffset      = $tokenOffset;
223 8
224
            return true;
225
        }
226 8
227
        $currentLiteralDelimiter = $token;
228
        ++$tokenOffset;
229
230 8
        return true;
231 8
    }
232
233 8
    /**
234
     * Finds closing quote
235
     *
236
     * @param string      $statement               The SQL statement to parse
237
     * @param string      $tokenOffset             The offset to start searching from
238
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
239
     *                                             or NULL if not currently in a literal
240
     * @return bool Whether the token was found
241
     */
242
    private static function findClosingQuote(
243
        $statement,
244
        &$tokenOffset,
245 10
        &$currentLiteralDelimiter
246
    ) {
247 10
        $token = self::findToken(
248 10
            $statement,
249 10
            $tokenOffset,
250
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
251
        );
252 10
253
        if (! $token) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
254
            return false;
255
        }
256
257
        $currentLiteralDelimiter = false;
258
        ++$tokenOffset;
259
260
        return true;
261
    }
262
263
    /**
264
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
265
     * where the token was found.
266
     *
267
     * @param string $statement The SQL statement to parse
268
     * @param string $offset    The offset to start searching from
269
     * @param string $regex     The regex containing token pattern
270
     * @return string|null Token or NULL if not found
271
     */
272
    private static function findToken($statement, &$offset, $regex)
273
    {
274
        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

274
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, /** @scrutinizer ignore-type */ $offset)) {
Loading history...
275
            $offset = $matches[0][1];
276
            return $matches[0][0];
277
        }
278
279
        return null;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function bindValue($param, $value, $type = ParameterType::STRING)
286
    {
287
        return $this->bindParam($param, $value, $type, null);
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
294
    {
295
        $column = $this->_paramMap[$column] ?? $column;
296
297
        if ($type === ParameterType::LARGE_OBJECT) {
298
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
299
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
300
301
            $this->boundValues[$column] =& $lob;
302
303
            return oci_bind_by_name($this->_sth, $column, $lob, -1, OCI_B_BLOB);
304
        } elseif ($length !== null) {
305
            $this->boundValues[$column] =& $variable;
306
307
            return oci_bind_by_name($this->_sth, $column, $variable, $length);
308
        }
309
310
        $this->boundValues[$column] =& $variable;
311
312
        return oci_bind_by_name($this->_sth, $column, $variable);
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318
    public function closeCursor()
319
    {
320
        // not having the result means there's nothing to close
321
        if (! $this->result) {
322
            return true;
323
        }
324
325
        oci_cancel($this->_sth);
326
327
        $this->result = false;
328
329
        return true;
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335
    public function columnCount()
336
    {
337
        return oci_num_fields($this->_sth);
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     */
343
    public function errorCode()
344
    {
345
        $error = oci_error($this->_sth);
346
        if ($error !== false) {
0 ignored issues
show
introduced by
The condition $error !== false can never be false.
Loading history...
347
            $error = $error['code'];
348
        }
349
350
        return $error;
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356
    public function errorInfo()
357
    {
358
        return oci_error($this->_sth);
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     */
364
    public function execute($params = null)
365
    {
366
        if ($params) {
367
            $hasZeroIndex = array_key_exists(0, $params);
368
            foreach ($params as $key => $val) {
369
                if ($hasZeroIndex && is_numeric($key)) {
370
                    $this->bindValue($key + 1, $val);
371
                } else {
372
                    $this->bindValue($key, $val);
373
                }
374
            }
375
        }
376
377
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
378
        if (! $ret) {
379
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
380
        }
381
382
        $this->result = true;
383
384
        return $ret;
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     */
390
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
391
    {
392
        $this->_defaultFetchMode = $fetchMode;
393
394
        return true;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    public function getIterator()
401
    {
402
        return new StatementIterator($this);
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     */
408
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
409
    {
410
        // do not try fetching from the statement if it's not expected to contain result
411
        // in order to prevent exceptional situation
412
        if (! $this->result) {
413
            return false;
414
        }
415
416
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
417
418
        if ($fetchMode === FetchMode::COLUMN) {
419
            return $this->fetchColumn();
420
        }
421
422
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
423
            return oci_fetch_object($this->_sth);
424
        }
425
426
        if (! isset(self::$fetchModeMap[$fetchMode])) {
427
            throw new \InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
428
        }
429
430
        return oci_fetch_array(
431
            $this->_sth,
432
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
433
        );
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     */
439
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
440
    {
441
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
442
443
        $result = [];
444
445
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
446
            while ($row = $this->fetch($fetchMode)) {
447
                $result[] = $row;
448
            }
449
450
            return $result;
451
        }
452
453
        if (! isset(self::$fetchModeMap[$fetchMode])) {
454
            throw new \InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
455
        }
456
457
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
458
            while ($row = $this->fetch($fetchMode)) {
459
                $result[] = $row;
460
            }
461
        } else {
462
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
463
464
            if ($fetchMode === FetchMode::COLUMN) {
465
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
466
            }
467
468
            // do not try fetching from the statement if it's not expected to contain result
469
            // in order to prevent exceptional situation
470
            if (! $this->result) {
471
                return [];
472
            }
473
474
            oci_fetch_all(
475
                $this->_sth,
476
                $result,
477
                0,
478
                -1,
479
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
480
            );
481
482
            if ($fetchMode === FetchMode::COLUMN) {
483
                $result = $result[0];
484
            }
485
        }
486
487
        return $result;
488
    }
489
490
    /**
491
     * {@inheritdoc}
492
     */
493
    public function fetchColumn($columnIndex = 0)
494
    {
495
        // do not try fetching from the statement if it's not expected to contain result
496
        // in order to prevent exceptional situation
497
        if (! $this->result) {
498
            return false;
499
        }
500
501
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
502
503
        if ($row === false) {
0 ignored issues
show
introduced by
The condition $row === false can never be true.
Loading history...
504
            return false;
505
        }
506
507
        return $row[$columnIndex] ?? null;
508
    }
509
510
    /**
511
     * {@inheritdoc}
512
     */
513
    public function rowCount()
514
    {
515
        return oci_num_rows($this->_sth);
516
    }
517
}
518