Completed
Pull Request — master (#3738)
by
unknown
66:03
created

OCI8Statement::rowCount()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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