Completed
Pull Request — master (#2776)
by Alessandro
04:59
created

OCI8Statement   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 449
Duplicated Lines 4.23 %

Coupling/Cohesion

Components 2
Dependencies 2

Test Coverage

Coverage 32.42%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 51
lcom 2
cbo 2
dl 19
loc 449
ccs 48
cts 148
cp 0.3242
rs 8.3206
c 2
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A bindValue() 0 4 1
A bindParam() 0 21 4
A closeCursor() 0 13 2
A columnCount() 0 4 1
A errorCode() 9 9 2
A errorInfo() 0 4 1
B execute() 10 22 6
A setFetchMode() 0 6 1
A getIterator() 0 6 1
B fetch() 0 23 5
D fetchAll() 0 44 10
A fetchColumn() 0 16 4
A rowCount() 0 4 1
B findPlaceholderOrOpeningQuote() 0 31 3
A findToken() 0 9 2
B convertPositionalToNamedPlaceholders() 0 33 4
A findClosingQuote() 0 20 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 PDO;
23
use IteratorAggregate;
24
use Doctrine\DBAL\Driver\Statement;
25
26
/**
27
 * The OCI8 implementation of the Statement interface.
28
 *
29
 * @since 2.0
30
 * @author Roman Borschel <[email protected]>
31
 */
32
class OCI8Statement implements \IteratorAggregate, Statement
33
{
34
    /**
35
     * @var resource
36
     */
37
    protected $_dbh;
38
39
    /**
40
     * @var resource
41
     */
42
    protected $_sth;
43
44
    /**
45
     * @var \Doctrine\DBAL\Driver\OCI8\OCI8Connection
46
     */
47
    protected $_conn;
48
49
    /**
50
     * @var string
51
     */
52
    protected static $_PARAM = ':param';
53
54
    /**
55
     * @var array
56
     */
57
    protected static $fetchModeMap = [
58
        PDO::FETCH_BOTH => OCI_BOTH,
59
        PDO::FETCH_ASSOC => OCI_ASSOC,
60
        PDO::FETCH_NUM => OCI_NUM,
61
        PDO::FETCH_COLUMN => OCI_NUM,
62
    ];
63
64
    /**
65
     * @var integer
66
     */
67
    protected $_defaultFetchMode = PDO::FETCH_BOTH;
68
69
    /**
70
     * @var array
71
     */
72
    protected $_paramMap = [];
73
74
    /**
75
     * Holds references to bound parameter values.
76
     *
77
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
78
     *
79
     * @var array
80
     */
81
    private $boundValues = [];
82
83
    /**
84
     * Indicates whether the statement is in the state when fetching results is possible
85
     *
86
     * @var bool
87
     */
88
    private $result = false;
89
90
    /**
91
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
92
     *
93
     * @param resource                                  $dbh       The connection handle.
94
     * @param string                                    $statement The SQL statement.
95
     * @param \Doctrine\DBAL\Driver\OCI8\OCI8Connection $conn
96
     */
97
    public function __construct($dbh, $statement, OCI8Connection $conn)
98
    {
99
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
100
        $this->_sth = oci_parse($dbh, $statement);
101
        $this->_dbh = $dbh;
102
        $this->_paramMap = $paramMap;
103
        $this->_conn = $conn;
104
    }
105
106
    /**
107
     * Converts positional (?) into named placeholders (:param<num>).
108
     *
109
     * Oracle does not support positional parameters, hence this method converts all
110
     * positional parameters into artificially named parameters. Note that this conversion
111
     * is not perfect. All question marks (?) in the original statement are treated as
112
     * placeholders and converted to a named parameter.
113
     *
114
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
115
     * Question marks inside literal strings are therefore handled correctly by this method.
116
     * This comes at a cost, the whole sql statement has to be looped over.
117
     *
118
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
119
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
120
     *
121
     * @param string $statement The SQL statement to convert.
122
     *
123
     * @return string
124
     * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
125
     */
126 10
    static public function convertPositionalToNamedPlaceholders($statement)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
127
    {
128 10
        $fragmentOffset = $tokenOffset = 0;
129 10
        $fragments = $paramMap = [];
130 10
        $currentLiteralDelimiter = null;
131
132
        do {
133 10
            if (!$currentLiteralDelimiter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $currentLiteralDelimiter of type string|null 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...
134 10
                $result = self::findPlaceholderOrOpeningQuote(
135 10
                    $statement,
136 10
                    $tokenOffset,
137 10
                    $fragmentOffset,
138 10
                    $fragments,
139 10
                    $currentLiteralDelimiter,
140 10
                    $paramMap
141
                );
142
            } else {
143 8
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
144
            }
145 10
        } while ($result);
146
147 10
        if ($currentLiteralDelimiter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $currentLiteralDelimiter of type string|null is loosely compared to true; 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...
148
            throw new OCI8Exception(sprintf(
149
                'The statement contains non-terminated string literal starting at offset %d',
150
                $tokenOffset - 1
151
            ));
152
        }
153
154 10
        $fragments[] = substr($statement, $fragmentOffset);
155 10
        $statement = implode('', $fragments);
156
157 10
        return [$statement, $paramMap];
158
    }
159
160
    /**
161
     * Finds next placeholder or opening quote.
162
     *
163
     * @param string $statement The SQL statement to parse
164
     * @param string $tokenOffset The offset to start searching from
165
     * @param int $fragmentOffset The offset to build the next fragment from
166
     * @param string[] $fragments Fragments of the original statement not containing placeholders
167
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
168
     *                                             or NULL if not currently in a literal
169
     * @param array<int, string> $paramMap Mapping of the original parameter positions to their named replacements
170
     * @return bool Whether the token was found
171
     */
172 10
    private static function findPlaceholderOrOpeningQuote(
173
        $statement,
174
        &$tokenOffset,
175
        &$fragmentOffset,
176
        &$fragments,
177
        &$currentLiteralDelimiter,
178
        &$paramMap
179
    ) {
180 10
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
181
182 10
        if (!$token) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type string|null 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...
183 10
            return false;
184
        }
185
186 10
        if ($token === '?') {
187 10
            $position = count($paramMap) + 1;
188 10
            $param = ':param' . $position;
189 10
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
190 10
            $fragments[] = $param;
191 10
            $paramMap[$position] = $param;
192 10
            $tokenOffset += 1;
193 10
            $fragmentOffset = $tokenOffset;
194
195 10
            return true;
196
        }
197
198 8
        $currentLiteralDelimiter = $token;
199 8
        ++$tokenOffset;
200
201 8
        return true;
202
    }
203
204
    /**
205
     * Finds closing quote
206
     *
207
     * @param string $statement The SQL statement to parse
208
     * @param string $tokenOffset The offset to start searching from
209
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
210
     *                                             or NULL if not currently in a literal
211
     * @return bool Whether the token was found
212
     */
213 8
    private static function findClosingQuote(
214
        $statement,
215
        &$tokenOffset,
216
        &$currentLiteralDelimiter
217
    ) {
218 8
        $token = self::findToken(
219 8
            $statement,
220 8
            $tokenOffset,
221 8
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
222
        );
223
224 8
        if (!$token) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type string|null 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...
225
            return false;
226
        }
227
228 8
        $currentLiteralDelimiter = false;
229 8
        ++$tokenOffset;
230
231 8
        return true;
232
    }
