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 | 262 | public function __construct($dbh, $statement, OCI8Connection $conn) |
|||
99 | { |
||||
100 | 262 | [$statement, $paramMap] = self::convertPositionalToNamedPlaceholders($statement); |
|||
101 | 262 | $this->_sth = oci_parse($dbh, $statement); |
|||
102 | 262 | $this->_dbh = $dbh; |
|||
103 | 262 | $this->_paramMap = $paramMap; |
|||
104 | 262 | $this->_conn = $conn; |
|||
105 | 262 | } |
|||
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 | 538 | public static function convertPositionalToNamedPlaceholders($statement) |
|||
129 | { |
||||
130 | 538 | $fragmentOffset = $tokenOffset = 0; |
|||
131 | 538 | $fragments = $paramMap = []; |
|||
132 | 538 | $currentLiteralDelimiter = null; |
|||
133 | |||||
134 | do { |
||||
135 | 538 | if (! $currentLiteralDelimiter) { |
|||
136 | 538 | $result = self::findPlaceholderOrOpeningQuote( |
|||
137 | 538 | $statement, |
|||
138 | 538 | $tokenOffset, |
|||
139 | 538 | $fragmentOffset, |
|||
140 | 538 | $fragments, |
|||
141 | 538 | $currentLiteralDelimiter, |
|||
142 | 538 | $paramMap |
|||
143 | ); |
||||
144 | } else { |
||||
145 | 340 | $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter); |
|||
146 | } |
||||
147 | 538 | } while ($result); |
|||
148 | |||||
149 | 538 | if ($currentLiteralDelimiter) { |
|||
150 | 6 | throw new OCI8Exception(sprintf( |
|||
151 | 6 | 'The statement contains non-terminated string literal starting at offset %d', |
|||
152 | 6 | $tokenOffset - 1 |
|||
153 | )); |
||||
154 | } |
||||
155 | |||||
156 | 532 | $fragments[] = substr($statement, $fragmentOffset); |
|||
157 | 532 | $statement = implode('', $fragments); |
|||
158 | |||||
159 | 532 | 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 | 538 | private static function findPlaceholderOrOpeningQuote( |
|||
176 | $statement, |
||||
177 | &$tokenOffset, |
||||
178 | &$fragmentOffset, |
||||
179 | &$fragments, |
||||
180 | &$currentLiteralDelimiter, |
||||
181 | &$paramMap |
||||
182 | ) { |
||||
183 | 538 | $token = self::findToken($statement, $tokenOffset, '/[?\'"]/'); |
|||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
184 | |||||
185 | 538 | if (! $token) { |
|||
186 | 532 | return false; |
|||
187 | } |
||||
188 | |||||
189 | 503 | if ($token === '?') { |
|||
190 | 393 | $position = count($paramMap) + 1; |
|||
191 | 393 | $param = ':param' . $position; |
|||
192 | 393 | $fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset); |
|||
193 | 393 | $fragments[] = $param; |
|||
194 | 393 | $paramMap[$position] = $param; |
|||
195 | 393 | $tokenOffset += 1; |
|||
196 | 393 | $fragmentOffset = $tokenOffset; |
|||
197 | |||||
198 | 393 | return true; |
|||
199 | } |
||||
200 | |||||
201 | 340 | $currentLiteralDelimiter = $token; |
|||
202 | 340 | ++$tokenOffset; |
|||
203 | |||||
204 | 340 | 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 | 340 | private static function findClosingQuote( |
|||
218 | $statement, |
||||
219 | &$tokenOffset, |
||||
220 | &$currentLiteralDelimiter |
||||
221 | ) { |
||||
222 | 340 | $token = self::findToken( |
|||
223 | 340 | $statement, |
|||
224 | 340 | $tokenOffset, |
|||
0 ignored issues
–
show
$tokenOffset of type string is incompatible with the type integer expected by parameter $offset of Doctrine\DBAL\Driver\OCI...8Statement::findToken() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
225 | 340 | '/' . preg_quote($currentLiteralDelimiter, '/') . '/' |
|||
226 | ); |
||||
227 | |||||
228 | 340 | if (! $token) { |
|||
229 | 6 | return false; |
|||
230 | } |
||||
231 | |||||
232 | 336 | $currentLiteralDelimiter = false; |
|||
233 | 336 | ++$tokenOffset; |
|||
234 | |||||
235 | 336 | 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 int $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 | 538 | private static function findToken($statement, &$offset, $regex) |
|||
249 | { |
||||
250 | 538 | if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) { |
|||
251 | 503 | $offset = $matches[0][1]; |
|||
252 | 503 | return $matches[0][0]; |
|||
253 | } |
||||
254 | |||||
255 | 538 | return null; |
|||
256 | } |
||||
257 | |||||
258 | /** |
||||
259 | * {@inheritdoc} |
||||
260 | */ |
||||
261 | 118 | public function bindValue($param, $value, $type = ParameterType::STRING) |
|||
262 | { |
||||
263 | 118 | return $this->bindParam($param, $value, $type, null); |
|||
264 | } |
||||
265 | |||||
266 | /** |
||||
267 | * {@inheritdoc} |
||||
268 | */ |
||||
269 | 123 | public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) |
|||
270 | { |
||||
271 | 123 | $column = $this->_paramMap[$column] ?? $column; |
|||
272 | |||||
273 | 123 | if ($type === ParameterType::LARGE_OBJECT) { |
|||
274 | 4 | $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB); |
|||
275 | 4 | $lob->writeTemporary($variable, OCI_TEMP_BLOB); |
|||
276 | |||||
277 | 4 | $variable =& $lob; |
|||
278 | } |
||||
279 | |||||
280 | 123 | $this->boundValues[$column] =& $variable; |
|||
281 | |||||
282 | 123 | return oci_bind_by_name( |
|||
283 | 123 | $this->_sth, |
|||
284 | 123 | $column, |
|||
285 | 123 | $variable, |
|||
286 | 123 | $length ?? -1, |
|||
287 | 123 | $this->convertParameterType($type) |
|||
288 | ); |
||||
289 | } |
||||
290 | |||||
291 | /** |
||||
292 | * Converts DBAL parameter type to oci8 parameter type |
||||
293 | */ |
||||
294 | 123 | private function convertParameterType(int $type) : int |
|||
295 | { |
||||
296 | 123 | switch ($type) { |
|||
297 | case ParameterType::BINARY: |
||||
298 | 1 | return OCI_B_BIN; |
|||
299 | |||||
300 | case ParameterType::LARGE_OBJECT: |
||||
301 | 4 | return OCI_B_BLOB; |
|||
302 | |||||
303 | default: |
||||
304 | 121 | return SQLT_CHR; |
|||
305 | } |
||||
306 | } |
||||
307 | |||||
308 | /** |
||||
309 | * {@inheritdoc} |
||||
310 | */ |
||||
311 | 20 | public function closeCursor() |
|||
312 | { |
||||
313 | // not having the result means there's nothing to close |
||||
314 | 20 | if (! $this->result) { |
|||
315 | 4 | return true; |
|||
316 | } |
||||
317 | |||||
318 | 16 | oci_cancel($this->_sth); |
|||
319 | |||||
320 | 16 | $this->result = false; |
|||
321 | |||||
322 | 16 | return true; |
|||
323 | } |
||||
324 | |||||
325 | /** |
||||
326 | * {@inheritdoc} |
||||
327 | */ |
||||
328 | 4 | public function columnCount() |
|||
329 | { |
||||
330 | 4 | 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 | 142 | public function errorInfo() |
|||
350 | { |
||||
351 | 142 | return oci_error($this->_sth); |
|||
352 | } |
||||
353 | |||||
354 | /** |
||||
355 | * {@inheritdoc} |
||||
356 | */ |
||||
357 | 265 | public function execute($params = null) |
|||
358 | { |
||||
359 | 265 | if ($params) { |
|||
360 | 93 | $hasZeroIndex = array_key_exists(0, $params); |
|||
361 | 93 | foreach ($params as $key => $val) { |
|||
362 | 93 | if ($hasZeroIndex && is_numeric($key)) { |
|||
363 | 93 | $this->bindValue($key + 1, $val); |
|||
364 | } else { |
||||
365 | 93 | $this->bindValue($key, $val); |
|||
366 | } |
||||
367 | } |
||||
368 | } |
||||
369 | |||||
370 | 261 | $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode()); |
|||
371 | 261 | if (! $ret) { |
|||
372 | 146 | throw OCI8Exception::fromErrorInfo($this->errorInfo()); |
|||
373 | } |
||||
374 | |||||
375 | 256 | $this->result = true; |
|||
376 | |||||
377 | 256 | return $ret; |
|||
378 | } |
||||
379 | |||||
380 | /** |
||||
381 | * {@inheritdoc} |
||||
382 | */ |
||||
383 | 229 | public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) |
|||
384 | { |
||||
385 | 229 | $this->_defaultFetchMode = $fetchMode; |
|||
386 | |||||
387 | 229 | return true; |
|||
388 | } |
||||
389 | |||||
390 | /** |
||||
391 | * {@inheritdoc} |
||||
392 | */ |
||||
393 | 5 | public function getIterator() |
|||
394 | { |
||||
395 | 5 | return new StatementIterator($this); |
|||
396 | } |
||||
397 | |||||
398 | /** |
||||
399 | * {@inheritdoc} |
||||
400 | */ |
||||
401 | 78 | 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 | 78 | if (! $this->result) { |
|||
406 | 3 | return false; |
|||
407 | } |
||||
408 | |||||
409 | 75 | $fetchMode = $fetchMode ?: $this->_defaultFetchMode; |
|||
410 | |||||
411 | 75 | if ($fetchMode === FetchMode::COLUMN) { |
|||
412 | 1 | return $this->fetchColumn(); |
|||
413 | } |
||||
414 | |||||
415 | 74 | if ($fetchMode === FetchMode::STANDARD_OBJECT) { |
|||
416 | 1 | return oci_fetch_object($this->_sth); |
|||
417 | } |
||||
418 | |||||
419 | 73 | if (! isset(self::$fetchModeMap[$fetchMode])) { |
|||
420 | throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode); |
||||
421 | } |
||||
422 | |||||
423 | 73 | return oci_fetch_array( |
|||
424 | 73 | $this->_sth, |
|||
425 | 73 | self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS |
|||
426 | ); |
||||
427 | } |
||||
428 | |||||
429 | /** |
||||
430 | * {@inheritdoc} |
||||
431 | */ |
||||
432 | 99 | public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) |
|||
433 | { |
||||
434 | 99 | $fetchMode = $fetchMode ?: $this->_defaultFetchMode; |
|||
435 | |||||
436 | 99 | $result = []; |
|||
437 | |||||
438 | 99 | if ($fetchMode === FetchMode::STANDARD_OBJECT) { |
|||
439 | 1 | while ($row = $this->fetch($fetchMode)) { |
|||
440 | 1 | $result[] = $row; |
|||
441 | } |
||||
442 | |||||
443 | 1 | return $result; |
|||
444 | } |
||||
445 | |||||
446 | 98 | if (! isset(self::$fetchModeMap[$fetchMode])) { |
|||
447 | throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode); |
||||
448 | } |
||||
449 | |||||
450 | 98 | if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) { |
|||
451 | 1 | while ($row = $this->fetch($fetchMode)) { |
|||
452 | 1 | $result[] = $row; |
|||
453 | } |
||||
454 | } else { |
||||
455 | 97 | $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW; |
|||
456 | |||||
457 | 97 | if ($fetchMode === FetchMode::COLUMN) { |
|||
458 | 6 | $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 | 97 | if (! $this->result) { |
|||
464 | 3 | return []; |
|||
465 | } |
||||
466 | |||||
467 | 94 | oci_fetch_all( |
|||
468 | 94 | $this->_sth, |
|||
469 | 94 | $result, |
|||
470 | 94 | 0, |
|||
471 | 94 | -1, |
|||
472 | 94 | self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS |
|||
473 | ); |
||||
474 | |||||
475 | 94 | if ($fetchMode === FetchMode::COLUMN) { |
|||
476 | 6 | $result = $result[0]; |
|||
477 | } |
||||
478 | } |
||||
479 | |||||
480 | 95 | return $result; |
|||
481 | } |
||||
482 | |||||
483 | /** |
||||
484 | * {@inheritdoc} |
||||
485 | */ |
||||
486 | 52 | 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 | 52 | if (! $this->result) { |
|||
491 | 3 | return false; |
|||
492 | } |
||||
493 | |||||
494 | 49 | $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS); |
|||
495 | |||||
496 | 49 | if ($row === false) { |
|||
497 | 3 | return false; |
|||
498 | } |
||||
499 | |||||
500 | 46 | return $row[$columnIndex] ?? null; |
|||
501 | } |
||||
502 | |||||
503 | /** |
||||
504 | * {@inheritdoc} |
||||
505 | */ |
||||
506 | 171 | public function rowCount() |
|||
507 | { |
||||
508 | 171 | return oci_num_rows($this->_sth); |
|||
509 | } |
||||
510 | } |
||||
511 |