Passed
Pull Request — master (#2718)
by Ian
08:35
created

OCI8Statement::fetch()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 0
cts 11
cp 0
rs 8.5906
c 0
b 0
f 0
cc 5
eloc 11
nc 7
nop 3
crap 30
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 IteratorAggregate;
25
use PDO;
26
27
/**
28
 * The OCI8 implementation of the Statement interface.
29
 *
30
 * @since 2.0
31
 * @author Roman Borschel <[email protected]>
32
 */
33
class OCI8Statement implements IteratorAggregate, Statement
34
{
35
    /**
36
     * @var resource
37
     */
38
    protected $_dbh;
39
40
    /**
41
     * @var resource
42
     */
43
    protected $_sth;
44
45
    /**
46
     * @var \Doctrine\DBAL\Driver\OCI8\OCI8Connection
47
     */
48
    protected $_conn;
49
50
    /**
51
     * @var string
52
     */
53
    protected static $_PARAM = ':param';
54
55
    /**
56
     * @var array
57
     */
58
    protected static $fetchModeMap = [
59
        PDO::FETCH_BOTH => OCI_BOTH,
60
        PDO::FETCH_ASSOC => OCI_ASSOC,
61
        PDO::FETCH_NUM => OCI_NUM,
62
        PDO::FETCH_COLUMN => OCI_NUM,
63
    ];
64
65
    /**
66
     * @var integer
67
     */
68
    protected $_defaultFetchMode = PDO::FETCH_BOTH;
69
70
    /**
71
     * @var array
72
     */
73
    protected $_paramMap = [];
74
75
    /**
76
     * Holds references to bound parameter values.
77
     *
78
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
79
     *
80
     * @var array
81
     */
82
    private $boundValues = [];
83
84
    /**
85
     * Indicates whether the statement is in the state when fetching results is possible
86
     *
87
     * @var bool
88
     */
89
    private $result = false;
90
91
    /**
92
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
93
     *
94
     * @param resource                                  $dbh       The connection handle.
95
     * @param string                                    $statement The SQL statement.
96
     * @param \Doctrine\DBAL\Driver\OCI8\OCI8Connection $conn
97
     */
98
    public function __construct($dbh, $statement, OCI8Connection $conn)
99
    {
100
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
101
        $this->_sth = oci_parse($dbh, $statement);
102
        $this->_dbh = $dbh;
103
        $this->_paramMap = $paramMap;
104
        $this->_conn = $conn;
105
    }
106
107
    /**
108
     * Converts positional (?) into named placeholders (:param<num>).
109
     *
110
     * Oracle does not support positional parameters, hence this method converts all
111
     * positional parameters into artificially named parameters. Note that this conversion
112
     * is not perfect. All question marks (?) in the original statement are treated as
113
     * placeholders and converted to a named parameter.
114
     *
115
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
116
     * Question marks inside literal strings are therefore handled correctly by this method.
117
     * This comes at a cost, the whole sql statement has to be looped over.
118
     *
119
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
120
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
121
     *
122
     * @param string $statement The SQL statement to convert.
123
     *
124
     * @return array [0] => the statement value (string), [1] => the paramMap value (array).
125
     * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
126
     */
127 10
    public static function convertPositionalToNamedPlaceholders($statement)
128
    {
129 10
        $fragmentOffset = $tokenOffset = 0;
130 10
        $fragments = $paramMap = [];
131 10
        $currentLiteralDelimiter = null;
132
133
        do {
134 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...
135 10
                $result = self::findPlaceholderOrOpeningQuote(
136 10
                    $statement,
137 10
                    $tokenOffset,
138 10
                    $fragmentOffset,
139 10
                    $fragments,
140 10
                    $currentLiteralDelimiter,
141 10
                    $paramMap
142
                );
143
            } else {
144 8
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
145
            }
146 10
        } while ($result);
147
148 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...
149
            throw new OCI8Exception(sprintf(
150
                'The statement contains non-terminated string literal starting at offset %d',
151
                $tokenOffset - 1
152
            ));
153
        }
154
155 10
        $fragments[] = substr($statement, $fragmentOffset);
156 10
        $statement = implode('', $fragments);
157
158 10
        return [$statement, $paramMap];
159
    }
160
161
    /**
162
     * Finds next placeholder or opening quote.
163
     *
164
     * @param string $statement The SQL statement to parse
165
     * @param string $tokenOffset The offset to start searching from
166
     * @param int $fragmentOffset The offset to build the next fragment from
167
     * @param string[] $fragments Fragments of the original statement not containing placeholders
168
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
169
     *                                             or NULL if not currently in a literal
170
     * @param array<int, string> $paramMap Mapping of the original parameter positions to their named replacements
171
     * @return bool Whether the token was found
172
     */
173 10
    private static function findPlaceholderOrOpeningQuote(
174
        $statement,
175
        &$tokenOffset,
176
        &$fragmentOffset,
177
        &$fragments,
178
        &$currentLiteralDelimiter,
179
        &$paramMap
180
    ) {
181 10
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
182
183 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...
184 10
            return false;
185
        }
186
187 10
        if ($token === '?') {
188 10
            $position = count($paramMap) + 1;
189 10
            $param = ':param' . $position;
190 10
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
191 10
            $fragments[] = $param;
192 10
            $paramMap[$position] = $param;
193 10
            $tokenOffset += 1;
194 10
            $fragmentOffset = $tokenOffset;
195
196 10
            return true;
197
        }
198
199 8
        $currentLiteralDelimiter = $token;
200 8
        ++$tokenOffset;
201
202 8
        return true;
203
    }
