Passed
Pull Request — master (#3149)
by Sergei
15:02
created

OCI8Statement   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Test Coverage

Coverage 94.27%

Importance

Changes 0
Metric Value
wmc 52
dl 0
loc 468
ccs 148
cts 157
cp 0.9427
rs 7.9487
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
B convertPositionalToNamedPlaceholders() 0 32 4
A findToken() 0 8 2
A findClosingQuote() 0 19 2
A bindValue() 0 3 1
B findPlaceholderOrOpeningQuote() 0 30 3
A rowCount() 0 3 1
A errorInfo() 0 3 1
A columnCount() 0 3 1
A convertParamType() 0 12 3
A fetchColumn() 0 15 3
D fetchAll() 0 44 10
A closeCursor() 0 12 2
A setFetchMode() 0 5 1
A getIterator() 0 3 1
A errorCode() 0 8 2
B execute() 0 21 6
B fetch() 0 25 6
A bindParam() 0 19 2

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

177
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
178
            }
179 451
        } while ($result);
180
181 451
        if ($currentLiteralDelimiter) {
182 3
            throw new OCI8Exception(sprintf(
183 3
                'The statement contains non-terminated string literal starting at offset %d',
184 3
                $tokenOffset - 1
185
            ));
186
        }
187
188 448
        $fragments[] = substr($statement, $fragmentOffset);
189 448
        $statement = implode('', $fragments);
190
191 448
        return [$statement, $paramMap];
192
    }
193
194
    /**
195
     * Finds next placeholder or opening quote.
196
     *
197
     * @param string             $statement               The SQL statement to parse
198
     * @param string             $tokenOffset             The offset to start searching from
199
     * @param int                $fragmentOffset          The offset to build the next fragment from
200
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
201
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
202
     *                                                    or NULL if not currently in a literal
203
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
204
     * @return bool Whether the token was found
205
     */
206 451
    private static function findPlaceholderOrOpeningQuote(
207
        $statement,
208
        &$tokenOffset,
209
        &$fragmentOffset,
210
        &$fragments,
211
        &$currentLiteralDelimiter,
212
        &$paramMap
213
    ) {
214 451
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
215
216 451
        if (!$token) {
217 448
            return false;
218
        }
219
220 416
        if ($token === '?') {
221 313
            $position = count($paramMap) + 1;
222 313
            $param = ':param' . $position;
223 313
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
224 313
            $fragments[] = $param;
225 313
            $paramMap[$position] = $param;
226 313
            $tokenOffset += 1;
227 313
            $fragmentOffset = $tokenOffset;
228
229 313
            return true;
230
        }
231
232 269
        $currentLiteralDelimiter = $token;
233 269
        ++$tokenOffset;
234
235 269
        return true;
236
    }
237
238
    /**
239
     * Finds closing quote
240
     *
241
     * @param string      $statement               The SQL statement to parse
242
     * @param string      $tokenOffset             The offset to start searching from
243
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
244
     *                                             or NULL if not currently in a literal
245
     * @return bool Whether the token was found
246
     */
247 269
    private static function findClosingQuote(
248
        $statement,
249
        &$tokenOffset,
250
        &$currentLiteralDelimiter
251
    ) {
252 269
        $token = self::findToken(
253 269
            $statement,
254 269
            $tokenOffset,
255 269
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
256
        );
257
258 269
        if (!$token) {
259 3
            return false;
260
        }
261
262 267
        $currentLiteralDelimiter = false;
263 267
        ++$tokenOffset;
264
265 267
        return true;
266
    }
267
268
    /**
269
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
270
     * where the token was found.
271
     *
272
     * @param string $statement The SQL statement to parse
273
     * @param string $offset    The offset to start searching from
274
     * @param string $regex     The regex containing token pattern
275
     * @return string|null Token or NULL if not found
276
     */
277 451
    private static function findToken($statement, &$offset, $regex)
278
    {
279 451
        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

279
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, /** @scrutinizer ignore-type */ $offset)) {
Loading history...
280 416
            $offset = $matches[0][1];
281 416
            return $matches[0][0];
282
        }
283
284 451
        return null;
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290 118
    public function bindValue($param, $value, $type = ParameterType::STRING)
