Failed Conditions
Push — master ( 94cec7...7f79d0 )
by Marco
25s queued 13s
created

OCI8Statement   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Test Coverage

Coverage 95.6%

Importance

Changes 0
Metric Value
wmc 55
eloc 164
dl 0
loc 476
ccs 152
cts 159
cp 0.956
rs 6
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A convertParameterType() 0 11 3
A errorInfo() 0 9 2
A columnCount() 0 3 2
A convertPositionalToNamedPlaceholders() 0 32 4
A findToken() 0 8 2
A findClosingQuote() 0 19 2
A bindValue() 0 3 1
A closeCursor() 0 12 2
A errorCode() 0 8 2
A findPlaceholderOrOpeningQuote() 0 30 3
A bindParam() 0 23 2
A rowCount() 0 3 2
A fetchColumn() 0 15 3
B fetchAll() 0 49 10
A setFetchMode() 0 5 1
A getIterator() 0 3 1
A execute() 0 22 6
A fetch() 0 25 6

How to fix   Complexity   

Complex Class

Complex classes like OCI8Statement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OCI8Statement, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Doctrine\DBAL\Driver\OCI8;
4
5
use Doctrine\DBAL\Driver\Statement;
6
use Doctrine\DBAL\Driver\StatementIterator;
7
use Doctrine\DBAL\FetchMode;
8
use Doctrine\DBAL\ParameterType;
9
use InvalidArgumentException;
10
use IteratorAggregate;
11
use PDO;
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 262
     * @param string   $query The SQL query.
99
     */
100 262
    public function __construct($dbh, $query, OCI8Connection $conn)
101 262
    {
102 262
        [$query, $paramMap] = self::convertPositionalToNamedPlaceholders($query);
103 262
104 262
        $stmt = oci_parse($dbh, $query);
105 262
        assert(is_resource($stmt));
106
107
        $this->_sth      = $stmt;
108
        $this->_dbh      = $dbh;
109
        $this->_paramMap = $paramMap;
110
        $this->_conn     = $conn;
111
    }
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 538
     *
129
     * @throws OCI8Exception
130 538
     *
131 538
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
132 538
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
133
     */
134
    public static function convertPositionalToNamedPlaceholders($statement)
135 538
    {
136 538
        $fragmentOffset          = $tokenOffset = 0;
137 538
        $fragments               = $paramMap = [];
138 538
        $currentLiteralDelimiter = null;
139 538
140 538
        do {
141 538
            if (! $currentLiteralDelimiter) {
142 538
                $result = self::findPlaceholderOrOpeningQuote(
143
                    $statement,
144
                    $tokenOffset,
145 340
                    $fragmentOffset,
146
                    $fragments,
147 538
                    $currentLiteralDelimiter,
148
                    $paramMap
149 538
                );
150 6
            } else {
151 6
                $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

151
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
152 6
            }
153
        } while ($result);
154
155
        if ($currentLiteralDelimiter) {
156 532
            throw new OCI8Exception(sprintf(
157 532
                'The statement contains non-terminated string literal starting at offset %d',
158
                $tokenOffset - 1
159 532
            ));
160
        }
161
162
        $fragments[] = substr($statement, $fragmentOffset);
163
        $statement   = implode('', $fragments);
164
165
        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 538
     * @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
    private static function findPlaceholderOrOpeningQuote(
182
        $statement,
183 538
        &$tokenOffset,
184
        &$fragmentOffset,
185 538
        &$fragments,
186 532
        &$currentLiteralDelimiter,
187
        &$paramMap
188
    ) {
189 503
        $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 393
191 393
        if (! $token) {
192 393
            return false;
193 393
        }
194 393
195 393
        if ($token === '?') {
196 393
            $position            = count($paramMap) + 1;
197
            $param               = ':param' . $position;
198 393
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
199
            $fragments[]         = $param;
200
            $paramMap[$position] = $param;
201 340
            $tokenOffset        += 1;
202 340
            $fragmentOffset      = $tokenOffset;
203
204 340
            return true;
205
        }
206
207
        $currentLiteralDelimiter = $token;
208
        ++$tokenOffset;
209
210
        return true;
211
    }
212
213
    /**
214
     * Finds closing quote
215
     *
216
     * @param string      $statement               The SQL statement to parse
217 340
     * @param string      $tokenOffset             The offset to start searching from
218
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
219
     *                                             or NULL if not currently in a literal
220
     *
221
     * @return bool Whether the token was found
222 340
     */
223 340
    private static function findClosingQuote(
224 340
        $statement,
225 340
        &$tokenOffset,
226
        &$currentLiteralDelimiter
227
    ) {
228 340
        $token = self::findToken(
229 6
            $statement,
230
            $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

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