Completed
Push — master ( cc3868...bfc8bb )
by Marco
21s queued 15s
created

OCI8Statement::fetchColumn()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 19
ccs 9
cts 9
cp 1
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 1
crap 4
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
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
95
     *
96
     * @param resource $dbh   The connection handle.
97
     * @param string   $query The SQL query.
98
     */
99 356
    public function __construct($dbh, string $query, OCI8Connection $conn)
100
    {
101 356
        [$query, $paramMap] = self::convertPositionalToNamedPlaceholders($query);
102
103 356
        $stmt = oci_parse($dbh, $query);
104 356
        assert(is_resource($stmt));
105
106 356
        $this->_sth      = $stmt;
107 356
        $this->_dbh      = $dbh;
108 356
        $this->_paramMap = $paramMap;
109 356
        $this->_conn     = $conn;
110 356
    }
111
112
    /**
113
     * Converts positional (?) into named placeholders (:param<num>).
114
     *
115
     * Oracle does not support positional parameters, hence this method converts all
116
     * positional parameters into artificially named parameters. Note that this conversion
117
     * is not perfect. All question marks (?) in the original statement are treated as
118
     * placeholders and converted to a named parameter.
119
     *
120
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
121
     * Question marks inside literal strings are therefore handled correctly by this method.
122
     * This comes at a cost, the whole sql statement has to be looped over.
123
     *
124
     * @param string $statement The SQL statement to convert.
125
     *
126
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
127
     *
128
     * @throws OCI8Exception
129
     *
130
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
131
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
132
     */
133 622
    public static function convertPositionalToNamedPlaceholders(string $statement) : array
134
    {
135 622
        $fragmentOffset          = $tokenOffset = 0;
136 622
        $fragments               = $paramMap = [];
137 622
        $currentLiteralDelimiter = null;
138
139
        do {
140 622
            if ($currentLiteralDelimiter === null) {
141 622
                $result = self::findPlaceholderOrOpeningQuote(
142 622
                    $statement,
143
                    $tokenOffset,
144
                    $fragmentOffset,
145
                    $fragments,
146
                    $currentLiteralDelimiter,
147
                    $paramMap
148
                );
149
            } else {
150 384
                $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

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