Failed Conditions
Pull Request — master (#3359)
by Sergei
72:35 queued 69:45
created

OCI8Statement::bindValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
1
<?php
2
3
namespace Doctrine\DBAL\Driver\OCI8;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Driver\Statement;
7
use Doctrine\DBAL\Driver\StatementIterator;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\ParameterType;
10
use InvalidArgumentException;
11
use IteratorAggregate;
12
use PDO;
13
use const OCI_ASSOC;
14
use const OCI_B_BIN;
15
use const OCI_B_BLOB;
16
use const OCI_BOTH;
17
use const OCI_D_LOB;
18
use const OCI_FETCHSTATEMENT_BY_COLUMN;
19
use const OCI_FETCHSTATEMENT_BY_ROW;
20
use const OCI_NUM;
21
use const OCI_RETURN_LOBS;
22
use const OCI_RETURN_NULLS;
23
use const OCI_TEMP_BLOB;
24
use const PREG_OFFSET_CAPTURE;
25
use const SQLT_CHR;
26
use function array_key_exists;
27
use function count;
28
use function implode;
29
use function is_numeric;
30
use function oci_bind_by_name;
31
use function oci_cancel;
32
use function oci_error;
33
use function oci_execute;
34
use function oci_fetch_all;
35
use function oci_fetch_array;
36
use function oci_fetch_object;
37
use function oci_new_descriptor;
38
use function oci_num_fields;
39
use function oci_num_rows;
40
use function oci_parse;
41
use function preg_match;
42
use function preg_quote;
43
use function sprintf;
44
use function substr;
45
46
/**
47
 * The OCI8 implementation of the Statement interface.
48
 */
49
class OCI8Statement implements IteratorAggregate, Statement
50
{
51
    /** @var resource */
52
    protected $_dbh;
53
54
    /** @var resource */
55
    protected $_sth;
56
57
    /** @var OCI8Connection */
58
    protected $_conn;
59
60
    /** @var string */
61
    protected static $_PARAM = ':param';
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   $statement The SQL statement.
98 261
     */
99
    public function __construct($dbh, $statement, OCI8Connection $conn)
100 261
    {
101 261
        [$statement, $paramMap] = self::convertPositionalToNamedPlaceholders($statement);
102 261
        $this->_sth             = oci_parse($dbh, $statement);
103 261
        $this->_dbh             = $dbh;
104 261
        $this->_paramMap        = $paramMap;
105 261
        $this->_conn            = $conn;
106
    }
107
108
    /**
109
     * Converts positional (?) into named placeholders (:param<num>).
110
     *
111
     * Oracle does not support positional parameters, hence this method converts all
112
     * positional parameters into artificially named parameters. Note that this conversion
113
     * is not perfect. All question marks (?) in the original statement are treated as
114
     * placeholders and converted to a named parameter.
115
     *
116
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
117
     * Question marks inside literal strings are therefore handled correctly by this method.
118
     * This comes at a cost, the whole sql statement has to be looped over.
119
     *
120
     * @param string $statement The SQL statement to convert.
121
     *
122
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
123
     *
124
     * @throws OCI8Exception
125
     *
126
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
127
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
128 504
     */
129
    public static function convertPositionalToNamedPlaceholders($statement)
130 504
    {
131 504
        $fragmentOffset          = $tokenOffset = 0;
132 504
        $fragments               = $paramMap = [];
133
        $currentLiteralDelimiter = null;
134
135 504
        do {
136 504
            if (! $currentLiteralDelimiter) {
137 504
                $result = self::findPlaceholderOrOpeningQuote(
138 504
                    $statement,
139 504
                    $tokenOffset,
140 504
                    $fragmentOffset,
141 504
                    $fragments,
142 504
                    $currentLiteralDelimiter,
143
                    $paramMap
144
                );
145 312
            } else {
146
                $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

146
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
147 504
            }
148
        } while ($result);
149 504
150 3
        if ($currentLiteralDelimiter) {
151 3
            throw new OCI8Exception(sprintf(
152 3
                'The statement contains non-terminated string literal starting at offset %d',
153
                $tokenOffset - 1
154
            ));
155
        }
156 501
157 501
        $fragments[] = substr($statement, $fragmentOffset);
158
        $statement   = implode('', $fragments);
159 501
160
        return [$statement, $paramMap];
161
    }
162
163
    /**
164
     * Finds next placeholder or opening quote.
165
     *
166
     * @param string             $statement               The SQL statement to parse
167
     * @param string             $tokenOffset             The offset to start searching from
168
     * @param int                $fragmentOffset          The offset to build the next fragment from
169
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
170
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
171
     *                                                    or NULL if not currently in a literal
172
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
173
     *
174
     * @return bool Whether the token was found
175 504
     */
176
    private static function findPlaceholderOrOpeningQuote(
177
        $statement,
178
        &$tokenOffset,
179
        &$fragmentOffset,
180
        &$fragments,
181
        &$currentLiteralDelimiter,
182
        &$paramMap
183 504
    ) {
184
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
185 504
186 501
        if (! $token) {
187
            return false;
188
        }
189 469
190 363
        if ($token === '?') {
191 363
            $position            = count($paramMap) + 1;
192 363
            $param               = ':param' . $position;
193 363
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
194 363
            $fragments[]         = $param;
195 363
            $paramMap[$position] = $param;
196 363
            $tokenOffset        += 1;
197
            $fragmentOffset      = $tokenOffset;
198 363
199
            return true;
200
        }
201 312
202 312
        $currentLiteralDelimiter = $token;
203
        ++$tokenOffset;
204 312
205
        return true;
206
    }
207
208
    /**
209
     * Finds closing quote
210
     *
211
     * @param string      $statement               The SQL statement to parse
212
     * @param string      $tokenOffset             The offset to start searching from
213
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
214
     *                                             or NULL if not currently in a literal
215
     *
216
     * @return bool Whether the token was found
217 312
     */
218
    private static function findClosingQuote(
219
        $statement,
220
        &$tokenOffset,
221
        &$currentLiteralDelimiter
222 312
    ) {
223 312
        $token = self::findToken(
224 312
            $statement,
225 312
            $tokenOffset,
226
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
227
        );
228 312
229 3
        if (! $token) {
230
            return false;
231
        }
232 310
233 310
        $currentLiteralDelimiter = false;
234
        ++$tokenOffset;
235 310
236
        return true;
237
    }
238
239
    /**
240
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
241
     * where the token was found.
242
     *
243
     * @param string $statement The SQL statement to parse
244
     * @param string $offset    The offset to start searching from
245
     * @param string $regex     The regex containing token pattern
246
     *
247
     * @return string|null Token or NULL if not found
248 504
     */
249
    private static function findToken($statement, &$offset, $regex)
250 504
    {
251 469
        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

251
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, /** @scrutinizer ignore-type */ $offset)) {
Loading history...
252 469
            $offset = $matches[0][1];
253
            return $matches[0][0];
254
        }
255 504
256
        return null;
257
    }
