Passed
Pull Request — 2.11.x (#3971)
by Grégoire
03:18
created

OCI8Statement   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Test Coverage

Coverage 94.48%

Importance

Changes 0
Metric Value
wmc 57
eloc 167
dl 0
loc 486
ccs 154
cts 163
cp 0.9448
rs 5.04
c 0
b 0
f 0

19 Methods

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

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
    /**
62
     * @deprecated
63
     *
64
     * @var string
65
     */
66
    protected static $_PARAM = ':param';
67
68
    /** @var int[] */
69
    protected static $fetchModeMap = [
70
        FetchMode::MIXED       => OCI_BOTH,
71
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
72
        FetchMode::NUMERIC     => OCI_NUM,
73
        FetchMode::COLUMN      => OCI_NUM,
74
    ];
75
76
    /** @var int */
77
    protected $_defaultFetchMode = FetchMode::MIXED;
78
79
    /** @var string[] */
80
    protected $_paramMap = [];
81
82
    /**
83
     * Holds references to bound parameter values.
84
     *
85
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
86
     *
87
     * @var mixed[]
88
     */
89
    private $boundValues = [];
90
91
    /**
92
     * Indicates whether the statement is in the state when fetching results is possible
93
     *
94
     * @var bool
95
     */
96
    private $result = false;
97
98
    /**
99
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
100
     *
101
     * @param resource $dbh   The connection handle.
102
     * @param string   $query The SQL query.
103
     */
104 307
    public function __construct($dbh, $query, OCI8Connection $conn)
105
    {
106 307
        [$query, $paramMap] = self::convertPositionalToNamedPlaceholders($query);
107
108 307
        $stmt = oci_parse($dbh, $query);
109 307
        assert(is_resource($stmt));
110
111 307
        $this->_sth      = $stmt;
112 307
        $this->_dbh      = $dbh;
113 307
        $this->_paramMap = $paramMap;
114 307
        $this->_conn     = $conn;
115 307
    }
116
117
    /**
118
     * Converts positional (?) into named placeholders (:param<num>).
119
     *
120
     * Oracle does not support positional parameters, hence this method converts all
121
     * positional parameters into artificially named parameters. Note that this conversion
122
     * is not perfect. All question marks (?) in the original statement are treated as
123
     * placeholders and converted to a named parameter.
124
     *
125
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
126
     * Question marks inside literal strings are therefore handled correctly by this method.
127
     * This comes at a cost, the whole sql statement has to be looped over.
128
     *
129
     * @param string $statement The SQL statement to convert.
130
     *
131
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
132
     *
133
     * @throws OCI8Exception
134
     *
135
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
136
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
137
     */
138 458
    public static function convertPositionalToNamedPlaceholders($statement)
139
    {
140 458
        $fragmentOffset          = $tokenOffset = 0;
141 458
        $fragments               = $paramMap = [];
142 458
        $currentLiteralDelimiter = null;
143
144
        do {
145 458
            if (! $currentLiteralDelimiter) {
146 458
                $result = self::findPlaceholderOrOpeningQuote(
147 458
                    $statement,
148
                    $tokenOffset,
149
                    $fragmentOffset,
150
                    $fragments,
151
                    $currentLiteralDelimiter,
152
                    $paramMap
153
                );
154
            } else {
155 258
                $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

155
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
156
            }
157 458
        } while ($result);
158
159 458
        if ($currentLiteralDelimiter) {
160 3
            throw new OCI8Exception(sprintf(
161 3
                'The statement contains non-terminated string literal starting at offset %d',
162 3
                $tokenOffset - 1
163
            ));
164
        }
165
166 455
        $fragments[] = substr($statement, $fragmentOffset);
167 455
        $statement   = implode('', $fragments);
168
169 455
        return [$statement, $paramMap];
170
    }
171
172
    /**
173
     * Finds next placeholder or opening quote.
174
     *
175
     * @param string             $statement               The SQL statement to parse
176
     * @param string             $tokenOffset             The offset to start searching from
177
     * @param int                $fragmentOffset          The offset to build the next fragment from
178
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
179
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
180
     *                                                    or NULL if not currently in a literal
181
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
182
     *
183
     * @return bool Whether the token was found
184
     */
185 458
    private static function findPlaceholderOrOpeningQuote(
186
        $statement,
187
        &$tokenOffset,
188
        &$fragmentOffset,
189
        &$fragments,
190
        &$currentLiteralDelimiter,
191
        &$paramMap
192
    ) {
193 458
        $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

193
        $token = self::findToken($statement, /** @scrutinizer ignore-type */ $tokenOffset, '/[?\'"]/');
Loading history...
194
195 458
        if (! $token) {
196 455
            return false;
197
        }
198
199 405
        if ($token === '?') {
200 279
            $position            = count($paramMap) + 1;
201 279
            $param               = ':param' . $position;
202 279
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
203 279
            $fragments[]         = $param;
204 279
            $paramMap[$position] = $param;
205 279
            $tokenOffset        += 1;
206 279
            $fragmentOffset      = $tokenOffset;
207
208 279
            return true;
209
        }
210
211 258
        $currentLiteralDelimiter = $token;
212 258
        ++$tokenOffset;
213
214 258
        return true;
215
    }
216
217
    /**
218
     * Finds closing quote
219
     *
220
     * @param string $statement               The SQL statement to parse
221
     * @param string $tokenOffset             The offset to start searching from
222
     * @param string $currentLiteralDelimiter The delimiter of the current string literal
223
     *
224
     * @return bool Whether the token was found
225
     */
226 258
    private static function findClosingQuote(
227
        $statement,
228
        &$tokenOffset,
229
        &$currentLiteralDelimiter
230
    ) {
231 258
        $token = self::findToken(
232 166
            $statement,
233
            $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

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