Failed Conditions
Pull Request — develop (#3581)
by Jonathan
12:44
created

OCI8Statement   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Test Coverage

Coverage 97.53%

Importance

Changes 0
Metric Value
wmc 53
eloc 157
dl 0
loc 451
ccs 158
cts 162
cp 0.9753
rs 6.96
c 0
b 0
f 0

17 Methods

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

153
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
154
            }
155 509
        } while ($result);
156
157 509
        if ($currentLiteralDelimiter) {
158 3
            throw new OCI8Exception(sprintf(
159 3
                'The statement contains non-terminated string literal starting at offset %d.',
160 3
                $tokenOffset - 1
161
            ));
162
        }
163
164 506
        $fragments[] = substr($statement, $fragmentOffset);
165 506
        $statement   = implode('', $fragments);
166
167 506
        return [$statement, $paramMap];
168
    }
169
170
    /**
171
     * Finds next placeholder or opening quote.
172
     *
173
     * @param string      $statement               The SQL statement to parse
174
     * @param int         $tokenOffset             The offset to start searching from
175
     * @param int         $fragmentOffset          The offset to build the next fragment from
176
     * @param string[]    $fragments               Fragments of the original statement not containing placeholders
177
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
178
     *                                             or NULL if not currently in a literal
179
     * @param string[]    $paramMap                Mapping of the original parameter positions to their named replacements
180
     *
181
     * @return bool Whether the token was found
182
     */
183 509
    private static function findPlaceholderOrOpeningQuote(
184
        string $statement,
185
        int &$tokenOffset,
186
        int &$fragmentOffset,
187
        array &$fragments,
188
        ?string &$currentLiteralDelimiter,
189
        array &$paramMap
190
    ) : bool {
191 509
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
192
193 509
        if (! $token) {
194 506
            return false;
195
        }
196
197 423
        if ($token === '?') {
198 300
            $position            = count($paramMap) + 1;
199 300
            $param               = ':param' . $position;
200 300
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
201 300
            $fragments[]         = $param;
202 300
            $paramMap[$position] = $param;
203 300
            $tokenOffset        += 1;
204 300
            $fragmentOffset      = $tokenOffset;
205
206 300
            return true;
207
        }
208
209 260
        $currentLiteralDelimiter = $token;
210 260
        ++$tokenOffset;
211
212 260
        return true;
213
    }
214
215
    /**
216
     * Finds closing quote
217
     *
218
     * @param string $statement               The SQL statement to parse
219
     * @param int    $tokenOffset             The offset to start searching from
220
     * @param string $currentLiteralDelimiter The delimiter of the current string literal
221
     *
222
     * @return bool Whether the token was found
223
     */
224 260
    private static function findClosingQuote(
225
        string $statement,
226
        int &$tokenOffset,
227
        string &$currentLiteralDelimiter
228
    ) : bool {
229 260
        $token = self::findToken(
230 160
            $statement,
231 160
            $tokenOffset,
232 260
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
233
        );
234
235 260
        if (! $token) {
236 3
            return false;
237
        }
238
239 258
        $currentLiteralDelimiter = null;
240 258
        ++$tokenOffset;
241
242 258
        return true;
243
    }
244
245
    /**
246
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
247
     * where the token was found.
248
     *
249
     * @param string $statement The SQL statement to parse
250
     * @param int    $offset    The offset to start searching from
251
     * @param string $regex     The regex containing token pattern
252
     *
253
     * @return string|null Token or NULL if not found
254
     */
255 509
    private static function findToken(string $statement, int &$offset, string $regex) : ?string
256
    {
257 509
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
258 423
            $offset = $matches[0][1];
259
260 423
            return $matches[0][0];
261
        }
