Completed
Pull Request — master (#3137)
by Sergei
16:13
created

OCI8Statement::findClosingQuote()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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

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

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