Passed
Push — fix-branch-aliases ( 626e10 )
by Michael
26:54
created

OCI8Statement::convertParameterType()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
crap 12
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
    public function __construct($dbh, $statement, OCI8Connection $conn)
132
    {
133
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
134
        $this->_sth = oci_parse($dbh, $statement);
135
        $this->_dbh = $dbh;
136
        $this->_paramMap = $paramMap;
137
        $this->_conn = $conn;
138
    }
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 180
    public static function convertPositionalToNamedPlaceholders($statement)
161
    {
162 180
        $fragmentOffset = $tokenOffset = 0;
163 180
        $fragments = $paramMap = [];
164 180
        $currentLiteralDelimiter = null;
165
166
        do {
167 180
            if (!$currentLiteralDelimiter) {
168 180
                $result = self::findPlaceholderOrOpeningQuote(
169 180
                    $statement,
170 180
                    $tokenOffset,
171 180
                    $fragmentOffset,
172 180
                    $fragments,
173 180
                    $currentLiteralDelimiter,
174 180
                    $paramMap
175
                );
176
            } else {
177 144
                $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 180
        } while ($result);
180
181 180
        if ($currentLiteralDelimiter) {
182
            throw new OCI8Exception(sprintf(
183
                'The statement contains non-terminated string literal starting at offset %d',
184
                $tokenOffset - 1
185
            ));
186
        }
187
188 180
        $fragments[] = substr($statement, $fragmentOffset);
189 180
        $statement = implode('', $fragments);
190
191 180
        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 180
    private static function findPlaceholderOrOpeningQuote(
207
        $statement,
208
        &$tokenOffset,
209
        &$fragmentOffset,
210
        &$fragments,
211
        &$currentLiteralDelimiter,
212
        &$paramMap
213
    ) {
214 180
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
215
216 180
        if (!$token) {
217 180
            return false;
218
        }
219
220 180
        if ($token === '?') {
221 180
            $position = count($paramMap) + 1;
222 180
            $param = ':param' . $position;
223 180
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
224 180
            $fragments[] = $param;
225 180
            $paramMap[$position] = $param;
226 180
            $tokenOffset += 1;
227 180
            $fragmentOffset = $tokenOffset;
228
229 180
            return true;
230
        }
231
232 144
        $currentLiteralDelimiter = $token;
233 144
        ++$tokenOffset;
234
235 144
        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 144
    private static function findClosingQuote(
248
        $statement,
249
        &$tokenOffset,
250
        &$currentLiteralDelimiter
251
    ) {
252 144
        $token = self::findToken(
253 144
            $statement,
254 144
            $tokenOffset,
255 144
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
256
        );
257
258 144
        if (!$token) {
259
            return false;
260
        }
261
262 144
        $currentLiteralDelimiter = false;
263 144
        ++$tokenOffset;
264
265 144
        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 180
    private static function findToken($statement, &$offset, $regex)
278
    {
279 180
        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 180
            $offset = $matches[0][1];
281 180
            return $matches[0][0];
282
        }
283
284 180
        return null;
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290
    public function bindValue($param, $value, $type = ParameterType::STRING)
291
    {
292
        return $this->bindParam($param, $value, $type, null);
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
299
    {
300
        $column = $this->_paramMap[$column] ?? $column;
301
302
        if ($type === ParameterType::LARGE_OBJECT) {
303
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
304
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
305
306
            $variable =& $lob;
307
        }
308
309
        $this->boundValues[$column] =& $variable;
310
311
        return oci_bind_by_name(
312
            $this->_sth,
313
            $column,
314
            $variable,
315
            $length ?? -1,
316
            $this->convertParameterType($type)
317
        );
318
    }
319
320
    /**
321
     * Converts DBAL parameter type to oci8 parameter type
322
     */
323
    private function convertParameterType(int $type) : int
324
    {
325
        switch ($type) {
326
            case ParameterType::BINARY:
327
                return OCI_B_BIN;
328
329
            case ParameterType::LARGE_OBJECT:
330
                return OCI_B_BLOB;
331
332
            default:
333
                return SQLT_CHR;
334
        }
335
    }
336
337
    /**
338
     * {@inheritdoc}
339
     */
340
    public function closeCursor()
341
    {
342
        // not having the result means there's nothing to close
343
        if (!$this->result) {
344
            return true;
345
        }
346
347
        oci_cancel($this->_sth);
348
349
        $this->result = false;
350
351
        return true;
352
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357
    public function columnCount()
358
    {
359
        return oci_num_fields($this->_sth);
360
    }
361
362
    /**
363
     * {@inheritdoc}
364
     */
365
    public function errorCode()
366
    {
367
        $error = oci_error($this->_sth);
368
        if ($error !== false) {
369
            $error = $error['code'];
370
        }
371
372
        return $error;
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378
    public function errorInfo()
379
    {
380
        return oci_error($this->_sth);
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386
    public function execute($params = null)
387
    {
388
        if ($params) {
389
            $hasZeroIndex = array_key_exists(0, $params);
390
            foreach ($params as $key => $val) {
391
                if ($hasZeroIndex && is_numeric($key)) {
392
                    $this->bindValue($key + 1, $val);
393
                } else {
394
                    $this->bindValue($key, $val);
395
                }
396
            }
397
        }
398
399
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
400
        if ( ! $ret) {
401
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
402
        }
403
404
        $this->result = true;
405
406
        return $ret;
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     */
412
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
413
    {
414
        $this->_defaultFetchMode = $fetchMode;
415
416
        return true;
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    public function getIterator()
423
    {
424
        return new StatementIterator($this);
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     */
430
    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
431
    {
432
        // do not try fetching from the statement if it's not expected to contain result
433
        // in order to prevent exceptional situation
434
        if (!$this->result) {
435
            return false;
436
        }
437
438
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
439
440
        if ($fetchMode === FetchMode::COLUMN) {
441
            return $this->fetchColumn();
442
        }
443
444
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
445
            return oci_fetch_object($this->_sth);
446
        }
447
448
        if (! isset(self::$fetchModeMap[$fetchMode])) {
449
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
450
        }
451
452
        return oci_fetch_array(
453
            $this->_sth,
454
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
455
        );
456
    }
457
458
    /**
459
     * {@inheritdoc}
460
     */
461
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
462
    {
463
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
464
465
        $result = [];
466
467
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
468
            while ($row = $this->fetch($fetchMode)) {
469
                $result[] = $row;
470
            }
471
472
            return $result;
473
        }
474
475
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
476
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
477
        }
478
479
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
480
            while ($row = $this->fetch($fetchMode)) {
481
                $result[] = $row;
482
            }
483
        } else {
484
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
485
486
            if ($fetchMode === FetchMode::COLUMN) {
487
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
488
            }
489
490
            // do not try fetching from the statement if it's not expected to contain result
491
            // in order to prevent exceptional situation
492
            if (!$this->result) {
493
                return [];
494
            }
495
496
            oci_fetch_all($this->_sth, $result, 0, -1,
497
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
498
499
            if ($fetchMode === FetchMode::COLUMN) {
500
                $result = $result[0];
501
            }
502
        }
503
504
        return $result;
505
    }
506
507
    /**
508
     * {@inheritdoc}
509
     */
510
    public function fetchColumn($columnIndex = 0)
511
    {
512
        // do not try fetching from the statement if it's not expected to contain result
513
        // in order to prevent exceptional situation
514
        if (!$this->result) {
515
            return false;
516
        }
517
518
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
519
520
        if (false === $row) {
521
            return false;
522
        }
523
524
        return $row[$columnIndex] ?? null;
525
    }
526
527
    /**
528
     * {@inheritdoc}
529
     */
530
    public function rowCount()
531
    {
532
        return oci_num_rows($this->_sth);
533
    }
534
}
535