291
    {
292 118
        return $this->bindParam($param, $value, $type, null);
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298 123
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
299
    {
300 123
        $column = $this->_paramMap[$column] ?? $column;
301
302 123
        if ($type === ParameterType::LARGE_OBJECT) {
303 4
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
304 4
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
305
306 4
            $variable =& $lob;
307
        }
308
309 123
        $this->boundValues[$column] =& $variable;
310
311 123
        return oci_bind_by_name(
312 123
            $this->_sth,
313 123
            $column,
314 123
            $variable,
315 123
            $length ?? -1,
316 123
            $this->convertParamType($type)
317
        );
318
    }
319
320
    /**
321
     * Converts DBAL parameter type to oci8 parameter type
322
     */
323 123
    private function convertParamType(int $type) : int
324
    {
325 123
        switch ($type) {
326
            case ParameterType::BINARY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
327 1
                return OCI_B_BIN;
328
329
            case ParameterType::LARGE_OBJECT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
330 4
                return OCI_B_BLOB;
331
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
332
333
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
334 121
                return SQLT_CHR;
335
        }
336
    }
337
338
    /**
339
     * {@inheritdoc}
340
     */
341 19
    public function closeCursor()
342
    {
343
        // not having the result means there's nothing to close
344 19
        if (!$this->result) {
345 4
            return true;
346
        }
347
348 15
        oci_cancel($this->_sth);
349
350 15
        $this->result = false;
351
352 15
        return true;
353
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358 4
    public function columnCount()
359
    {
360 4
        return oci_num_fields($this->_sth);
361
    }
362
363
    /**
364
     * {@inheritdoc}
365
     */
366
    public function errorCode()
367
    {
368
        $error = oci_error($this->_sth);
369
        if ($error !== false) {
370
            $error = $error['code'];
371
        }
372
373
        return $error;
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     */
379 139
    public function errorInfo()
380
    {
381 139
        return oci_error($this->_sth);
382
    }
383
384
    /**
385
     * {@inheritdoc}
386
     */
387 259
    public function execute($params = null)
388
    {
389 259
        if ($params) {
390 91
            $hasZeroIndex = array_key_exists(0, $params);
391 91
            foreach ($params as $key => $val) {
392 91
                if ($hasZeroIndex && is_numeric($key)) {
393 91
                    $this->bindValue($key + 1, $val);
394
                } else {
395 91
                    $this->bindValue($key, $val);
396
                }
397
            }
398
        }
399
400 255
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
401 255
        if ( ! $ret) {
402 141
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
403
        }
404
405 252
        $this->result = true;
406
407 252
        return $ret;
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413 225
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
414
    {
415 225
        $this->_defaultFetchMode = $fetchMode;
416
417 225
        return true;
418
    }
419
420
    /**
421
     * {@inheritdoc}
422
     */
423 3
    public function getIterator()
424
    {
425 3
        return new StatementIterator($this);
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431 78
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
432
    {
433
        // do not try fetching from the statement if it's not expected to contain result
434
        // in order to prevent exceptional situation
435 78
        if (!$this->result) {
436 3
            return false;
437
        }
438
439 75
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
440
441 75
        if ($fetchMode === FetchMode::COLUMN) {
442 1
            return $this->fetchColumn();
443
        }
444
445 74
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
446 1
            return oci_fetch_object($this->_sth);
447
        }
448
449 73
        if (! isset(self::$fetchModeMap[$fetchMode])) {
450
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
451
        }
452
453 73
        return oci_fetch_array(
454 73
            $this->_sth,
455 73
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
456
        );
457
    }
458
459
    /**
460
     * {@inheritdoc}
461
     */
462 96
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
463
    {
464 96
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
465
466 96
        $result = [];
467
468 96
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
469 1
            while ($row = $this->fetch($fetchMode)) {
470 1
                $result[] = $row;
471
            }
472
473 1
            return $result;
474
        }
475
476 95
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
477
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
478
        }
479
480 95
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
481 1
            while ($row = $this->fetch($fetchMode)) {
482 1
                $result[] = $row;
483
            }
484
        } else {
485 94
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
486
487 94
            if ($fetchMode === FetchMode::COLUMN) {
488 4
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
489
            }
490
491
            // do not try fetching from the statement if it's not expected to contain result
492
            // in order to prevent exceptional situation
493 94
            if (!$this->result) {
494 3
                return [];
495
            }
496
497 91
            oci_fetch_all($this->_sth, $result, 0, -1,
498 91
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
499
500 91
            if ($fetchMode === FetchMode::COLUMN) {
501 4
                $result = $result[0];
502
            }
503
        }
504
505 92
        return $result;
506
    }
507
508
    /**
509
     * {@inheritdoc}
510
     */
511 51
    public function fetchColumn($columnIndex = 0)
512
    {
513
        // do not try fetching from the statement if it's not expected to contain result
514
        // in order to prevent exceptional situation
515 51
        if (!$this->result) {
516 3
            return false;
517
        }
518
519 48
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
520
521 48
        if (false === $row) {
522 3
            return false;
523
        }
524
525 45
        return $row[$columnIndex] ?? null;
526
    }
527
528
    /**
529
     * {@inheritdoc}
530
     */
531 174
    public function rowCount()
532
    {
533 174
        return oci_num_rows($this->_sth);
534
    }
535
}
536