Failed Conditions
Pull Request — develop (#3154)
by Sergei
07:30
created

OCI8Statement::convertParamType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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

264
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, /** @scrutinizer ignore-type */ $offset)) {
Loading history...
265
            $offset = $matches[0][1];
266
            return $matches[0][0];
267
        }
268
269
        return null;
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function bindValue($param, $value, $type = ParameterType::STRING) : void
276
    {
277
        $this->bindParam($param, $value, $type);
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) : void
284
    {
285
        $column = $this->_paramMap[$column] ?? $column;
286
287
        if ($type === ParameterType::LARGE_OBJECT) {
288
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
289
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
290
            $variable =& $lob;
291
        }
292
293
        $this->boundValues[$column] =& $variable;
294
295
        oci_bind_by_name(
296
            $this->_sth,
297
            $column,
298
            $variable,
299
            $length ?? -1,
300
            $this->convertParamType($type)
301
        );
302
    }
303
304
    /**
305
     * Converts DBAL parameter type to oci8 parameter type
306
     */
307
    private function convertParamType(int $type) : int
308
    {
309
        switch ($type) {
310
            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...
311
                return OCI_B_BLOB;
312
313
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

Loading history...
314
                return SQLT_CHR;
315
        }
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321
    public function closeCursor() : void
322
    {
323
        // not having the result means there's nothing to close
324
        if (!$this->result) {
325
            return;
326
        }
327
328
        oci_cancel($this->_sth);
329
330
        $this->result = false;
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336
    public function columnCount() : int
337
    {
338
        return oci_num_fields($this->_sth);
339
    }
340
341
    /**
342
     * {@inheritdoc}
343
     */
344
    public function errorCode()
345
    {
346
        $error = oci_error($this->_sth);
347
        if ($error !== false) {
348
            $error = $error['code'];
349
        }
350
351
        return $error;
352
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357
    public function errorInfo()
358
    {
359
        return oci_error($this->_sth);
360
    }
361
362
    /**
363
     * {@inheritdoc}
364
     */
365
    public function execute($params = null) : void
366
    {
367
        if ($params) {
368
            $hasZeroIndex = array_key_exists(0, $params);
369
            foreach ($params as $key => $val) {
370
                if ($hasZeroIndex && is_numeric($key)) {
371
                    $this->bindValue($key + 1, $val);
372
                } else {
373
                    $this->bindValue($key, $val);
374
                }
375
            }
376
        }
377
378
        if (! @oci_execute($this->_sth, $this->_conn->getExecuteMode())) {
379
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
380
        }
381
382
        $this->result = true;
383
    }
384
385
    /**
386
     * {@inheritdoc}
387
     */
388
    public function setFetchMode($fetchMode, ...$args) : void
389
    {
390
        $this->_defaultFetchMode = $fetchMode;
391
    }
392
393
    /**
394
     * {@inheritdoc}
395
     */
396
    public function getIterator()
397
    {
398
        return new StatementIterator($this);
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     */
404
    public function fetch($fetchMode = null, ...$args)
405
    {
406
        if (!$this->result) {
407
            throw new OCI8Exception('The statement does not contain a result to be fetched');
408
        }
409
410
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
411
412
        if ($fetchMode === FetchMode::COLUMN) {
413
            return $this->fetchColumn();
414
        }
415
416
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
417
            return oci_fetch_object($this->_sth);
418
        }
419
420
        if (! isset(self::$fetchModeMap[$fetchMode])) {
421
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
422
        }
423
424
        return oci_fetch_array(
425
            $this->_sth,
426
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
427
        );
428
    }
429
430
    /**
431
     * {@inheritdoc}
432
     */
433
    public function fetchAll($fetchMode = null, ...$args) : array
434
    {
435
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
436
437
        $result = [];
438
439
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
440
            while ($row = $this->fetch($fetchMode)) {
441
                $result[] = $row;
442
            }
443
444
            return $result;
445
        }
446
447
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
448
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
449
        }
450
451
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
452
            while ($row = $this->fetch($fetchMode)) {
453
                $result[] = $row;
454
            }
455
        } else {
456
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
457
458
            if ($fetchMode === FetchMode::COLUMN) {
459
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
460
            }
461
462
            if (!$this->result) {
463
                throw new OCI8Exception('The statement does not contain a result to be fetched');
464
            }
465
466
            oci_fetch_all($this->_sth, $result, 0, -1,
467
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
468
469
            if ($fetchMode === FetchMode::COLUMN) {
470
                $result = $result[0];
471
            }
472
        }
473
474
        return $result;
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480
    public function fetchColumn($columnIndex = 0)
481
    {
482
        if (!$this->result) {
483
            throw new OCI8Exception('The statement does not contain a result to be fetched');
484
        }
485
486
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
487
488
        if (false === $row) {
489
            return false;
490
        }
491
492
        return $row[$columnIndex] ?? null;
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498
    public function rowCount() : int
499
    {
500
        return oci_num_rows($this->_sth);
501
    }
502
}
503