204
205
    /**
206
     * Finds closing quote
207
     *
208
     * @param string $statement The SQL statement to parse
209
     * @param string $tokenOffset The offset to start searching from
210
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
211
     *                                             or NULL if not currently in a literal
212
     * @return bool Whether the token was found
213
     */
214 8
    private static function findClosingQuote(
215
        $statement,
216
        &$tokenOffset,
217
        &$currentLiteralDelimiter
218
    ) {
219 8
        $token = self::findToken(
220 8
            $statement,
221 8
            $tokenOffset,
222 8
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
223
        );
224
225 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...
226
            return false;
227
        }
228
229 8
        $currentLiteralDelimiter = false;
230 8
        ++$tokenOffset;
231
232 8
        return true;
233
    }
234
235
    /**
236
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
237
     * where the token was found.
238
     *
239
     * @param string $statement The SQL statement to parse
240
     * @param string $offset The offset to start searching from
241
     * @param string $regex The regex containing token pattern
242
     * @return string|null Token or NULL if not found
243
     */
244 10
    private static function findToken($statement, &$offset, $regex)
245
    {
246 10
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
247 10
            $offset = $matches[0][1];
248 10
            return $matches[0][0];
249
        }
250
251 10
        return null;
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257
    public function bindValue($param, $value, $type = null)
258
    {
259
        return $this->bindParam($param, $value, $type, null);
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265
    public function bindParam($column, &$variable, $type = null, $length = null)
266
    {
267
        $column = isset($this->_paramMap[$column]) ? $this->_paramMap[$column] : $column;
268
269
        if ($type == \PDO::PARAM_LOB) {
270
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
271
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
272
273
            $this->boundValues[$column] =& $lob;
274
275
            return oci_bind_by_name($this->_sth, $column, $lob, -1, OCI_B_BLOB);
276
        } elseif ($length !== null) {
277
            $this->boundValues[$column] =& $variable;
278
279
            return oci_bind_by_name($this->_sth, $column, $variable, $length);
280
        }
281
282
        $this->boundValues[$column] =& $variable;
283
284
        return oci_bind_by_name($this->_sth, $column, $variable);
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290
    public function closeCursor()
291
    {
292
        // not having the result means there's nothing to close
293
        if (!$this->result) {
294
            return true;
295
        }
296
297
        oci_cancel($this->_sth);
298
299
        $this->result = false;
300
301
        return true;
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    public function columnCount()
308
    {
309
        return oci_num_fields($this->_sth);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315 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...
316
    {
317
        $error = oci_error($this->_sth);
318
        if ($error !== false) {
319
            $error = $error['code'];
320
        }
321
322
        return $error;
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328
    public function errorInfo()
329
    {
330
        return oci_error($this->_sth);
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336
    public function execute($params = null)
337
    {
338 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...
339
            $hasZeroIndex = array_key_exists(0, $params);
340
            foreach ($params as $key => $val) {
341
                if ($hasZeroIndex && is_numeric($key)) {
342
                    $this->bindValue($key + 1, $val);
343
                } else {
344
                    $this->bindValue($key, $val);
345
                }
346
            }
347
        }
348
349
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
350
        if ( ! $ret) {
351
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
352
        }
353
354
        $this->result = true;
355
356
        return $ret;
357
    }
358
359
    /**
360
     * {@inheritdoc}
361
     */
362
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
363
    {
364
        $this->_defaultFetchMode = $fetchMode;
365
366
        return true;
367
    }
368
369
    /**
370
     * {@inheritdoc}
371
     */
372
    public function getIterator()
373
    {
374
        return new StatementIterator($this);
375
    }
376
377
    /**
378
     * {@inheritdoc}
379
     */
380
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
381
    {
382
        // do not try fetching from the statement if it's not expected to contain result
383
        // in order to prevent exceptional situation
384
        if (!$this->result) {
385
            return false;
386
        }
387
388
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
389
390
        if (PDO::FETCH_OBJ == $fetchMode) {
391
            return oci_fetch_object($this->_sth);
392
        }
393
394
        if (! isset(self::$fetchModeMap[$fetchMode])) {
395
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
396
        }
397
398
        return oci_fetch_array(
399
            $this->_sth,
400
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
401
        );
402
    }
403
404
    /**
405
     * {@inheritdoc}
406
     */
407
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
408
    {
409
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
410
411
        $result = [];
412
413
        if (PDO::FETCH_OBJ == $fetchMode) {
414
            while ($row = $this->fetch($fetchMode)) {
415
                $result[] = $row;
416
            }
417
418
            return $result;
419
        }
420
421
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
422
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
423
        }
424
425
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
426
            while ($row = $this->fetch($fetchMode)) {
427
                $result[] = $row;
428
            }
429
        } else {
430
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
431
            if ($fetchMode == PDO::FETCH_COLUMN) {
432
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
433
            }
434
435
            // do not try fetching from the statement if it's not expected to contain result
436
            // in order to prevent exceptional situation
437
            if (!$this->result) {
438
                return [];
439
            }
440
441
            oci_fetch_all($this->_sth, $result, 0, -1,
442
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
443
444
            if ($fetchMode == PDO::FETCH_COLUMN) {
445
                $result = $result[0];
446
            }
447
        }
448
449
        return $result;
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455
    public function fetchColumn($columnIndex = 0)
456
    {
457
        // do not try fetching from the statement if it's not expected to contain result
458
        // in order to prevent exceptional situation
459
        if (!$this->result) {
460
            return false;
461
        }
462
463
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
464
465
        if (false === $row) {
466
            return false;
467
        }
468
469
        return isset($row[$columnIndex]) ? $row[$columnIndex] : null;
470
    }
471
472
    /**
473
     * {@inheritdoc}
474
     */
475
    public function rowCount()
476
    {
477
        return oci_num_rows($this->_sth);
478
    }
479
}
480