233
234
    /**
235
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
236
     * where the token was found.
237
     *
238
     * @param string $statement The SQL statement to parse
239
     * @param string $offset The offset to start searching from
240
     * @param string $regex The regex containing token pattern
241
     * @return string|null Token or NULL if not found
242
     */
243 10
    private static function findToken($statement, &$offset, $regex)
244
    {
245 10
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
246 10
            $offset = $matches[0][1];
247 10
            return $matches[0][0];
248
        }
249
250 10
        return null;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256
    public function bindValue($param, $value, $type = null)
257
    {
258
        return $this->bindParam($param, $value, $type, null);
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264
    public function bindParam($column, &$variable, $type = null, $length = null)
265
    {
266
        $column = isset($this->_paramMap[$column]) ? $this->_paramMap[$column] : $column;
267
268
        if ($type == \PDO::PARAM_LOB) {
269
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
270
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
271
272
            $this->boundValues[$column] =& $lob;
273
274
            return oci_bind_by_name($this->_sth, $column, $lob, -1, OCI_B_BLOB);
275
        } elseif ($length !== null) {
276
            $this->boundValues[$column] =& $variable;
277
278
            return oci_bind_by_name($this->_sth, $column, $variable, $length);
279
        }
280
281
        $this->boundValues[$column] =& $variable;
282
283
        return oci_bind_by_name($this->_sth, $column, $variable);
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289
    public function closeCursor()
290
    {
291
        // not having the result means there's nothing to close
292
        if (!$this->result) {
293
            return true;
294
        }
295
296
        oci_cancel($this->_sth);
297
298
        $this->result = false;
299
300
        return true;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function columnCount()
307
    {
308
        return oci_num_fields($this->_sth);
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314 View Code Duplication
    public function errorCode()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
315
    {
316
        $error = oci_error($this->_sth);
317
        if ($error !== false) {
318
            $error = $error['code'];
319
        }
320
321
        return $error;
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     */
327
    public function errorInfo()
328
    {
329
        return oci_error($this->_sth);
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335
    public function execute($params = null)
336
    {
337 View Code Duplication
        if ($params) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
338
            $hasZeroIndex = array_key_exists(0, $params);
339
            foreach ($params as $key => $val) {
340
                if ($hasZeroIndex && is_numeric($key)) {
341
                    $this->bindValue($key + 1, $val);
342
                } else {
343
                    $this->bindValue($key, $val);
344
                }
345
            }
346
        }
347
348
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
349
        if ( ! $ret) {
350
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
351
        }
352
353
        $this->result = true;
354
355
        return $ret;
356
    }
357
358
    /**
359
     * {@inheritdoc}
360
     */
361
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
362
    {
363
        $this->_defaultFetchMode = $fetchMode;
364
365
        return true;
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371
    public function getIterator()
372
    {
373
        $data = $this->fetchAll();
374
375
        return new \ArrayIterator($data);
376
    }
377
378
    /**
379
     * {@inheritdoc}
380
     */
381
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
382
    {
383
        // do not try fetching from the statement if it's not expected to contain result
384
        // in order to prevent exceptional situation
385
        if (!$this->result) {
386
            return false;
387
        }
388
389
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
390
391
        if (PDO::FETCH_OBJ == $fetchMode) {
392
            return oci_fetch_object($this->_sth);
393
        }
394
395
        if (! isset(self::$fetchModeMap[$fetchMode])) {
396
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
397
        }
398
399
        return oci_fetch_array(
400
            $this->_sth,
401
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
402
        );
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     */
408
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
409
    {
410
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
411
412
        $result = [];
413
414
        if (PDO::FETCH_OBJ == $fetchMode) {
415
            while ($row = $this->fetch($fetchMode)) {
416
                $result[] = $row;
417
            }
418
419
            return $result;
420
        }
421
422
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
423
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
424
        }
425
426
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
427
            while ($row = $this->fetch($fetchMode)) {
428
                $result[] = $row;
429
            }
430
        } else {
431
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
432
            if ($fetchMode == PDO::FETCH_COLUMN) {
433
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
434
            }
435
436
            // do not try fetching from the statement if it's not expected to contain result
437
            // in order to prevent exceptional situation
438
            if (!$this->result) {
439
                return [];
440
            }
441
442
            oci_fetch_all($this->_sth, $result, 0, -1,
443
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
444
445
            if ($fetchMode == PDO::FETCH_COLUMN) {
446
                $result = $result[0];
447
            }
448
        }
449
450
        return $result;
451
    }
452
453
    /**
454
     * {@inheritdoc}
455
     */
456
    public function fetchColumn($columnIndex = 0)
457
    {
458
        // do not try fetching from the statement if it's not expected to contain result
459
        // in order to prevent exceptional situation
460
        if (!$this->result) {
461
            return false;
462
        }
463
464
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
465
466
        if (false === $row) {
467
            return false;
468
        }
469
470
        return isset($row[$columnIndex]) ? $row[$columnIndex] : null;
471
    }
472
473
    /**
474
     * {@inheritdoc}
475
     */
476
    public function rowCount()
477
    {
478
        return oci_num_rows($this->_sth);
479
    }
480
}
481