262
263 509
        return null;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269 120
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
270
    {
271 120
        $this->bindParam($param, $value, $type, null);
272 119
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277 141
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
278
    {
279 141
        $param = $this->_paramMap[$param];
280
281 141
        if ($type === ParameterType::LARGE_OBJECT) {
282 4
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
283
284 4
            $class = 'OCI-Lob';
285 4
            assert($lob instanceof $class);
286
287 4
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
288
289 4
            $variable =& $lob;
290
        }
291
292 141
        $this->boundValues[$param] =& $variable;
293
294 141
        if (! oci_bind_by_name(
295 141
            $this->_sth,
296 141
            $param,
297 141
            $variable,
298 141
            $length ?? -1,
299 141
            $this->convertParameterType($type)
300
        )) {
301
            throw OCI8Exception::fromErrorInfo(oci_error($this->_sth));
302
        }
303 140
    }
304
305
    /**
306
     * Converts DBAL parameter type to oci8 parameter type
307
     */
308 141
    private function convertParameterType(int $type) : int
309
    {
310 141
        switch ($type) {
311
            case ParameterType::BINARY:
312 1
                return OCI_B_BIN;
313
314
            case ParameterType::LARGE_OBJECT:
315 4
                return OCI_B_BLOB;
316
317
            default:
318 139
                return SQLT_CHR;
319
        }
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325 18
    public function closeCursor() : void
326
    {
327
        // not having the result means there's nothing to close
328 18
        if (! $this->result) {
329 3
            return;
330
        }
331
332 15
        oci_cancel($this->_sth);
333
334 15
        $this->result = false;
335 15
    }
336
337
    /**
338
     * {@inheritdoc}
339
     */
340 4
    public function columnCount() : int
341
    {
342 4
        return oci_num_fields($this->_sth) ?: 0;
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348 347
    public function execute(?array $params = null) : void
349
    {
350 347
        if ($params) {
351 93
            $hasZeroIndex = array_key_exists(0, $params);
352
353 93
            foreach ($params as $key => $val) {
354 93
                if ($hasZeroIndex && is_int($key)) {
355 93
                    $param = $key + 1;
356
                } else {
357
                    $param = $key;
358
                }
359
360 93
                $this->bindValue($param, $val);
361
            }
362
        }
363
364 343
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
365 343
        if (! $ret) {
366 147
            throw OCI8Exception::fromErrorInfo(oci_error($this->_sth));
367
        }
368
369 334
        $this->result = true;
370 334
    }
371
372
    /**
373
     * {@inheritdoc}
374
     */
375 308
    public function setFetchMode(int $fetchMode, ...$args) : void
376
    {
377 308
        $this->_defaultFetchMode = $fetchMode;
378 308
    }
379
380
    /**
381
     * {@inheritdoc}
382
     */
383 3
    public function getIterator()
384
    {
385 3
        return new StatementIterator($this);
386
    }
387
388
    /**
389
     * {@inheritdoc}
390
     */
391 78
    public function fetch(?int $fetchMode = null, ...$args)
392
    {
393
        // do not try fetching from the statement if it's not expected to contain result
394
        // in order to prevent exceptional situation
395 78
        if (! $this->result) {
396 3
            return false;
397
        }
398
399 75
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
400
401 75
        if ($fetchMode === FetchMode::COLUMN) {
402 1
            return $this->fetchColumn();
403
        }
404
405 74
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
406 1
            return oci_fetch_object($this->_sth);
407
        }
408
409 73
        if (! isset(self::$fetchModeMap[$fetchMode])) {
410
            throw new InvalidArgumentException(sprintf('Invalid fetch mode %d.', $fetchMode));
411
        }
412
413 73
        return oci_fetch_array(
414 73
            $this->_sth,
415 73
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
416
        );
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422 107
    public function fetchAll(?int $fetchMode = null, ...$args) : array
423
    {
424 107
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
425
426 107
        $result = [];
427
428 107
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
429 1
            while ($row = $this->fetch($fetchMode)) {
430 1
                $result[] = $row;
431
            }
432
433 1
            return $result;
434
        }
435
436 106
        if (! isset(self::$fetchModeMap[$fetchMode])) {
437
            throw new InvalidArgumentException(sprintf('Invalid fetch mode %d.', $fetchMode));
438
        }
439
440 106
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
441 1
            while ($row = $this->fetch($fetchMode)) {
442 1
                $result[] = $row;
443
            }
444
        } else {
445 105
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
446
447 105
            if ($fetchMode === FetchMode::COLUMN) {
448 6
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
449
            }
450
451
            // do not try fetching from the statement if it's not expected to contain result
452
            // in order to prevent exceptional situation
453 105
            if (! $this->result) {
454 3
                return [];
455
            }
456
457 102
            oci_fetch_all(
458 102
                $this->_sth,
459 102
                $result,
460 102
                0,
461 102
                -1,
462 102
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
463
            );
464
465 102
            if ($fetchMode === FetchMode::COLUMN) {
466 6
                $result = $result[0];
467
            }
468
        }
469
470 103
        return $result;
471
    }
472
473
    /**
474
     * {@inheritdoc}
475
     */
476 122
    public function fetchColumn(int $columnIndex = 0)
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 122
        if (! $this->result) {
481 3
            return false;
482
        }
483
484 119
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
485
486 119
        if ($row === false) {
487 3
            return false;
488
        }
489
490 116
        if (! array_key_exists($columnIndex, $row)) {
491 2
            throw InvalidColumnIndex::new($columnIndex, count($row));
492
        }
493
494 114
        return $row[$columnIndex];
495
    }
496
497
    /**
498
     * {@inheritdoc}
499
     */
500 172
    public function rowCount() : int
501
    {
502 172
        return oci_num_rows($this->_sth) ?: 0;
503
    }
504
}
505