258
259
    /**
260
     * {@inheritdoc}
261 118
     */
262
    public function bindValue($param, $value, $type = ParameterType::STRING)
263 118
    {
264
        return $this->bindParam($param, $value, $type, null);
265
    }
266
267
    /**
268
     * {@inheritdoc}
269 123
     */
270
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
271 123
    {
272
        $column = $this->_paramMap[$column] ?? $column;
273 123
274 4
        if ($type === ParameterType::LARGE_OBJECT) {
275 4
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
276
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
277 4
278
            $variable =& $lob;
279
        }
280 123
281
        $this->boundValues[$column] =& $variable;
282 123
283 123
        return oci_bind_by_name(
284 123
            $this->_sth,
285 123
            $column,
286 123
            $variable,
287 123
            $length ?? -1,
288
            $this->convertParameterType($type)
289
        );
290
    }
291
292
    /**
293
     * Converts DBAL parameter type to oci8 parameter type
294 123
     */
295
    private function convertParameterType(int $type) : int
296 123
    {
297
        switch ($type) {
298 1
            case ParameterType::BINARY:
299
                return OCI_B_BIN;
300
301 4
            case ParameterType::LARGE_OBJECT:
302
                return OCI_B_BLOB;
303
304 121
            default:
305
                return SQLT_CHR;
306
        }
307
    }
308
309
    /**
310
     * {@inheritdoc}
311 19
     */
312
    public function closeCursor()
