Passed
Pull Request — master (#3149)
by Sergei
16:36
created

OCI8Statement::bindValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 3
crap 1
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 Doctrine\DBAL\FetchMode;
25
use Doctrine\DBAL\ParameterType;
26
use IteratorAggregate;
27
use const OCI_ASSOC;
28
use const OCI_B_BIN;
29
use const OCI_B_BLOB;
30
use const OCI_BOTH;
31
use const OCI_D_LOB;
32
use const OCI_FETCHSTATEMENT_BY_COLUMN;
33
use const OCI_FETCHSTATEMENT_BY_ROW;
34
use const OCI_NUM;
35
use const OCI_RETURN_LOBS;
36
use const OCI_RETURN_NULLS;
37
use const OCI_TEMP_BLOB;
38
use const PREG_OFFSET_CAPTURE;
39
use const SQLT_CHR;
40
use function array_key_exists;
41
use function count;
42
use function implode;
43
use function is_numeric;
44
use function oci_bind_by_name;
45
use function oci_cancel;
46
use function oci_error;
47
use function oci_execute;
48
use function oci_fetch_all;
49
use function oci_fetch_array;
50
use function oci_fetch_object;
51
use function oci_new_descriptor;
52
use function oci_num_fields;
53
use function oci_num_rows;
54
use function oci_parse;
55
use function preg_match;
56
use function preg_quote;
57
use function sprintf;
58
use function substr;
59
60
/**
61
 * The OCI8 implementation of the Statement interface.
62
 *
63
 * @since 2.0
64
 * @author Roman Borschel <[email protected]>
65
 */
66
class OCI8Statement implements IteratorAggregate, Statement
67
{
68
    /**
69
     * @var resource
70
     */
71
    protected $_dbh;
72
73
    /**
74
     * @var resource
75
     */
76
    protected $_sth;
77
78
    /**
79
     * @var \Doctrine\DBAL\Driver\OCI8\OCI8Connection
80
     */
81
    protected $_conn;
82
83
    /**
84
     * @var string
85
     */
86
    protected static $_PARAM = ':param';
87
88
    /**
89
     * @var array
90
     */
91
    protected static $fetchModeMap = [
92
        FetchMode::MIXED       => OCI_BOTH,
93
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
94
        FetchMode::NUMERIC     => OCI_NUM,
95
        FetchMode::COLUMN      => OCI_NUM,
96
    ];
97
98
    /**
99
     * @var int
100
     */
101
    protected $_defaultFetchMode = FetchMode::MIXED;
102
103
    /**
104
     * @var array
105
     */
106
    protected $_paramMap = [];
107
108
    /**
109
     * Holds references to bound parameter values.
110
     *
111
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
112
     *
113
     * @var array
114
     */
115
    private $boundValues = [];
116
117
    /**
118
     * Indicates whether the statement is in the state when fetching results is possible
119
     *
120
     * @var bool
121
     */
122
    private $result = false;
123
124
    /**
125
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
126
     *
127
     * @param resource                                  $dbh       The connection handle.
128
     * @param string                                    $statement The SQL statement.
129
     * @param \Doctrine\DBAL\Driver\OCI8\OCI8Connection $conn
130
     */
131 258
    public function __construct($dbh, $statement, OCI8Connection $conn)
132
    {
133 258
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
134 258
        $this->_sth = oci_parse($dbh, $statement);
135 258
        $this->_dbh = $dbh;
136 258
        $this->_paramMap = $paramMap;
137 258
        $this->_conn = $conn;
138 258
    }
139
140
    /**
141
     * Converts positional (?) into named placeholders (:param<num>).
142
     *
143
     * Oracle does not support positional parameters, hence this method converts all
144
     * positional parameters into artificially named parameters. Note that this conversion
145
     * is not perfect. All question marks (?) in the original statement are treated as
146
     * placeholders and converted to a named parameter.
147
     *
148
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
149
     * Question marks inside literal strings are therefore handled correctly by this method.
150
     * This comes at a cost, the whole sql statement has to be looped over.
151
     *
152
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
153
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
154
     *
155
     * @param string $statement The SQL statement to convert.
156
     *
157
     * @return array [0] => the statement value (string), [1] => the paramMap value (array).
158
     * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
159
     */
160 451
    public static function convertPositionalToNamedPlaceholders($statement)
161
    {
162 451
        $fragmentOffset = $tokenOffset = 0;
163 451
        $fragments = $paramMap = [];
164 451
        $currentLiteralDelimiter = null;
165
166
        do {
167 451
            if (!$currentLiteralDelimiter) {
168 451
                $result = self::findPlaceholderOrOpeningQuote(
169 451
                    $statement,
170 451
                    $tokenOffset,
171 451
                    $fragmentOffset,
172 451
                    $fragments,
173 451
                    $currentLiteralDelimiter,
174 451
                    $paramMap
175
                );
176
            } else {
177 269
                $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

177
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
178
            }
179 451
        } while ($result);
180
181 451
        if ($currentLiteralDelimiter) {
182 3
            throw new OCI8Exception(sprintf(
183 3
                'The statement contains non-terminated string literal starting at offset %d',
184 3
                $tokenOffset - 1
185
            ));
186
        }
187
188 448
        $fragments[] = substr($statement, $fragmentOffset);
189 448
        $statement = implode('', $fragments);
190
191 448
        return [$statement, $paramMap];
192
    }
193
194
    /**
195
     * Finds next placeholder or opening quote.
196
     *
197
     * @param string             $statement               The SQL statement to parse
198
     * @param string             $tokenOffset             The offset to start searching from
199
     * @param int                $fragmentOffset          The offset to build the next fragment from
200
     * @param string[]           $fragments               Fragments of the original statement not containing placeholders
201
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
202
     *                                                    or NULL if not currently in a literal
203
     * @param array<int, string> $paramMap                Mapping of the original parameter positions to their named replacements
204
     * @return bool Whether the token was found
205
     */
206 451
    private static function findPlaceholderOrOpeningQuote(
207
        $statement,
208
        &$tokenOffset,
209
        &$fragmentOffset,
210
        &$fragments,
211
        &$currentLiteralDelimiter,
212
        &$paramMap
213
    ) {
214 451
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
215
216 451
        if (!$token) {
217 448
            return false;
218
        }
219
220 416
        if ($token === '?') {
221 313
            $position = count($paramMap) + 1;
222 313
            $param = ':param' . $position;
223 313
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
224 313
            $fragments[] = $param;
225 313
            $paramMap[$position] = $param;
226 313
            $tokenOffset += 1;
227 313
            $fragmentOffset = $tokenOffset;
228
229 313
            return true;
230
        }
231
232 269
        $currentLiteralDelimiter = $token;
233 269
        ++$tokenOffset;
234
235 269
        return true;
236
    }
237
238
    /**
239
     * Finds closing quote
240
     *
241
     * @param string      $statement               The SQL statement to parse
242
     * @param string      $tokenOffset             The offset to start searching from
243
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
244
     *                                             or NULL if not currently in a literal
245
     * @return bool Whether the token was found
246
     */
247 269
    private static function findClosingQuote(
248
        $statement,
249
        &$tokenOffset,
250
        &$currentLiteralDelimiter
251
    ) {
252 269
        $token = self::findToken(
253 269
            $statement,
254 269
            $tokenOffset,
255 269
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
256
        );
257
258 269
        if (!$token) {
259 3
            return false;
260
        }
261
262 267
        $currentLiteralDelimiter = false;
263 267
        ++$tokenOffset;
264
265 267
        return true;
266
    }
267
268
    /**
269
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
270
     * where the token was found.
271
     *
272
     * @param string $statement The SQL statement to parse
273
     * @param string $offset    The offset to start searching from
274
     * @param string $regex     The regex containing token pattern
275
     * @return string|null Token or NULL if not found
276
     */
277 451
    private static function findToken($statement, &$offset, $regex)
278
    {
279 451
        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

279
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, /** @scrutinizer ignore-type */ $offset)) {
Loading history...
280 416
            $offset = $matches[0][1];
281 416
            return $matches[0][0];
282
        }
283
284 451
        return null;
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290 118
    public function bindValue($param, $value, $type = ParameterType::STRING)
291
    {
292 118
        return $this->bindParam($param, $value, $type, null);
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298 123
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
299
    {
300 123
        $column = $this->_paramMap[$column] ?? $column;
301
302 123
        if ($type === ParameterType::LARGE_OBJECT) {
303 4
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
304 4
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
305
306 4
            $variable =& $lob;
307
        }
308
309 123
        switch ($type) {
310
            case ParameterType::BINARY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
311 1
                $bindType = OCI_B_BIN;
312 1
                break;
313
314
            case ParameterType::LARGE_OBJECT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
315 4
                $bindType = OCI_B_BLOB;
316 4
                break;
317
318
            default:
319 121
                $bindType = SQLT_CHR;
320 121
                break;
321
        }
322
323 123
        $this->boundValues[$column] =& $variable;
324
325 123
        return oci_bind_by_name($this->_sth, $column, $variable, $length ?? -1, $bindType);
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331 19
    public function closeCursor()
332
    {
333
        // not having the result means there's nothing to close
334 19
        if (!$this->result) {
335 4
            return true;
336
        }
337
338 15
        oci_cancel($this->_sth);
339
340 15
        $this->result = false;
341
342 15
        return true;
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348 4
    public function columnCount()
349
    {
350 4
        return oci_num_fields($this->_sth);
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356
    public function errorCode()
357
    {
358
        $error = oci_error($this->_sth);
359
        if ($error !== false) {
360
            $error = $error['code'];
361
        }
362
363
        return $error;
364
    }
365
366
    /**
367
     * {@inheritdoc}
368
     */
369 139
    public function errorInfo()
370
    {
371 139
        return oci_error($this->_sth);
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377 259
    public function execute($params = null)
378
    {
379 259
        if ($params) {
380 91
            $hasZeroIndex = array_key_exists(0, $params);
381 91
            foreach ($params as $key => $val) {
382 91
                if ($hasZeroIndex && is_numeric($key)) {
383 91
                    $this->bindValue($key + 1, $val);
384
                } else {
385 91
                    $this->bindValue($key, $val);
386
                }
387
            }
388
        }
389
390 255
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
391 255
        if ( ! $ret) {
392 141
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
393
        }
394
395 252
        $this->result = true;
396
397 252
        return $ret;
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403 225
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
404
    {
405 225
        $this->_defaultFetchMode = $fetchMode;
406
407 225
        return true;
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413 3
    public function getIterator()
414
    {
415 3
        return new StatementIterator($this);
416
    }
417
418
    /**
419
     * {@inheritdoc}
420
     */
421 78
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
422
    {
423
        // do not try fetching from the statement if it's not expected to contain result
424
        // in order to prevent exceptional situation
425 78
        if (!$this->result) {
426 3
            return false;
427
        }
428
429 75
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
430
431 75
        if ($fetchMode === FetchMode::COLUMN) {
432 1
            return $this->fetchColumn();
433
        }
434
435 74
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
436 1
            return oci_fetch_object($this->_sth);
437
        }
438
439 73
        if (! isset(self::$fetchModeMap[$fetchMode])) {
440
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
441
        }
442
443 73
        return oci_fetch_array(
444 73
            $this->_sth,
445 73
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
446
        );
447
    }
448
449
    /**
450
     * {@inheritdoc}
451
     */
452 96
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
453
    {
454 96
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
455
456 96
        $result = [];
457
458 96
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
459 1
            while ($row = $this->fetch($fetchMode)) {
460 1
                $result[] = $row;
461
            }
462
463 1
            return $result;
464
        }
465
466 95
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
467
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
468
        }
469
470 95
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
471 1
            while ($row = $this->fetch($fetchMode)) {
472 1
                $result[] = $row;
473
            }
474
        } else {
475 94
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
476
477 94
            if ($fetchMode === FetchMode::COLUMN) {
478 4
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
479
            }
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 94
            if (!$this->result) {
484 3
                return [];
485
            }
486
487 91
            oci_fetch_all($this->_sth, $result, 0, -1,
488 91
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
489
490 91
            if ($fetchMode === FetchMode::COLUMN) {
491 4
                $result = $result[0];
492
            }
493
        }
494
495 92
        return $result;
496
    }
497
498
    /**
499
     * {@inheritdoc}
500
     */
501 51
    public function fetchColumn($columnIndex = 0)
502
    {
503
        // do not try fetching from the statement if it's not expected to contain result
504
        // in order to prevent exceptional situation
505 51
        if (!$this->result) {
506 3
            return false;
507
        }
508
509 48
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
510
511 48
        if (false === $row) {
512 3
            return false;
513
        }
514
515 45
        return $row[$columnIndex] ?? null;
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521 174
    public function rowCount()
522
    {
523 174
        return oci_num_rows($this->_sth);
524
    }
525
}
526