|
1
|
|
|
<?php |
|
2
|
|
|
namespace Darya\Database\Connection; |
|
3
|
|
|
|
|
4
|
|
|
use mysqli as php_mysqli; |
|
5
|
|
|
use mysqli_result; |
|
6
|
|
|
use mysqli_stmt; |
|
7
|
|
|
use ReflectionClass; |
|
8
|
|
|
use Darya\Database\AbstractConnection; |
|
9
|
|
|
use Darya\Database\Error; |
|
10
|
|
|
use Darya\Database\Result; |
|
11
|
|
|
use Darya\Database\Query; |
|
12
|
|
|
use Darya\Database\Query\Translator; |
|
13
|
|
|
|
|
14
|
|
|
/** |
|
15
|
|
|
* Darya's MySQL database interface. Uses mysqli. |
|
16
|
|
|
* |
|
17
|
|
|
* @author Chris Andrew <[email protected]> |
|
18
|
|
|
*/ |
|
19
|
|
|
class MySql extends AbstractConnection { |
|
20
|
|
|
|
|
21
|
|
|
/** |
|
22
|
|
|
* Copy a flat array. Aids copying fetched results without the mysqlnd |
|
23
|
|
|
* extension installed without retaining references to array elements. |
|
24
|
|
|
* |
|
25
|
|
|
* Who knew references could be so awkward to get rid of? |
|
26
|
|
|
* |
|
27
|
|
|
* @param array $array |
|
28
|
|
|
* @return array |
|
29
|
|
|
*/ |
|
30
|
|
|
protected static function copyArray($array) { |
|
31
|
|
|
$copy = array(); |
|
32
|
|
|
|
|
33
|
|
|
foreach ($array as $key => $value) { |
|
34
|
|
|
$copy[$key] = $value; |
|
35
|
|
|
} |
|
36
|
|
|
|
|
37
|
|
|
return $copy; |
|
38
|
|
|
} |
|
39
|
|
|
|
|
40
|
|
|
/** |
|
41
|
|
|
* Fetch result data from the given MySQLi statement. |
|
42
|
|
|
* |
|
43
|
|
|
* Expects the statement to have been executed. |
|
44
|
|
|
* |
|
45
|
|
|
* Attempts to use mysqli_stmt::get_result() and mysqli_result::fetch_all(), |
|
46
|
|
|
* but falls back to fetching from the statement directly if get_result() |
|
47
|
|
|
* isn't found (mysqlnd isn't installed). |
|
48
|
|
|
* |
|
49
|
|
|
* @param mysqli_stmt $statement |
|
50
|
|
|
* @return array array($data, $fields, $count) |
|
51
|
|
|
*/ |
|
52
|
|
|
protected function fetchResult(mysqli_stmt $statement) { |
|
53
|
|
|
if (!method_exists($statement, 'get_result')) { |
|
54
|
|
|
return $this->fetchResultWithoutNativeDriver($statement); |
|
55
|
|
|
} |
|
56
|
|
|
|
|
57
|
|
|
$result = $statement->get_result(); |
|
58
|
|
|
|
|
59
|
|
|
if (is_object($result) && $result instanceof mysqli_result) { |
|
60
|
|
|
return array( |
|
61
|
|
|
$result->fetch_all(MYSQL_ASSOC), |
|
62
|
|
|
$result->fetch_fields(), |
|
63
|
|
|
$result->num_rows |
|
64
|
|
|
); |
|
65
|
|
|
} |
|
66
|
|
|
|
|
67
|
|
|
return array(array(), array(), null); |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
/** |
|
71
|
|
|
* Method for fetching the same information in a way that doesn't require |
|
72
|
|
|
* mysqlnd to be installed. |
|
73
|
|
|
* |
|
74
|
|
|
* Fetches directly from the statement with variable binding instead. |
|
75
|
|
|
* |
|
76
|
|
|
* @param mysqli_stmt $statement |
|
77
|
|
|
* @return array |
|
78
|
|
|
*/ |
|
79
|
|
|
protected function fetchResultWithoutNativeDriver(mysqli_stmt $statement) { |
|
80
|
|
|
$statement->store_result(); |
|
81
|
|
|
|
|
82
|
|
|
$data = array(); |
|
83
|
|
|
$metadata = $statement->result_metadata(); |
|
84
|
|
|
|
|
85
|
|
|
$row = array(); |
|
86
|
|
|
$count = 0; |
|
87
|
|
|
$fields = array(); |
|
88
|
|
|
$arguments = array(); |
|
89
|
|
|
|
|
90
|
|
|
if ($metadata) { |
|
91
|
|
|
while ($field = $metadata->fetch_field()) { |
|
92
|
|
|
$fields[] = (array) $field; |
|
93
|
|
|
$arguments[] = &$row[$field->name]; |
|
94
|
|
|
} |
|
95
|
|
|
|
|
96
|
|
|
call_user_func_array(array($statement, 'bind_result'), $arguments); |
|
97
|
|
|
|
|
98
|
|
|
while ($statement->fetch()) { |
|
99
|
|
|
$data[] = static::copyArray($row); |
|
100
|
|
|
$count++; |
|
101
|
|
|
} |
|
102
|
|
|
|
|
103
|
|
|
} |
|
104
|
|
|
|
|
105
|
|
|
return array($data, $fields, $count); |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
/** |
|
109
|
|
|
* Retrieve the type of a variable for binding mysqli parameters. |
|
110
|
|
|
* |
|
111
|
|
|
* @param mixed $parameter |
|
112
|
|
|
* @return string |
|
113
|
|
|
*/ |
|
114
|
|
|
protected function prepareType($parameter) { |
|
115
|
|
|
if (is_int($parameter)) { |
|
116
|
|
|
return 'i'; |
|
117
|
|
|
} |
|
118
|
|
|
|
|
119
|
|
|
if (is_float($parameter)) { |
|
120
|
|
|
return 'd'; |
|
121
|
|
|
} |
|
122
|
|
|
|
|
123
|
|
|
return 's'; |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
/** |
|
127
|
|
|
* Prepares an array of values as an array of references to those values. |
|
128
|
|
|
* |
|
129
|
|
|
* Required for PHP 5.3+ to prevent warnings when dynamically invoking |
|
130
|
|
|
* mysqli_stmt::bind_param(). |
|
131
|
|
|
* |
|
132
|
|
|
* @param array $parameters |
|
133
|
|
|
* @return array |
|
134
|
|
|
*/ |
|
135
|
|
|
protected function prepareReferences(array $parameters) { |
|
136
|
|
|
$references = array(); |
|
137
|
|
|
|
|
138
|
|
|
foreach ($parameters as $key => $value) { |
|
139
|
|
|
$references[$key] = &$parameters[$key]; |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
return $references; |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
/** |
|
146
|
|
|
* Prepare the given query and parameters as a mysqli statement. |
|
147
|
|
|
* |
|
148
|
|
|
* @param string $query |
|
149
|
|
|
* @param array $parameters [optional] |
|
150
|
|
|
* @return \mysqli_stmt |
|
151
|
|
|
*/ |
|
152
|
|
|
protected function prepareStatement($query, $parameters = array()) { |
|
153
|
|
|
$statement = $this->connection->stmt_init(); |
|
154
|
|
|
|
|
155
|
|
|
if (!$statement->prepare($query)) { |
|
156
|
|
|
return $statement; |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
if (empty($parameters)) { |
|
160
|
|
|
return $statement; |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
$types = ''; |
|
164
|
|
|
|
|
165
|
|
|
foreach ((array) $parameters as $parameter) { |
|
166
|
|
|
$types .= $this->prepareType($parameter); |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
array_unshift($parameters, $types); |
|
170
|
|
|
|
|
171
|
|
|
call_user_func_array( |
|
172
|
|
|
array($statement, 'bind_param'), |
|
173
|
|
|
$this->prepareReferences($parameters) |
|
174
|
|
|
); |
|
175
|
|
|
|
|
176
|
|
|
return $statement; |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
/** |
|
180
|
|
|
* Prepare a result array using the given mysqli statement. |
|
181
|
|
|
* |
|
182
|
|
|
* @param mysqli_stmt $statement |
|
183
|
|
|
* @return array |
|
184
|
|
|
*/ |
|
185
|
|
|
protected function prepareStatementResult(mysqli_stmt $statement) { |
|
186
|
|
|
list($data, $fields, $count) = $this->fetchResult($statement); |
|
187
|
|
|
|
|
188
|
|
|
$result = array( |
|
189
|
|
|
'data' => $data, |
|
190
|
|
|
'fields' => $fields, |
|
191
|
|
|
'affected' => $statement->affected_rows, |
|
192
|
|
|
'num_rows' => $count, |
|
193
|
|
|
'insert_id' => $statement->insert_id |
|
194
|
|
|
); |
|
195
|
|
|
|
|
196
|
|
|
$statement->free_result(); |
|
197
|
|
|
|
|
198
|
|
|
return $result; |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
/** |
|
202
|
|
|
* Initiate the connection. |
|
203
|
|
|
* |
|
204
|
|
|
* @return bool |
|
205
|
|
|
*/ |
|
206
|
|
|
public function connect() { |
|
207
|
|
|
if ($this->connected()) { |
|
208
|
|
|
return true; |
|
209
|
|
|
} |
|
210
|
|
|
|
|
211
|
|
|
$this->connection = new php_mysqli( |
|
212
|
|
|
$this->details['host'], |
|
213
|
|
|
$this->details['user'], |
|
214
|
|
|
$this->details['pass'], |
|
215
|
|
|
$this->details['name'], |
|
216
|
|
|
$this->details['port'] |
|
217
|
|
|
); |
|
218
|
|
|
|
|
219
|
|
|
if ($this->connection->connect_errno) { |
|
220
|
|
|
return false; |
|
221
|
|
|
} |
|
222
|
|
|
|
|
223
|
|
|
return $this->connected = true; |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
/** |
|
227
|
|
|
* Determine whether the connection is currently active. |
|
228
|
|
|
* |
|
229
|
|
|
* @return bool |
|
230
|
|
|
*/ |
|
231
|
|
|
public function connected() { |
|
232
|
|
|
return $this->connected && !$this->connection->connect_errno; |
|
233
|
|
|
} |
|
234
|
|
|
|
|
235
|
|
|
/** |
|
236
|
|
|
* Close the connection. |
|
237
|
|
|
*/ |
|
238
|
|
|
public function disconnect() { |
|
239
|
|
|
$this->connection->close(); |
|
240
|
|
|
$this->connected = false; |
|
241
|
|
|
} |
|
242
|
|
|
|
|
243
|
|
|
/** |
|
244
|
|
|
* Retrieve the query translator. |
|
245
|
|
|
* |
|
246
|
|
|
* @return Translator |
|
247
|
|
|
*/ |
|
248
|
|
|
public function translator() { |
|
249
|
|
|
if (!$this->translator) { |
|
250
|
|
|
$this->translator = new Translator\MySql; |
|
251
|
|
|
} |
|
252
|
|
|
|
|
253
|
|
|
return $this->translator; |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
/** |
|
257
|
|
|
* Query the database with the given query and optional parameters. |
|
258
|
|
|
* |
|
259
|
|
|
* TODO: Simplify this. |
|
260
|
|
|
* |
|
261
|
|
|
* @param Query|string $query |
|
262
|
|
|
* @param array $parameters [optional] |
|
263
|
|
|
* @return Result |
|
264
|
|
|
*/ |
|
265
|
|
|
public function query($query, array $parameters = array()) { |
|
266
|
|
|
if (!$query instanceof Query) { |
|
267
|
|
|
$query = new Query((string) $query, $parameters); |
|
268
|
|
|
} |
|
269
|
|
|
|
|
270
|
|
|
$this->connect(); |
|
271
|
|
|
|
|
272
|
|
View Code Duplication |
if (!$this->connected()) { |
|
|
|
|
|
|
273
|
|
|
$this->lastResult = new Result($query, array(), array(), $this->error()); |
|
274
|
|
|
|
|
275
|
|
|
$this->event('mysql.query', array($this->lastResult)); |
|
276
|
|
|
|
|
277
|
|
|
return $this->lastResult; |
|
278
|
|
|
} |
|
279
|
|
|
|
|
280
|
|
|
$this->lastResult = null; |
|
281
|
|
|
|
|
282
|
|
|
$this->event('mysql.prequery', array($query)); |
|
283
|
|
|
|
|
284
|
|
|
$statement = $this->prepareStatement($query->string, $query->parameters); |
|
285
|
|
|
|
|
286
|
|
|
if ($statement->errno) { |
|
287
|
|
|
$error = new Error($statement->errno, $statement->error); |
|
288
|
|
|
$this->lastResult = new Result($query, array(), array(), $error); |
|
289
|
|
|
|
|
290
|
|
|
$this->event('mysql.query', array($this->lastResult)); |
|
291
|
|
|
|
|
292
|
|
|
return $this->lastResult; |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
$statement->execute(); |
|
296
|
|
|
|
|
297
|
|
|
$error = $this->error(); |
|
298
|
|
|
|
|
299
|
|
|
if ($error) { |
|
|
|
|
|
|
300
|
|
|
$this->lastResult = new Result($query, array(), array(), $error); |
|
301
|
|
|
|
|
302
|
|
|
$this->event('mysql.query', array($this->lastResult)); |
|
303
|
|
|
|
|
304
|
|
|
return $this->lastResult; |
|
305
|
|
|
} |
|
306
|
|
|
|
|
307
|
|
|
$result = $this->prepareStatementResult($statement); |
|
308
|
|
|
|
|
309
|
|
|
$statement->close(); |
|
310
|
|
|
|
|
311
|
|
|
$info = array( |
|
312
|
|
|
'count' => $result['num_rows'], |
|
313
|
|
|
'fields' => $result['fields'], |
|
314
|
|
|
'affected' => $result['affected'], |
|
315
|
|
|
'insert_id' => $result['insert_id'] |
|
316
|
|
|
); |
|
317
|
|
|
|
|
318
|
|
|
$this->lastResult = new Result($query, $result['data'], $info, $error); |
|
319
|
|
|
|
|
320
|
|
|
$this->event('mysql.query', array($this->lastResult)); |
|
321
|
|
|
|
|
322
|
|
|
return $this->lastResult; |
|
323
|
|
|
} |
|
324
|
|
|
|
|
325
|
|
|
/** |
|
326
|
|
|
* Escape the given string for a MySQL query. |
|
327
|
|
|
* |
|
328
|
|
|
* @param string $string |
|
329
|
|
|
* @return string |
|
330
|
|
|
*/ |
|
331
|
|
|
public function escape($string) { |
|
332
|
|
|
$this->connect(); |
|
333
|
|
|
|
|
334
|
|
|
return $this->connection->real_escape_string($string); |
|
335
|
|
|
} |
|
336
|
|
|
|
|
337
|
|
|
/** |
|
338
|
|
|
* Retrieve error information regarding the last query or connection |
|
339
|
|
|
* attempt. |
|
340
|
|
|
* |
|
341
|
|
|
* Returns null if there is no error. |
|
342
|
|
|
* |
|
343
|
|
|
* @return Error |
|
344
|
|
|
*/ |
|
345
|
|
|
public function error() { |
|
346
|
|
|
$connectionError = $this->connectionError(); |
|
347
|
|
|
|
|
348
|
|
|
if ($connectionError) { |
|
349
|
|
|
return $connectionError; |
|
350
|
|
|
} |
|
351
|
|
|
|
|
352
|
|
|
if ($this->lastResult && $this->lastResult->error) { |
|
353
|
|
|
return $this->lastResult->error; |
|
354
|
|
|
} |
|
355
|
|
|
|
|
356
|
|
|
return null; |
|
357
|
|
|
} |
|
358
|
|
|
|
|
359
|
|
|
/** |
|
360
|
|
|
* Retrieve error information from the mysqli connection object. |
|
361
|
|
|
* |
|
362
|
|
|
* Checks for general errors first, then connection errors. |
|
363
|
|
|
* |
|
364
|
|
|
* Returns null if there is no error. |
|
365
|
|
|
* |
|
366
|
|
|
* @return Error |
|
367
|
|
|
*/ |
|
368
|
|
|
protected function connectionError() { |
|
369
|
|
|
if (!$this->connection) { |
|
370
|
|
|
return null; |
|
371
|
|
|
} |
|
372
|
|
|
|
|
373
|
|
|
if ($this->connection->errno) { |
|
374
|
|
|
return new Error($this->connection->errno, $this->connection->error); |
|
375
|
|
|
} |
|
376
|
|
|
|
|
377
|
|
|
if ($this->connection->connect_errno) { |
|
378
|
|
|
return new Error($this->connection->connect_errno, $this->connection->connect_error); |
|
379
|
|
|
} |
|
380
|
|
|
|
|
381
|
|
|
return null; |
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
} |
|
385
|
|
|
|
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.