313
    {
314 19
        // not having the result means there's nothing to close
315 4
        if (! $this->result) {
316
            return true;
317
        }
318 15
319
        oci_cancel($this->_sth);
320 15
321
        $this->result = false;
322 15
323
        return true;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328 4
     */
329
    public function columnCount()
330 4
    {
331
        return oci_num_fields($this->_sth);
332
    }
333
334
    /**
335
     * {@inheritdoc}
336
     */
337
    public function errorCode()
338
    {
339
        $error = oci_error($this->_sth);
340
        if ($error !== false) {
341
            $error = $error['code'];
342
        }
343
344
        return $error;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349 139
     */
350
    public function errorInfo()
351 139
    {
352
        return oci_error($this->_sth);
353
    }
354
355
    /**
356
     * {@inheritdoc}
357 262
     */
358
    public function execute($params = null)
359 262
    {
360 91
        if ($params) {
361 91
            $hasZeroIndex = array_key_exists(0, $params);
362 91
            foreach ($params as $key => $val) {
363 91
                if ($hasZeroIndex && is_numeric($key)) {
364
                    $this->bindValue($key + 1, $val);
365 91
                } else {
366
                    $this->bindValue($key, $val);
367
                }
368
            }
369
        }
370 258
371 258
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
372 141
        if (! $ret) {
373
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
374
        }
375 255
376
        $this->result = true;
377 255
378
        return $ret;
379
    }
380
381
    /**
382
     * {@inheritdoc}
383 228
     */
384
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
385 228
    {
386
        $this->_defaultFetchMode = $fetchMode;
387 228
388
        return true;
389
    }
390
391
    /**
392
     * {@inheritdoc}
393 3
     */
394
    public function getIterator()
395 3
    {
396
        return new StatementIterator($this);
397
    }
398
399
    /**
400
     * {@inheritdoc}
401 78
     */
402
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
403
    {
404
        // do not try fetching from the statement if it's not expected to contain result
405 78
        // in order to prevent exceptional situation
406 3
        if (! $this->result) {
407
            return false;
408
        }
409 75
410
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
411 75
412 1
        if ($fetchMode === FetchMode::COLUMN) {
413
            return $this->fetchColumn();
414
        }
415 74
416 1
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
417
            return oci_fetch_object($this->_sth);
418
        }
419 73
420
        if (! isset(self::$fetchModeMap[$fetchMode])) {
421
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
422
        }
423 73
424 73
        return oci_fetch_array(
425 73
            $this->_sth,
426
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
427
        );
428
    }
429
430
    /**
431
     * {@inheritdoc}
432 97
     */
433
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
434 97
    {
435
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
436 97
437
        $result = [];
438 97
439 1
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
440 1
            while ($row = $this->fetch($fetchMode)) {
441
                $result[] = $row;
442
            }
443 1
444
            return $result;
445
        }
446 96
447
        if (! isset(self::$fetchModeMap[$fetchMode])) {
448
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
449
        }
450 96
451 1
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
452 1
            while ($row = $this->fetch($fetchMode)) {
453
                $result[] = $row;
454
            }
455 95
        } else {
456
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
457 95
458 6
            if ($fetchMode === FetchMode::COLUMN) {
459
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
460
            }
461
462
            // do not try fetching from the statement if it's not expected to contain result
463 95
            // in order to prevent exceptional situation
464 3
            if (! $this->result) {
465
                return [];
466
            }
467 92
468 92
            oci_fetch_all(
469 92
                $this->_sth,
470 92
                $result,
471 92
                0,
472 92
                -1,
473
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
474
            );
475 92
476 6
            if ($fetchMode === FetchMode::COLUMN) {
477
                $result = $result[0];
478
            }
479
        }
480 93
481
        return $result;
482
    }
483
484
    /**
485
     * {@inheritdoc}
486 53
     */
487
    public function fetchColumn($columnIndex = 0)
488
    {
489
        // do not try fetching from the statement if it's not expected to contain result
490 53
        // in order to prevent exceptional situation
491 3
        if (! $this->result) {
492
            return false;
493
        }
494 50
495
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
496 50
497 3
        if ($row === false) {
498
            return false;
499
        }
500 47
501
        if ($columnIndex >= count($row)) {
502
            throw DBALException::invalidColumnIndex($columnIndex, count($row));
503
        }
504
505
        return $row[$columnIndex];
506 175
    }
507
508 175
    /**
509
     * {@inheritdoc}
510
     */
511
    public function rowCount()
512
    {
513
        return oci_num_rows($this->_sth);
514
    }
515
}
516