Completed
Push — master ( 8575c2...a53269 )
by Marco
02:31 queued 01:15
created

convertPositionalToNamedPlaceholders()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4.0466

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 32
rs 8.5806
ccs 18
cts 21
cp 0.8571
cc 4
eloc 22
nc 4
nop 1
crap 4.0466
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 IteratorAggregate;
25
use PDO;
26
27
/**
28
 * The OCI8 implementation of the Statement interface.
29
 *
30
 * @since 2.0
31
 * @author Roman Borschel <[email protected]>
32
 */
33
class OCI8Statement implements IteratorAggregate, Statement
34
{
35
    /**
36
     * @var resource
37
     */
38
    protected $_dbh;
39
40
    /**
41
     * @var resource
42
     */
43
    protected $_sth;
44
45
    /**
46
     * @var \Doctrine\DBAL\Driver\OCI8\OCI8Connection
47
     */
48
    protected $_conn;
49
50
    /**
51
     * @var string
52
     */
53
    protected static $_PARAM = ':param';
54
55
    /**
56
     * @var array
57
     */
58
    protected static $fetchModeMap = [
59
        PDO::FETCH_BOTH => OCI_BOTH,
60
        PDO::FETCH_ASSOC => OCI_ASSOC,
61
        PDO::FETCH_NUM => OCI_NUM,
62
        PDO::FETCH_COLUMN => OCI_NUM,
63
    ];
64
65
    /**
66
     * @var integer
67
     */
68
    protected $_defaultFetchMode = PDO::FETCH_BOTH;
69
70
    /**
71
     * @var array
72
     */
73
    protected $_paramMap = [];
74
75
    /**
76
     * Holds references to bound parameter values.
77
     *
78
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
79
     *
80
     * @var array
81
     */
82
    private $boundValues = [];
83
84
    /**
85
     * Indicates whether the statement is in the state when fetching results is possible
86
     *
87
     * @var bool
88
     */
89
    private $result = false;
90
91
    /**
92
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
93
     *
94
     * @param resource                                  $dbh       The connection handle.
95
     * @param string                                    $statement The SQL statement.
96
     * @param \Doctrine\DBAL\Driver\OCI8\OCI8Connection $conn
97
     */
98
    public function __construct($dbh, $statement, OCI8Connection $conn)
99
    {
100
        list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement);
101
        $this->_sth = oci_parse($dbh, $statement);
102
        $this->_dbh = $dbh;
103
        $this->_paramMap = $paramMap;
104
        $this->_conn = $conn;
105
    }
106
107
    /**
108
     * Converts positional (?) into named placeholders (:param<num>).
109
     *
110
     * Oracle does not support positional parameters, hence this method converts all
111
     * positional parameters into artificially named parameters. Note that this conversion
112
     * is not perfect. All question marks (?) in the original statement are treated as
113
     * placeholders and converted to a named parameter.
114
     *
115
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
116
     * Question marks inside literal strings are therefore handled correctly by this method.
117
     * This comes at a cost, the whole sql statement has to be looped over.
118
     *
119
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
120
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
121
     *
122
     * @param string $statement The SQL statement to convert.
123
     *
124
     * @return array [0] => the statement value (string), [1] => the paramMap value (array).
125
     * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
126
     */
127 10
    public static function convertPositionalToNamedPlaceholders($statement)
128
    {
129 10
        $fragmentOffset = $tokenOffset = 0;
130 10
        $fragments = $paramMap = [];
131 10
        $currentLiteralDelimiter = null;
132
133
        do {
134 10
            if (!$currentLiteralDelimiter) {
135 10
                $result = self::findPlaceholderOrOpeningQuote(
136 10
                    $statement,
137 10
                    $tokenOffset,
138 10
                    $fragmentOffset,
139 10
                    $fragments,
140 10
                    $currentLiteralDelimiter,
141 10
                    $paramMap
142
                );
143
            } else {
144 8
                $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

144
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
145
            }
146 10
        } while ($result);
147
148 10
        if ($currentLiteralDelimiter) {
149
            throw new OCI8Exception(sprintf(
150
                'The statement contains non-terminated string literal starting at offset %d',
151
                $tokenOffset - 1
152
            ));
153
        }
154
155 10
        $fragments[] = substr($statement, $fragmentOffset);
156 10
        $statement = implode('', $fragments);
157
158 10
        return [$statement, $paramMap];
159
    }
160
161
    /**
162
     * Finds next placeholder or opening quote.
163
     *
164
     * @param string $statement The SQL statement to parse
165
     * @param string $tokenOffset The offset to start searching from
166
     * @param int $fragmentOffset The offset to build the next fragment from
167
     * @param string[] $fragments Fragments of the original statement not containing placeholders
168
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
169
     *                                             or NULL if not currently in a literal
170
     * @param array<int, string> $paramMap Mapping of the original parameter positions to their named replacements
171
     * @return bool Whether the token was found
172
     */
173 10
    private static function findPlaceholderOrOpeningQuote(
174
        $statement,
175
        &$tokenOffset,
176
        &$fragmentOffset,
177
        &$fragments,
178
        &$currentLiteralDelimiter,
179
        &$paramMap
180
    ) {
181 10
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
182
183 10
        if (!$token) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
184 10
            return false;
185
        }
186
187 10
        if ($token === '?') {
188 10
            $position = count($paramMap) + 1;
189 10
            $param = ':param' . $position;
190 10
            $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
191 10
            $fragments[] = $param;
192 10
            $paramMap[$position] = $param;
193 10
            $tokenOffset += 1;
194 10
            $fragmentOffset = $tokenOffset;
195
196 10
            return true;
197
        }
198
199 8
        $currentLiteralDelimiter = $token;
200 8
        ++$tokenOffset;
201
202 8
        return true;
203
    }
204
205
    /**
206
     * Finds closing quote
207
     *
208
     * @param string $statement The SQL statement to parse
209
     * @param string $tokenOffset The offset to start searching from
210
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
211
     *                                             or NULL if not currently in a literal
212
     * @return bool Whether the token was found
213
     */
214 8
    private static function findClosingQuote(
215
        $statement,
216
        &$tokenOffset,
217
        &$currentLiteralDelimiter
218
    ) {
219 8
        $token = self::findToken(
220 8
            $statement,
221 8
            $tokenOffset,
222 8
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
223
        );
224
225 8
        if (!$token) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
226
            return false;
227
        }
228
229 8
        $currentLiteralDelimiter = false;
230 8
        ++$tokenOffset;
231
232 8
        return true;
233
    }
234
235
    /**
236
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
237
     * where the token was found.
238
     *
239
     * @param string $statement The SQL statement to parse
240
     * @param string $offset The offset to start searching from
241
     * @param string $regex The regex containing token pattern
242
     * @return string|null Token or NULL if not found
243
     */
244 10
    private static function findToken($statement, &$offset, $regex)
245
    {
246 10
        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

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