1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Class MySQLiDriver |
4
|
|
|
* |
5
|
|
|
* @filesource MySQLiDriver.php |
6
|
|
|
* @created 04.11.2015 |
7
|
|
|
* @package chillerlan\Database\Drivers\MySQLi |
8
|
|
|
* @author Smiley <[email protected]> |
9
|
|
|
* @copyright 2015 Smiley |
10
|
|
|
* @license MIT |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace chillerlan\Database\Drivers\MySQLi; |
14
|
|
|
|
15
|
|
|
use chillerlan\Database\DBException; |
16
|
|
|
use chillerlan\Database\Drivers\DBBaseDriver; |
17
|
|
|
use chillerlan\Database\Drivers\DBDriverInterface; |
18
|
|
|
use mysqli; |
19
|
|
|
use mysqli_result; |
20
|
|
|
use mysqli_sql_exception; |
21
|
|
|
use mysqli_stmt; |
22
|
|
|
use ReflectionClass; |
23
|
|
|
use ReflectionMethod; |
24
|
|
|
use stdClass; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* |
28
|
|
|
*/ |
29
|
|
|
class MySQLiDriver extends DBBaseDriver implements DBDriverInterface{ |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Holds the database resource object |
33
|
|
|
* |
34
|
|
|
* @var mysqli |
35
|
|
|
*/ |
36
|
|
|
protected $db; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Establishes a database connection and returns the connection object |
40
|
|
|
* |
41
|
|
|
* @return mysqli the database resource object |
42
|
|
|
* @throws DBException |
43
|
|
|
*/ |
44
|
|
|
public function connect(){ |
45
|
|
|
|
46
|
|
|
if($this->db instanceof mysqli){ |
47
|
|
|
return $this->db; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
$this->db = mysqli_init(); |
|
|
|
|
51
|
|
|
|
52
|
|
|
if(!$this->db->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->options->mysqli_timeout)){ |
53
|
|
|
throw new DBException('Could not set database timeout.'); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
if($this->options->use_ssl){ |
57
|
|
|
$this->db->ssl_set( |
58
|
|
|
$this->options->ssl_key, |
59
|
|
|
$this->options->ssl_cert, |
60
|
|
|
$this->options->ssl_ca, |
61
|
|
|
$this->options->ssl_capath, |
62
|
|
|
$this->options->ssl_cipher |
63
|
|
|
); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
if(!$this->db->real_connect( |
67
|
|
|
$this->options->host, |
68
|
|
|
$this->options->username, |
69
|
|
|
$this->options->password, |
70
|
|
|
$this->options->database, |
71
|
|
|
(int)$this->options->port, |
72
|
|
|
$this->options->socket |
73
|
|
|
)){ |
74
|
|
|
throw new DBException('Could not connect to the database.'); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @see https://mathiasbynens.be/notes/mysql-utf8mb4 How to support full Unicode in MySQL |
79
|
|
|
*/ |
80
|
|
|
if(!$this->db->set_charset($this->options->mysql_charset)){ |
81
|
|
|
throw new DBException('Could not set database character set.'); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
return $this->db; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Closes a database connection |
89
|
|
|
* |
90
|
|
|
* @return $this |
|
|
|
|
91
|
|
|
*/ |
92
|
|
|
public function disconnect(){ |
93
|
|
|
$this->db->close(); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Returns info about the used php client |
98
|
|
|
* |
99
|
|
|
* @return string php's database client string |
100
|
|
|
*/ |
101
|
|
|
public function getClientInfo(){ |
102
|
|
|
return $this->db->client_info; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Returns info about the database server |
107
|
|
|
* |
108
|
|
|
* @return string database's serverinfo string |
109
|
|
|
*/ |
110
|
|
|
public function getServerInfo(){ |
111
|
|
|
return $this->db->server_info; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Sanitizer. |
116
|
|
|
* |
117
|
|
|
* Recursively escapes string values, optional htmlspecialchars() |
118
|
|
|
* |
119
|
|
|
* @param array|string $data array or string to escape |
120
|
|
|
* @param bool $specialchars [optional] if true, it performs a htmlspecialchars() on each value given |
121
|
|
|
* |
122
|
|
|
* @return array|string array or string. escaped. obviously. |
123
|
|
|
*/ |
124
|
|
View Code Duplication |
public function escape($data, $specialchars = false){ |
|
|
|
|
125
|
|
|
|
126
|
|
|
if(is_array($data)){ |
127
|
|
|
|
128
|
|
|
foreach($data as $key => $value){ |
129
|
|
|
$data[$key] = $this->escape($value, $specialchars); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
} |
133
|
|
|
else if(is_object($data)){ |
134
|
|
|
|
135
|
|
|
foreach($data as $key => $value){ |
136
|
|
|
$data->{$key} = $this->escape($value, $specialchars); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
} |
140
|
|
|
else{ |
141
|
|
|
|
142
|
|
|
if($specialchars){ |
143
|
|
|
$data = htmlspecialchars($data, ENT_HTML5, 'UTF-8', false); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
$data = $this->db->real_escape_string($data); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
return $data; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Basic SQL query for non prepared statements |
154
|
|
|
* |
155
|
|
|
* There is no escaping in here, so make sure, your SQL is clean/escaped. |
156
|
|
|
* Also, your SQL should NEVER contain user input, use prepared statements in this case. |
157
|
|
|
* |
158
|
|
|
* If the query was successful it returns either an array of results or true |
159
|
|
|
* if it was a void query. On errors, a false will be returned, obviously. |
160
|
|
|
* |
161
|
|
|
* @param string $sql The SQL statement |
162
|
|
|
* @param string $index [optional] an index column to assingn as the result's keys |
163
|
|
|
* @param bool $assoc [optional] If true, the fields are named with the respective column names, otherwise numbered |
164
|
|
|
* @param bool $fetch_array [optional] fetch the vaues as array instead of object |
165
|
|
|
* |
166
|
|
|
* @return array|bool array with results, true on void query success, otherwise false. |
167
|
|
|
* @throws \chillerlan\Database\DBException |
168
|
|
|
*/ |
169
|
|
|
public function raw($sql, $index = '', $assoc = true, $fetch_array = false){ |
170
|
|
|
|
171
|
|
|
try{ |
172
|
|
|
$result = $this->db->query($sql); |
173
|
|
|
|
174
|
|
|
$this->addStats([ |
175
|
|
|
'affected_rows' => $this->db->affected_rows, |
176
|
|
|
'error' => $this->db->error_list, |
177
|
|
|
'insert_id' => $this->db->insert_id, |
178
|
|
|
'sql' => $sql, |
179
|
|
|
'index' => $index, |
180
|
|
|
'assoc' => $assoc, |
181
|
|
|
'fetch_array' => $fetch_array, |
182
|
|
|
]); |
183
|
|
|
|
184
|
|
|
if(is_bool($result)){ |
185
|
|
|
return $result; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
$out = []; |
189
|
|
|
$method = 'fetch_'.($assoc ? ($fetch_array ? 'assoc' : 'object') : 'row'); |
190
|
|
|
$i = 0 ; |
191
|
|
|
|
192
|
|
|
// ok, we have a result with one or more rows, loop out the rows and output as array |
193
|
|
View Code Duplication |
while($row = $result->{$method}()){ |
|
|
|
|
194
|
|
|
$key = $i; |
195
|
|
|
|
196
|
|
|
if($assoc && !empty($index)){ |
197
|
|
|
|
198
|
|
|
if($fetch_array && isset($row[$index])){ |
199
|
|
|
$key = $row[$index]; |
200
|
|
|
} |
201
|
|
|
elseif(isset($row->{$index})){ |
202
|
|
|
$key = $row->{$index}; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$out[$key] = $row; |
208
|
|
|
$i++; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
$result->free(); |
212
|
|
|
|
213
|
|
|
return $out; |
214
|
|
|
} |
215
|
|
|
catch(mysqli_sql_exception $mysqli_sql_exception){ |
216
|
|
|
throw new DBException($mysqli_sql_exception->getMessage()); |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Prepared statements wrapper |
222
|
|
|
* |
223
|
|
|
* Does everything for you: prepares the statement and fetches the results as an object or array |
224
|
|
|
* just pass a query along with values and you're done. Not meant for multi-inserts. |
225
|
|
|
* |
226
|
|
|
* @param string $sql The SQL statement to prepare |
227
|
|
|
* @param array $values [optional] the value for each "?" in the statement - in the respective order, of course |
228
|
|
|
* @param string $index [optional] an index column to assingn as the result's keys |
229
|
|
|
* @param bool $assoc [optional] If true, the fields are named with the respective column names, otherwise numbered |
230
|
|
|
* @param bool $fetch_array [optional] fetch the vaues as array instead of object |
231
|
|
|
* |
232
|
|
|
* @return array|bool Array with results, true on void query success, otherwise false |
233
|
|
|
* @throws \chillerlan\Database\DBException |
234
|
|
|
*/ |
235
|
|
|
public function prepared($sql, array $values = [], $index = '', $assoc = true, $fetch_array = false){ |
236
|
|
|
$stmt = $this->db->stmt_init(); |
237
|
|
|
|
238
|
|
|
if($stmt->prepare($sql)){ |
239
|
|
|
throw new DBException('could not prepare statement ('.$sql.')'); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
$types = $this->getTypes($values); |
243
|
|
|
|
244
|
|
|
if(count($values) > 0){ |
245
|
|
|
$references = $this->getReferences($values); |
246
|
|
|
array_unshift($references, $types); |
247
|
|
|
|
248
|
|
|
(new ReflectionMethod($stmt, 'bind_param'))->invokeArgs($stmt, $references); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
$stmt->execute(); |
252
|
|
|
|
253
|
|
|
$this->addStats([ |
254
|
|
|
'affected_rows' => $stmt->affected_rows, |
255
|
|
|
'error' => $stmt->error_list, |
256
|
|
|
'insert_id' => $stmt->insert_id, |
257
|
|
|
'sql' => $sql, |
258
|
|
|
'values' => $values, |
259
|
|
|
'types' => $types, |
260
|
|
|
'index' => $index, |
261
|
|
|
'assoc' => $assoc, |
262
|
|
|
'fetch_array' => $fetch_array, |
263
|
|
|
]); |
264
|
|
|
|
265
|
|
|
$result = $stmt->result_metadata(); |
266
|
|
|
|
267
|
|
|
// void result |
268
|
|
|
if(!$result){ |
269
|
|
|
return true; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return $this->getResult($stmt, $result, $assoc, $fetch_array); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Prepared multi line insert |
277
|
|
|
* |
278
|
|
|
* Prepared statement multi insert/update |
279
|
|
|
* |
280
|
|
|
* @param string $sql The SQL statement to prepare |
281
|
|
|
* @param array $values a multidimensional array with the values, each row represents one line to insert. |
282
|
|
|
* |
283
|
|
|
* @return bool true query success, otherwise false |
284
|
|
|
* @throws \chillerlan\Database\DBException |
285
|
|
|
*/ |
286
|
|
|
public function multi($sql, array $values){ |
287
|
|
|
|
288
|
|
|
if(!is_array($values) || count($values) < 1 || !is_array($values[0]) || count($values[0]) < 1){ |
289
|
|
|
throw new DBException('invalid data'); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
$stmt = $this->db->stmt_init(); |
293
|
|
|
|
294
|
|
|
if(!$stmt->prepare($sql)){ |
295
|
|
|
throw new DBException('could not prepare statement ('.$sql.')'); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$reflectionMethod = new ReflectionMethod($stmt, 'bind_param'); |
299
|
|
|
|
300
|
|
|
foreach($values as $row){ |
301
|
|
|
$this->insertPreparedRow($stmt, $reflectionMethod, $row); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
$stmt->close(); |
305
|
|
|
return true; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Prepared multi line insert/update with callback |
310
|
|
|
* @see https://gist.github.com/krakjoe/6437782 |
311
|
|
|
* @see https://gist.github.com/krakjoe/9384409 |
312
|
|
|
* |
313
|
|
|
* @param string $sql The SQL statement to prepare |
314
|
|
|
* @param array $data an array with the (raw) data to insert, each row represents one line to insert. |
315
|
|
|
* @param callable|array $callback a callback that processes the values for each row. |
316
|
|
|
* |
317
|
|
|
* @return bool true query success, otherwise false |
318
|
|
|
* @throws \chillerlan\Database\DBException |
319
|
|
|
*/ |
320
|
|
|
public function multi_callback($sql, array $data, $callback){ |
321
|
|
|
|
322
|
|
|
if(count($data) < 1){ |
323
|
|
|
throw new DBException('invalid data'); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
if(!is_callable($callback) || (is_array($callback) && (!isset($callback[0]) || !is_object($callback[0])))){ |
327
|
|
|
throw new DBException('invalid callback'); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
$stmt = $this->db->stmt_init(); |
331
|
|
|
|
332
|
|
|
if(!$stmt->prepare($sql)){ |
333
|
|
|
throw new DBException('could not prepare statement ('.$sql.')'); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
$reflectionMethod = new ReflectionMethod($stmt, 'bind_param'); |
337
|
|
|
|
338
|
|
|
foreach($data as $row){ |
339
|
|
|
if($row = call_user_func_array($callback, [$row])){ |
340
|
|
|
$this->insertPreparedRow($stmt, $reflectionMethod, $row); |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
$stmt->close(); |
345
|
|
|
|
346
|
|
|
return true; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* @param \mysqli_stmt $stmt |
351
|
|
|
* @param \ReflectionMethod $reflectionMethod |
352
|
|
|
* @param array $row |
353
|
|
|
*/ |
354
|
|
|
protected function insertPreparedRow(mysqli_stmt &$stmt, ReflectionMethod &$reflectionMethod, array &$row){ |
355
|
|
|
$references = $this->getReferences($row); |
356
|
|
|
array_unshift($references, $this->getTypes($references)); |
357
|
|
|
$reflectionMethod->invokeArgs($stmt, $references); |
358
|
|
|
$stmt->execute(); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* @param \mysqli_stmt $stmt |
363
|
|
|
* @param \mysqli_result $result |
364
|
|
|
* @param bool $assoc |
365
|
|
|
* @param bool $fetch_array |
366
|
|
|
* |
367
|
|
|
* @return array|bool |
368
|
|
|
*/ |
369
|
|
|
protected function getResult(mysqli_stmt &$stmt, mysqli_result &$result, $assoc, $fetch_array){ |
370
|
|
|
// get the columns and their references |
371
|
|
|
// http://php.net/manual/mysqli-stmt.bind-result.php |
372
|
|
|
$cols = $refs = []; |
373
|
|
|
|
374
|
|
|
foreach($result->fetch_fields() as $i => &$field){ |
|
|
|
|
375
|
|
|
$refs[] = &$cols[$assoc ? $field->name : $i]; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
(new ReflectionMethod($stmt, 'bind_result'))->invokeArgs($stmt, $refs); |
379
|
|
|
|
380
|
|
|
// fetch the data |
381
|
|
|
$output = []; |
382
|
|
|
$count = 0; |
383
|
|
|
|
384
|
|
|
while($stmt->fetch()){ |
385
|
|
|
$row = $fetch_array || !$assoc ? [] : new stdClass; |
386
|
|
|
|
387
|
|
|
foreach($cols as $field => &$field){ |
388
|
|
|
if($fetch_array || !$assoc){ |
389
|
|
|
$row[$field] = $field; |
390
|
|
|
} |
391
|
|
|
else{ |
392
|
|
|
$row->{$field} = $field; |
393
|
|
|
} |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
$key = !empty($index) && isset($cols[$index]) ? $cols[$index] : $count; |
397
|
|
|
$output[$key] = $row; |
398
|
|
|
|
399
|
|
|
$count++; |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
// KTHXBYE! |
403
|
|
|
$stmt->free_result(); |
404
|
|
|
$stmt->close(); |
405
|
|
|
|
406
|
|
|
return $count === 0 ? true : $output; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* Returns a string of types for the given values |
411
|
|
|
* |
412
|
|
|
* @link http://php.net/manual/mysqli-stmt.bind-param.php |
413
|
|
|
* |
414
|
|
|
* @param array $values |
415
|
|
|
* |
416
|
|
|
* @return string |
417
|
|
|
* @internal |
418
|
|
|
*/ |
419
|
|
|
protected function getTypes(array &$values){ |
420
|
|
|
|
421
|
|
|
$types = []; |
422
|
|
|
foreach($values as &$v){ |
423
|
|
|
switch(gettype($v)){ |
424
|
|
|
case 'integer': $types[] = 'i'; break; |
|
|
|
|
425
|
|
|
case 'double': $types[] ='d'; break; |
|
|
|
|
426
|
|
|
default: $types[] = 's'; break; |
|
|
|
|
427
|
|
|
} |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
return implode($types); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Copies an array to an array of referenced values |
435
|
|
|
* |
436
|
|
|
* @param array $row |
437
|
|
|
* |
438
|
|
|
* @return array |
439
|
|
|
* @see http://php.net/manual/mysqli-stmt.bind-param.php |
440
|
|
|
*/ |
441
|
|
|
protected function getReferences(array &$row){ |
442
|
|
|
$references = []; |
443
|
|
|
|
444
|
|
|
foreach($row as &$field){ |
445
|
|
|
$references[] = &$field; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
return $references; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
} |
452
|
|
|
|
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..