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 PDO; |
12
|
|
|
use const OCI_ASSOC; |
13
|
|
|
use const OCI_B_BIN; |
14
|
|
|
use const OCI_B_BLOB; |
15
|
|
|
use const OCI_BOTH; |
16
|
|
|
use const OCI_D_LOB; |
17
|
|
|
use const OCI_FETCHSTATEMENT_BY_COLUMN; |
18
|
|
|
use const OCI_FETCHSTATEMENT_BY_ROW; |
19
|
|
|
use const OCI_NUM; |
20
|
|
|
use const OCI_RETURN_LOBS; |
21
|
|
|
use const OCI_RETURN_NULLS; |
22
|
|
|
use const OCI_TEMP_BLOB; |
23
|
|
|
use const PREG_OFFSET_CAPTURE; |
24
|
|
|
use const SQLT_CHR; |
25
|
|
|
use function array_key_exists; |
26
|
|
|
use function count; |
27
|
|
|
use function implode; |
28
|
|
|
use function is_numeric; |
29
|
|
|
use function oci_bind_by_name; |
30
|
|
|
use function oci_cancel; |
31
|
|
|
use function oci_error; |
32
|
|
|
use function oci_execute; |
33
|
|
|
use function oci_fetch_all; |
34
|
|
|
use function oci_fetch_array; |
35
|
|
|
use function oci_fetch_object; |
36
|
|
|
use function oci_new_descriptor; |
37
|
|
|
use function oci_num_fields; |
38
|
|
|
use function oci_num_rows; |
39
|
|
|
use function oci_parse; |
40
|
|
|
use function preg_match; |
41
|
|
|
use function preg_quote; |
42
|
|
|
use function sprintf; |
43
|
|
|
use function substr; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* The OCI8 implementation of the Statement interface. |
47
|
|
|
*/ |
48
|
|
|
class OCI8Statement implements IteratorAggregate, Statement |
49
|
|
|
{ |
50
|
|
|
/** @var resource */ |
51
|
|
|
protected $_dbh; |
52
|
|
|
|
53
|
|
|
/** @var resource */ |
54
|
|
|
protected $_sth; |
55
|
|
|
|
56
|
|
|
/** @var OCI8Connection */ |
57
|
|
|
protected $_conn; |
58
|
|
|
|
59
|
|
|
/** @var string */ |
60
|
|
|
protected static $_PARAM = ':param'; |
61
|
|
|
|
62
|
|
|
/** @var int[] */ |
63
|
|
|
protected static $fetchModeMap = [ |
64
|
|
|
FetchMode::MIXED => OCI_BOTH, |
65
|
|
|
FetchMode::ASSOCIATIVE => OCI_ASSOC, |
66
|
|
|
FetchMode::NUMERIC => OCI_NUM, |
67
|
|
|
FetchMode::COLUMN => OCI_NUM, |
68
|
|
|
]; |
69
|
|
|
|
70
|
|
|
/** @var int */ |
71
|
|
|
protected $_defaultFetchMode = FetchMode::MIXED; |
72
|
|
|
|
73
|
|
|
/** @var string[] */ |
74
|
|
|
protected $_paramMap = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Holds references to bound parameter values. |
78
|
|
|
* |
79
|
|
|
* This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected. |
80
|
|
|
* |
81
|
|
|
* @var mixed[] |
82
|
|
|
*/ |
83
|
|
|
private $boundValues = []; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Indicates whether the statement is in the state when fetching results is possible |
87
|
|
|
* |
88
|
|
|
* @var bool |
89
|
|
|
*/ |
90
|
|
|
private $result = false; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Creates a new OCI8Statement that uses the given connection handle and SQL statement. |
94
|
|
|
* |
95
|
|
|
* @param resource $dbh The connection handle. |
96
|
|
|
* @param string $statement The SQL statement. |
97
|
|
|
*/ |
98
|
|
|
public function __construct($dbh, $statement, OCI8Connection $conn) |
99
|
|
|
{ |
100
|
|
|
[$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
|
|
|
* @param string $statement The SQL statement to convert. |
120
|
|
|
* |
121
|
|
|
* @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array). |
122
|
|
|
* |
123
|
|
|
* @throws OCI8Exception |
124
|
|
|
* |
125
|
|
|
* @todo extract into utility class in Doctrine\DBAL\Util namespace |
126
|
|
|
* @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements. |
127
|
|
|
*/ |
128
|
300 |
|
public static function convertPositionalToNamedPlaceholders($statement) |
129
|
|
|
{ |
130
|
300 |
|
$fragmentOffset = $tokenOffset = 0; |
131
|
300 |
|
$fragments = $paramMap = []; |
132
|
300 |
|
$currentLiteralDelimiter = null; |
133
|
|
|
|
134
|
|
|
do { |
135
|
300 |
|
if (! $currentLiteralDelimiter) { |
136
|
300 |
|
$result = self::findPlaceholderOrOpeningQuote( |
137
|
300 |
|
$statement, |
138
|
300 |
|
$tokenOffset, |
139
|
300 |
|
$fragmentOffset, |
140
|
300 |
|
$fragments, |
141
|
300 |
|
$currentLiteralDelimiter, |
142
|
300 |
|
$paramMap |
143
|
|
|
); |
144
|
|
|
} else { |
145
|
240 |
|
$result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter); |
|
|
|
|
146
|
|
|
} |
147
|
300 |
|
} while ($result); |
148
|
|
|
|
149
|
300 |
|
if ($currentLiteralDelimiter) { |
150
|
|
|
throw new OCI8Exception(sprintf( |
151
|
|
|
'The statement contains non-terminated string literal starting at offset %d', |
152
|
|
|
$tokenOffset - 1 |
153
|
|
|
)); |
154
|
|
|
} |
155
|
|
|
|
156
|
300 |
|
$fragments[] = substr($statement, $fragmentOffset); |
157
|
300 |
|
$statement = implode('', $fragments); |
158
|
|
|
|
159
|
300 |
|
return [$statement, $paramMap]; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Finds next placeholder or opening quote. |
164
|
|
|
* |
165
|
|
|
* @param string $statement The SQL statement to parse |
166
|
|
|
* @param string $tokenOffset The offset to start searching from |
167
|
|
|
* @param int $fragmentOffset The offset to build the next fragment from |
168
|
|
|
* @param string[] $fragments Fragments of the original statement not containing placeholders |
169
|
|
|
* @param string|null $currentLiteralDelimiter The delimiter of the current string literal |
170
|
|
|
* or NULL if not currently in a literal |
171
|
|
|
* @param array<int, string> $paramMap Mapping of the original parameter positions to their named replacements |
172
|
|
|
* |
173
|
|
|
* @return bool Whether the token was found |
174
|
|
|
*/ |
175
|
300 |
|
private static function findPlaceholderOrOpeningQuote( |
176
|
|
|
$statement, |
177
|
|
|
&$tokenOffset, |
178
|
|
|
&$fragmentOffset, |
179
|
|
|
&$fragments, |
180
|
|
|
&$currentLiteralDelimiter, |
181
|
|
|
&$paramMap |
182
|
|
|
) { |
183
|
300 |
|
$token = self::findToken($statement, $tokenOffset, '/[?\'"]/'); |
184
|
|
|
|
185
|
300 |
|
if (! $token) { |
186
|
300 |
|
return false; |
187
|
|
|
} |
188
|
|
|
|
189
|
300 |
|
if ($token === '?') { |
190
|
300 |
|
$position = count($paramMap) + 1; |
191
|
300 |
|
$param = ':param' . $position; |
192
|
300 |
|
$fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset); |
193
|
300 |
|
$fragments[] = $param; |
194
|
300 |
|
$paramMap[$position] = $param; |
195
|
300 |
|
$tokenOffset += 1; |
196
|
300 |
|
$fragmentOffset = $tokenOffset; |
197
|
|
|
|
198
|
300 |
|
return true; |
199
|
|
|
} |
200
|
|
|
|
201
|
240 |
|
$currentLiteralDelimiter = $token; |
202
|
240 |
|
++$tokenOffset; |
203
|
|
|
|
204
|
240 |
|
return true; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Finds closing quote |
209
|
|
|
* |
210
|
|
|
* @param string $statement The SQL statement to parse |
211
|
|
|
* @param string $tokenOffset The offset to start searching from |
212
|
|
|
* @param string|null $currentLiteralDelimiter The delimiter of the current string literal |
213
|
|
|
* or NULL if not currently in a literal |
214
|
|
|
* |
215
|
|
|
* @return bool Whether the token was found |
216
|
|
|
*/ |
217
|
240 |
|
private static function findClosingQuote( |
218
|
|
|
$statement, |
219
|
|
|
&$tokenOffset, |
220
|
|
|
&$currentLiteralDelimiter |
221
|
|
|
) { |
222
|
240 |
|
$token = self::findToken( |
223
|
240 |
|
$statement, |
224
|
240 |
|
$tokenOffset, |
225
|
240 |
|
'/' . preg_quote($currentLiteralDelimiter, '/') . '/' |
226
|
|
|
); |
227
|
|
|
|
228
|
240 |
|
if (! $token) { |
229
|
|
|
return false; |
230
|
|
|
} |
231
|
|
|
|
232
|
240 |
|
$currentLiteralDelimiter = false; |
233
|
240 |
|
++$tokenOffset; |
234
|
|
|
|
235
|
240 |
|
return true; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Finds the token described by regex starting from the given offset. Updates the offset with the position |
240
|
|
|
* where the token was found. |
241
|
|
|
* |
242
|
|
|
* @param string $statement The SQL statement to parse |
243
|
|
|
* @param string $offset The offset to start searching from |
244
|
|
|
* @param string $regex The regex containing token pattern |
245
|
|
|
* |
246
|
|
|
* @return string|null Token or NULL if not found |
247
|
|
|
*/ |
248
|
300 |
|
private static function findToken($statement, &$offset, $regex) |
249
|
|
|
{ |
250
|
300 |
|
if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) { |
|
|
|
|
251
|
300 |
|
$offset = $matches[0][1]; |
252
|
300 |
|
return $matches[0][0]; |
253
|
|
|
} |
254
|
|
|
|
255
|
300 |
|
return null; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* {@inheritdoc} |
260
|
|
|
*/ |
261
|
|
|
public function bindValue($param, $value, $type = ParameterType::STRING) |
262
|
|
|
{ |
263
|
|
|
return $this->bindParam($param, $value, $type, null); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* {@inheritdoc} |
268
|
|
|
*/ |
269
|
|
|
public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) |
270
|
|
|
{ |
271
|
|
|
$column = $this->_paramMap[$column] ?? $column; |
272
|
|
|
|
273
|
|
|
if ($type === ParameterType::LARGE_OBJECT) { |
274
|
|
|
$lob = oci_new_descriptor($this->_dbh, OCI_D_LOB); |
275
|
|
|
$lob->writeTemporary($variable, OCI_TEMP_BLOB); |
276
|
|
|
|
277
|
|
|
$variable =& $lob; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
$this->boundValues[$column] =& $variable; |
281
|
|
|
|
282
|
|
|
return oci_bind_by_name( |
283
|
|
|
$this->_sth, |
284
|
|
|
$column, |
285
|
|
|
$variable, |
286
|
|
|
$length ?? -1, |
287
|
|
|
$this->convertParameterType($type) |
288
|
|
|
); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Converts DBAL parameter type to oci8 parameter type |
293
|
|
|
*/ |
294
|
|
|
private function convertParameterType(int $type) : int |
295
|
|
|
{ |
296
|
|
|
switch ($type) { |
297
|
|
|
case ParameterType::BINARY: |
298
|
|
|
return OCI_B_BIN; |
299
|
|
|
|
300
|
|
|
case ParameterType::LARGE_OBJECT: |
301
|
|
|
return OCI_B_BLOB; |
302
|
|
|
|
303
|
|
|
default: |
304
|
|
|
return SQLT_CHR; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* {@inheritdoc} |
310
|
|
|
*/ |
311
|
|
|
public function closeCursor() |
312
|
|
|
{ |
313
|
|
|
// not having the result means there's nothing to close |
314
|
|
|
if (! $this->result) { |
315
|
|
|
return true; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
oci_cancel($this->_sth); |
319
|
|
|
|
320
|
|
|
$this->result = false; |
321
|
|
|
|
322
|
|
|
return true; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* {@inheritdoc} |
327
|
|
|
*/ |
328
|
|
|
public function columnCount() |
329
|
|
|
{ |
330
|
|
|
return oci_num_fields($this->_sth); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* {@inheritdoc} |
335
|
|
|
*/ |
336
|
|
|
public function errorCode() |
337
|
|
|
{ |
338
|
|
|
$error = oci_error($this->_sth); |
339
|
|
|
if ($error !== false) { |
340
|
|
|
$error = $error['code']; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
return $error; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* {@inheritdoc} |
348
|
|
|
*/ |
349
|
|
|
public function errorInfo() |
350
|
|
|
{ |
351
|
|
|
return oci_error($this->_sth); |
|
|
|
|
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* {@inheritdoc} |
356
|
|
|
*/ |
357
|
|
|
public function execute($params = null) |
358
|
|
|
{ |
359
|
|
|
if ($params) { |
360
|
|
|
$hasZeroIndex = array_key_exists(0, $params); |
361
|
|
|
foreach ($params as $key => $val) { |
362
|
|
|
if ($hasZeroIndex && is_numeric($key)) { |
363
|
|
|
$this->bindValue($key + 1, $val); |
364
|
|
|
} else { |
365
|
|
|
$this->bindValue($key, $val); |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
$ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode()); |
371
|
|
|
if (! $ret) { |
372
|
|
|
throw OCI8Exception::fromErrorInfo($this->errorInfo()); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
$this->result = true; |
376
|
|
|
|
377
|
|
|
return $ret; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* {@inheritdoc} |
382
|
|
|
*/ |
383
|
|
|
public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) |
384
|
|
|
{ |
385
|
|
|
$this->_defaultFetchMode = $fetchMode; |
386
|
|
|
|
387
|
|
|
return true; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* {@inheritdoc} |
392
|
|
|
*/ |
393
|
|
|
public function getIterator() |
394
|
|
|
{ |
395
|
|
|
return new StatementIterator($this); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* {@inheritdoc} |
400
|
|
|
*/ |
401
|
|
|
public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) |
402
|
|
|
{ |
403
|
|
|
// do not try fetching from the statement if it's not expected to contain result |
404
|
|
|
// in order to prevent exceptional situation |
405
|
|
|
if (! $this->result) { |
406
|
|
|
return false; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
$fetchMode = $fetchMode ?: $this->_defaultFetchMode; |
410
|
|
|
|
411
|
|
|
if ($fetchMode === FetchMode::COLUMN) { |
412
|
|
|
return $this->fetchColumn(); |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
if ($fetchMode === FetchMode::STANDARD_OBJECT) { |
416
|
|
|
return oci_fetch_object($this->_sth); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
if (! isset(self::$fetchModeMap[$fetchMode])) { |
420
|
|
|
throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
return oci_fetch_array( |
424
|
|
|
$this->_sth, |
425
|
|
|
self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS |
426
|
|
|
); |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* {@inheritdoc} |
431
|
|
|
*/ |
432
|
|
|
public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) |
433
|
|
|
{ |
434
|
|
|
$fetchMode = $fetchMode ?: $this->_defaultFetchMode; |
435
|
|
|
|
436
|
|
|
$result = []; |
437
|
|
|
|
438
|
|
|
if ($fetchMode === FetchMode::STANDARD_OBJECT) { |
439
|
|
|
while ($row = $this->fetch($fetchMode)) { |
440
|
|
|
$result[] = $row; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
return $result; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
if (! isset(self::$fetchModeMap[$fetchMode])) { |
447
|
|
|
throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode); |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) { |
451
|
|
|
while ($row = $this->fetch($fetchMode)) { |
452
|
|
|
$result[] = $row; |
453
|
|
|
} |
454
|
|
|
} else { |
455
|
|
|
$fetchStructure = OCI_FETCHSTATEMENT_BY_ROW; |
456
|
|
|
|
457
|
|
|
if ($fetchMode === FetchMode::COLUMN) { |
458
|
|
|
$fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN; |
459
|
|
|
} |
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 []; |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
oci_fetch_all( |
468
|
|
|
$this->_sth, |
469
|
|
|
$result, |
470
|
|
|
0, |
471
|
|
|
-1, |
472
|
|
|
self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS |
473
|
|
|
); |
474
|
|
|
|
475
|
|
|
if ($fetchMode === FetchMode::COLUMN) { |
476
|
|
|
$result = $result[0]; |
477
|
|
|
} |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
return $result; |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* {@inheritdoc} |
485
|
|
|
*/ |
486
|
|
|
public function fetchColumn($columnIndex = 0) |
487
|
|
|
{ |
488
|
|
|
// do not try fetching from the statement if it's not expected to contain result |
489
|
|
|
// in order to prevent exceptional situation |
490
|
|
|
if (! $this->result) { |
491
|
|
|
return false; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
$row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS); |
495
|
|
|
|
496
|
|
|
if ($row === false) { |
497
|
|
|
return false; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
return $row[$columnIndex] ?? null; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* {@inheritdoc} |
505
|
|
|
*/ |
506
|
|
|
public function rowCount() |
507
|
|
|
{ |
508
|
|
|
return oci_num_rows($this->_sth); |
509
|
|
|
} |
510
|
|
|
} |
511
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.