1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Doctrine\DBAL\Driver\Mysqli; |
4
|
|
|
|
5
|
|
|
use Doctrine\DBAL\Driver\Statement; |
6
|
|
|
use Doctrine\DBAL\Driver\StatementIterator; |
7
|
|
|
use Doctrine\DBAL\Exception\InvalidArgumentException; |
8
|
|
|
use Doctrine\DBAL\FetchMode; |
9
|
|
|
use Doctrine\DBAL\ParameterType; |
10
|
|
|
use IteratorAggregate; |
11
|
|
|
use mysqli; |
12
|
|
|
use mysqli_stmt; |
13
|
|
|
use PDO; |
14
|
|
|
use function array_combine; |
15
|
|
|
use function array_fill; |
16
|
|
|
use function assert; |
17
|
|
|
use function count; |
18
|
|
|
use function feof; |
19
|
|
|
use function fread; |
20
|
|
|
use function get_resource_type; |
21
|
|
|
use function is_array; |
22
|
|
|
use function is_int; |
23
|
|
|
use function is_resource; |
24
|
|
|
use function sprintf; |
25
|
|
|
use function str_repeat; |
26
|
|
|
|
27
|
|
|
class MysqliStatement implements IteratorAggregate, Statement |
28
|
|
|
{ |
29
|
|
|
/** @var string[] */ |
30
|
|
|
protected static $_paramTypeMap = [ |
31
|
|
|
ParameterType::STRING => 's', |
32
|
|
|
ParameterType::BINARY => 's', |
33
|
|
|
ParameterType::BOOLEAN => 'i', |
34
|
|
|
ParameterType::NULL => 's', |
35
|
|
|
ParameterType::INTEGER => 'i', |
36
|
|
|
ParameterType::LARGE_OBJECT => 'b', |
37
|
|
|
]; |
38
|
|
|
|
39
|
|
|
/** @var mysqli */ |
40
|
|
|
protected $_conn; |
41
|
|
|
|
42
|
|
|
/** @var mysqli_stmt */ |
43
|
|
|
protected $_stmt; |
44
|
|
|
|
45
|
|
|
/** @var string[]|false|null */ |
46
|
|
|
protected $_columnNames; |
47
|
|
|
|
48
|
|
|
/** @var mixed[] */ |
49
|
|
|
protected $_rowBindedValues = []; |
50
|
|
|
|
51
|
|
|
/** @var mixed[] */ |
52
|
|
|
protected $_bindedValues; |
53
|
|
|
|
54
|
|
|
/** @var string */ |
55
|
|
|
protected $types; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Contains ref values for bindValue(). |
59
|
|
|
* |
60
|
|
|
* @var mixed[] |
61
|
|
|
*/ |
62
|
|
|
protected $_values = []; |
63
|
|
|
|
64
|
|
|
/** @var int */ |
65
|
|
|
protected $_defaultFetchMode = FetchMode::MIXED; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Indicates whether the statement is in the state when fetching results is possible |
69
|
|
|
* |
70
|
|
|
* @var bool |
71
|
|
|
*/ |
72
|
|
|
private $result = false; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @param string $prepareString |
76
|
|
|
* |
77
|
|
|
* @throws MysqliException |
78
|
|
|
*/ |
79
|
1543 |
|
public function __construct(mysqli $conn, $prepareString) |
80
|
|
|
{ |
81
|
1543 |
|
$this->_conn = $conn; |
82
|
|
|
|
83
|
1543 |
|
$stmt = $conn->prepare($prepareString); |
84
|
|
|
|
85
|
1543 |
|
if ($stmt === false) { |
86
|
1177 |
|
throw new MysqliException($this->_conn->error, $this->_conn->sqlstate, $this->_conn->errno); |
87
|
|
|
} |
88
|
|
|
|
89
|
1543 |
|
$this->_stmt = $stmt; |
90
|
|
|
|
91
|
1543 |
|
$paramCount = $this->_stmt->param_count; |
92
|
1543 |
|
if (0 >= $paramCount) { |
93
|
1537 |
|
return; |
94
|
|
|
} |
95
|
|
|
|
96
|
1543 |
|
$this->types = str_repeat('s', $paramCount); |
97
|
1543 |
|
$this->_bindedValues = array_fill(1, $paramCount, null); |
98
|
1543 |
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* {@inheritdoc} |
102
|
|
|
*/ |
103
|
1513 |
|
public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) |
104
|
|
|
{ |
105
|
1513 |
|
assert(is_int($column)); |
106
|
|
|
|
107
|
1513 |
|
if (! isset(self::$_paramTypeMap[$type])) { |
108
|
|
|
throw new MysqliException(sprintf("Unknown type: '%s'", $type)); |
109
|
|
|
} |
110
|
|
|
|
111
|
1513 |
|
$this->_bindedValues[$column] =& $variable; |
112
|
1513 |
|
$this->types[$column - 1] = self::$_paramTypeMap[$type]; |
113
|
|
|
|
114
|
1513 |
|
return true; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* {@inheritdoc} |
119
|
|
|
*/ |
120
|
1543 |
|
public function bindValue($param, $value, $type = ParameterType::STRING) |
121
|
|
|
{ |
122
|
1543 |
|
assert(is_int($param)); |
123
|
|
|
|
124
|
1543 |
|
if (! isset(self::$_paramTypeMap[$type])) { |
125
|
|
|
throw new MysqliException(sprintf("Unknown type: '%s'", $type)); |
126
|
|
|
} |
127
|
|
|
|
128
|
1543 |
|
$this->_values[$param] = $value; |
129
|
1543 |
|
$this->_bindedValues[$param] =& $this->_values[$param]; |
130
|
1543 |
|
$this->types[$param - 1] = self::$_paramTypeMap[$type]; |
131
|
|
|
|
132
|
1543 |
|
return true; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* {@inheritdoc} |
137
|
|
|
*/ |
138
|
1543 |
|
public function execute($params = null) |
139
|
|
|
{ |
140
|
1543 |
|
if ($this->_bindedValues !== null) { |
141
|
1543 |
|
if ($params !== null) { |
142
|
1447 |
|
if (! $this->bindUntypedValues($params)) { |
143
|
1447 |
|
throw new MysqliException($this->_stmt->error, $this->_stmt->errno); |
144
|
|
|
} |
145
|
|
|
} else { |
146
|
1543 |
|
$this->bindTypedParameters(); |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
|
150
|
1543 |
View Code Duplication |
if (! $this->_stmt->execute()) { |
|
|
|
|
151
|
1183 |
|
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno); |
152
|
|
|
} |
153
|
|
|
|
154
|
1543 |
|
if ($this->_columnNames === null) { |
155
|
1543 |
|
$meta = $this->_stmt->result_metadata(); |
156
|
1543 |
|
if ($meta !== false) { |
157
|
1537 |
|
$fields = $meta->fetch_fields(); |
158
|
1537 |
|
assert(is_array($fields)); |
159
|
|
|
|
160
|
1537 |
|
$columnNames = []; |
161
|
1537 |
|
foreach ($fields as $col) { |
162
|
1537 |
|
$columnNames[] = $col->name; |
163
|
|
|
} |
164
|
|
|
|
165
|
1537 |
|
$meta->free(); |
166
|
|
|
|
167
|
1537 |
|
$this->_columnNames = $columnNames; |
168
|
|
|
} else { |
169
|
1543 |
|
$this->_columnNames = false; |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
1543 |
|
if ($this->_columnNames !== false) { |
174
|
|
|
// Store result of every execution which has it. Otherwise it will be impossible |
175
|
|
|
// to execute a new statement in case if the previous one has non-fetched rows |
176
|
|
|
// @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html |
177
|
1537 |
|
$this->_stmt->store_result(); |
178
|
|
|
|
179
|
|
|
// Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql, |
180
|
|
|
// it will have to allocate as much memory as it may be needed for the given column type |
181
|
|
|
// (e.g. for a LONGBLOB field it's 4 gigabytes) |
182
|
|
|
// @link https://bugs.php.net/bug.php?id=51386#1270673122 |
183
|
|
|
// |
184
|
|
|
// Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been |
185
|
|
|
// previously called on the statement, the values are unbound making the statement unusable. |
186
|
|
|
// |
187
|
|
|
// It's also important that row values are bound after _each_ call to store_result(). Otherwise, |
188
|
|
|
// if mysqli is compiled with libmysql, subsequently fetched string values will get truncated |
189
|
|
|
// to the length of the ones fetched during the previous execution. |
190
|
1537 |
|
$this->_rowBindedValues = array_fill(0, count($this->_columnNames), null); |
191
|
|
|
|
192
|
1537 |
|
$refs = []; |
193
|
1537 |
|
foreach ($this->_rowBindedValues as $key => &$value) { |
194
|
1537 |
|
$refs[$key] =& $value; |
195
|
|
|
} |
196
|
|
|
|
197
|
1537 |
View Code Duplication |
if (! $this->_stmt->bind_result(...$refs)) { |
|
|
|
|
198
|
|
|
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno); |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
|
202
|
1543 |
|
$this->result = true; |
203
|
|
|
|
204
|
1543 |
|
return true; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Binds parameters with known types previously bound to the statement |
209
|
|
|
*/ |
210
|
1543 |
|
private function bindTypedParameters() : void |
211
|
|
|
{ |
212
|
1543 |
|
$streams = $values = []; |
213
|
1543 |
|
$types = $this->types; |
214
|
|
|
|
215
|
1543 |
|
foreach ($this->_bindedValues as $parameter => $value) { |
216
|
1543 |
|
assert(is_int($parameter)); |
217
|
|
|
|
218
|
1543 |
|
if (! isset($types[$parameter - 1])) { |
219
|
|
|
$types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING]; |
220
|
|
|
} |
221
|
|
|
|
222
|
1543 |
|
if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) { |
223
|
1543 |
|
if (is_resource($value)) { |
224
|
1537 |
|
if (get_resource_type($value) !== 'stream') { |
225
|
|
|
throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.'); |
226
|
|
|
} |
227
|
|
|
|
228
|
1537 |
|
$streams[$parameter] = $value; |
229
|
1537 |
|
$values[$parameter] = null; |
230
|
1537 |
|
continue; |
231
|
|
|
} |
232
|
|
|
|
233
|
1543 |
|
$types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING]; |
234
|
|
|
} |
235
|
|
|
|
236
|
1543 |
|
$values[$parameter] = $value; |
237
|
|
|
} |
238
|
|
|
|
239
|
1543 |
View Code Duplication |
if (! $this->_stmt->bind_param($types, ...$values)) { |
|
|
|
|
240
|
|
|
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno); |
241
|
|
|
} |
242
|
|
|
|
243
|
1543 |
|
$this->sendLongData($streams); |
244
|
1543 |
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Handle $this->_longData after regular query parameters have been bound |
248
|
|
|
* |
249
|
|
|
* @param array<int, resource> $streams |
250
|
|
|
* |
251
|
|
|
* @throws MysqliException |
252
|
|
|
*/ |
253
|
1543 |
|
private function sendLongData(array $streams) : void |
254
|
|
|
{ |
255
|
1543 |
|
foreach ($streams as $paramNr => $stream) { |
256
|
1537 |
|
while (! feof($stream)) { |
257
|
1537 |
|
$chunk = fread($stream, 8192); |
258
|
|
|
|
259
|
1537 |
|
if ($chunk === false) { |
260
|
|
|
throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}."); |
261
|
|
|
} |
262
|
|
|
|
263
|
1537 |
View Code Duplication |
if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) { |
|
|
|
|
264
|
|
|
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno); |
265
|
|
|
} |
266
|
|
|
} |
267
|
|
|
} |
268
|
1543 |
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Binds a array of values to bound parameters. |
272
|
|
|
* |
273
|
|
|
* @param mixed[] $values |
274
|
|
|
* |
275
|
|
|
* @return bool |
276
|
|
|
*/ |
277
|
1447 |
|
private function bindUntypedValues(array $values) |
278
|
|
|
{ |
279
|
1447 |
|
$params = []; |
280
|
1447 |
|
$types = str_repeat('s', count($values)); |
281
|
|
|
|
282
|
1447 |
|
foreach ($values as &$v) { |
283
|
1447 |
|
$params[] =& $v; |
284
|
|
|
} |
285
|
|
|
|
286
|
1447 |
|
return $this->_stmt->bind_param($types, ...$params); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @return mixed[]|false|null |
291
|
|
|
*/ |
292
|
1537 |
|
private function _fetch() |
293
|
|
|
{ |
294
|
1537 |
|
$ret = $this->_stmt->fetch(); |
295
|
|
|
|
296
|
1537 |
|
if ($ret === true) { |
297
|
1537 |
|
$values = []; |
298
|
1537 |
|
foreach ($this->_rowBindedValues as $v) { |
299
|
1537 |
|
$values[] = $v; |
300
|
|
|
} |
301
|
|
|
|
302
|
1537 |
|
return $values; |
303
|
|
|
} |
304
|
|
|
|
305
|
1537 |
|
return $ret; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* {@inheritdoc} |
310
|
|
|
*/ |
311
|
1537 |
|
public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) |
312
|
|
|
{ |
313
|
|
|
// do not try fetching from the statement if it's not expected to contain result |
314
|
|
|
// in order to prevent exceptional situation |
315
|
1537 |
|
if (! $this->result) { |
316
|
299 |
|
return false; |
317
|
|
|
} |
318
|
|
|
|
319
|
1537 |
|
$fetchMode = $fetchMode ?: $this->_defaultFetchMode; |
320
|
|
|
|
321
|
1537 |
|
if ($fetchMode === FetchMode::COLUMN) { |
322
|
233 |
|
return $this->fetchColumn(); |
323
|
|
|
} |
324
|
|
|
|
325
|
1537 |
|
$values = $this->_fetch(); |
326
|
|
|
|
327
|
1537 |
|
if ($values === null) { |
328
|
1537 |
|
return false; |
329
|
|
|
} |
330
|
|
|
|
331
|
1537 |
View Code Duplication |
if ($values === false) { |
|
|
|
|
332
|
|
|
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno); |
333
|
|
|
} |
334
|
|
|
|
335
|
1537 |
|
if ($fetchMode === FetchMode::NUMERIC) { |
336
|
1537 |
|
return $values; |
337
|
|
|
} |
338
|
|
|
|
339
|
1489 |
|
assert(is_array($this->_columnNames)); |
340
|
1489 |
|
$assoc = array_combine($this->_columnNames, $values); |
341
|
1489 |
|
assert(is_array($assoc)); |
342
|
|
|
|
343
|
1489 |
|
switch ($fetchMode) { |
344
|
|
|
case FetchMode::ASSOCIATIVE: |
345
|
1489 |
|
return $assoc; |
346
|
|
|
|
347
|
|
|
case FetchMode::MIXED: |
348
|
1471 |
|
return $assoc + $values; |
349
|
|
|
|
350
|
|
|
case FetchMode::STANDARD_OBJECT: |
351
|
1231 |
|
return (object) $assoc; |
352
|
|
|
|
353
|
|
|
default: |
354
|
|
|
throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode)); |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* {@inheritdoc} |
360
|
|
|
*/ |
361
|
1537 |
|
public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) |
362
|
|
|
{ |
363
|
1537 |
|
$fetchMode = $fetchMode ?: $this->_defaultFetchMode; |
364
|
|
|
|
365
|
1537 |
|
$rows = []; |
366
|
|
|
|
367
|
1537 |
|
if ($fetchMode === FetchMode::COLUMN) { |
368
|
1537 |
|
while (($row = $this->fetchColumn()) !== false) { |
369
|
1537 |
|
$rows[] = $row; |
370
|
|
|
} |
371
|
|
|
} else { |
372
|
1477 |
|
while (($row = $this->fetch($fetchMode)) !== false) { |
373
|
1477 |
|
$rows[] = $row; |
374
|
|
|
} |
375
|
|
|
} |
376
|
|
|
|
377
|
1537 |
|
return $rows; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* {@inheritdoc} |
382
|
|
|
*/ |
383
|
1537 |
View Code Duplication |
public function fetchColumn($columnIndex = 0) |
|
|
|
|
384
|
|
|
{ |
385
|
1537 |
|
$row = $this->fetch(FetchMode::NUMERIC); |
386
|
|
|
|
387
|
1537 |
|
if ($row === false) { |
388
|
1537 |
|
return false; |
389
|
|
|
} |
390
|
|
|
|
391
|
1537 |
|
return $row[$columnIndex] ?? null; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* {@inheritdoc} |
396
|
|
|
*/ |
397
|
|
|
public function errorCode() |
398
|
|
|
{ |
399
|
|
|
return $this->_stmt->errno; |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* {@inheritdoc} |
404
|
|
|
*/ |
405
|
|
|
public function errorInfo() |
406
|
|
|
{ |
407
|
|
|
return $this->_stmt->error; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* {@inheritdoc} |
412
|
|
|
*/ |
413
|
912 |
|
public function closeCursor() |
414
|
|
|
{ |
415
|
912 |
|
$this->_stmt->free_result(); |
416
|
912 |
|
$this->result = false; |
417
|
|
|
|
418
|
912 |
|
return true; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* {@inheritdoc} |
423
|
|
|
*/ |
424
|
1543 |
|
public function rowCount() |
425
|
|
|
{ |
426
|
1543 |
|
if ($this->_columnNames === false) { |
427
|
1543 |
|
return $this->_stmt->affected_rows; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
return $this->_stmt->num_rows; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* {@inheritdoc} |
435
|
|
|
*/ |
436
|
912 |
|
public function columnCount() |
437
|
|
|
{ |
438
|
912 |
|
return $this->_stmt->field_count; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* {@inheritdoc} |
443
|
|
|
*/ |
444
|
1537 |
|
public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) |
445
|
|
|
{ |
446
|
1537 |
|
$this->_defaultFetchMode = $fetchMode; |
447
|
|
|
|
448
|
1537 |
|
return true; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* {@inheritdoc} |
453
|
|
|
*/ |
454
|
1590 |
|
public function getIterator() |
455
|
|
|
{ |
456
|
1590 |
|
return new StatementIterator($this); |
457
|
|
|
} |
458
|
|
|
} |
459
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.