1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* |
4
|
|
|
* @filesource PDODriver.php |
5
|
|
|
* @created 04.11.2015 |
6
|
|
|
* @package chillerlan\Database\Drivers\PDO |
7
|
|
|
* @author Smiley <[email protected]> |
8
|
|
|
* @copyright 2015 Smiley |
9
|
|
|
* @license MIT |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace chillerlan\Database\Drivers\PDO; |
13
|
|
|
|
14
|
|
|
use chillerlan\Database\DBException; |
15
|
|
|
use chillerlan\Database\Drivers\DBDriverAbstract; |
16
|
|
|
use PDO; |
17
|
|
|
use PDOException; |
18
|
|
|
use PDOStatement; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Class PDODriver |
22
|
|
|
* |
23
|
|
|
* @see http://php.net/manual/pdo.constants.php |
24
|
|
|
*/ |
25
|
|
|
class PDODriver extends DBDriverAbstract{ |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Holds the database resource object |
29
|
|
|
* |
30
|
|
|
* @var PDO |
31
|
|
|
*/ |
32
|
|
|
protected $db; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Some basic PDO options |
36
|
|
|
* |
37
|
|
|
* @see http://php.net/manual/pdo.getattribute.php PDO::getAttribute |
38
|
|
|
* |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $pdo_options = [ |
42
|
|
|
PDO::ATTR_CASE => PDO::CASE_NATURAL, |
43
|
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, |
44
|
|
|
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, |
45
|
|
|
PDO::ATTR_STRINGIFY_FETCHES => false, |
46
|
|
|
PDO::ATTR_EMULATE_PREPARES => false, |
47
|
|
|
]; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The PDO drivername which is being used in the DSN |
51
|
|
|
* |
52
|
|
|
* @var string |
53
|
|
|
*/ |
54
|
|
|
protected $drivername; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* This array holds one or more key=>value pairs to set attribute values for the PDOStatement object that this method returns. |
58
|
|
|
* You would most commonly use this to set the PDO::ATTR_CURSOR value to PDO::CURSOR_SCROLL to request a scrollable cursor. |
59
|
|
|
* |
60
|
|
|
* @var array |
61
|
|
|
*/ |
62
|
|
|
protected $pdo_stmt_options = []; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Returns a DSN string using the given options |
66
|
|
|
* @link http://www.phpro.org/tutorials/Introduction-to-PHP-PDO.html#4.4 |
67
|
|
|
* |
68
|
|
|
* @return string DSN |
69
|
|
|
*/ |
70
|
|
|
protected function getDSN():string { |
71
|
|
|
$dsn = $this->drivername; |
72
|
|
|
|
73
|
|
|
if($this->options->socket){ |
74
|
|
|
$dsn .= ':unix_socket='.$this->options->socket; |
75
|
|
|
} |
76
|
|
|
else{ |
77
|
|
|
$dsn .= ':host='.$this->options->host; |
78
|
|
|
$dsn .= (bool)$this->options->port ? ';port='.$this->options->port : ''; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
$dsn .= ';dbname='.$this->options->database; |
82
|
|
|
|
83
|
|
|
return $dsn; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Establishes a database connection and returns the connection object |
88
|
|
|
* |
89
|
|
|
* @return \PDO the database resource object |
90
|
|
|
* @throws \chillerlan\Database\DBException |
91
|
|
|
*/ |
92
|
|
|
public function connect():PDO { |
93
|
|
|
|
94
|
|
|
if($this->db instanceof PDO){ |
95
|
|
|
return $this->db; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
if($this->options->use_ssl){ |
99
|
|
|
$this->pdo_options += [ |
100
|
|
|
PDO::MYSQL_ATTR_SSL_KEY => $this->options->ssl_key, |
101
|
|
|
PDO::MYSQL_ATTR_SSL_CERT => $this->options->ssl_cert, |
102
|
|
|
PDO::MYSQL_ATTR_SSL_CA => $this->options->ssl_ca, |
103
|
|
|
PDO::MYSQL_ATTR_SSL_CAPATH => $this->options->ssl_capath, |
104
|
|
|
PDO::MYSQL_ATTR_SSL_CIPHER => $this->options->ssl_cipher, |
105
|
|
|
]; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
try{ |
109
|
|
|
$this->db = new PDO($this->getDSN(), $this->options->username, $this->options->password, $this->pdo_options); |
110
|
|
|
} |
111
|
|
|
catch(PDOException $PDOException){ |
112
|
|
|
throw new DBException($PDOException->getMessage()); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
return $this->db; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Closes a database connection |
120
|
|
|
* |
121
|
|
|
* @return bool |
122
|
|
|
*/ |
123
|
|
|
public function disconnect():bool { |
124
|
|
|
$this->db = null; |
125
|
|
|
|
126
|
|
|
return true; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Returns info about the used php client |
131
|
|
|
* |
132
|
|
|
* @return string php's database client string |
133
|
|
|
*/ |
134
|
|
|
public function getClientInfo():string { |
135
|
|
|
return $this->db->getAttribute(PDO::ATTR_CLIENT_VERSION); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Returns info about the database server |
140
|
|
|
* |
141
|
|
|
* @return string database's serverinfo string |
142
|
|
|
*/ |
143
|
|
|
public function getServerInfo():string { |
144
|
|
|
return $this->db->getAttribute(PDO::ATTR_SERVER_INFO); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Sanitizer. |
149
|
|
|
* |
150
|
|
|
* Recursively escapes string values, optional htmlspecialchars() |
151
|
|
|
* |
152
|
|
|
* @param array|string $data array or string to escape |
153
|
|
|
* @param bool $specialchars [optional] if true, it performs a htmlspecialchars() on each value given |
154
|
|
|
* |
155
|
|
|
* @return array|string array or string. escaped. obviously. |
156
|
|
|
*/ |
157
|
|
View Code Duplication |
public function escape($data, bool $specialchars = false){ |
|
|
|
|
158
|
|
|
|
159
|
|
|
if(is_array($data)){ |
160
|
|
|
|
161
|
|
|
foreach($data as $key => $value){ |
162
|
|
|
$data[$key] = $this->escape($value, $specialchars); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
} |
166
|
|
|
else if(is_object($data)){ |
167
|
|
|
|
168
|
|
|
foreach($data as $key => $value){ |
169
|
|
|
$data->{$key} = $this->escape($value, $specialchars); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
} |
173
|
|
|
else{ |
174
|
|
|
|
175
|
|
|
if($specialchars){ |
176
|
|
|
$data = htmlspecialchars($data, ENT_HTML5, 'UTF-8', false); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
$data = $this->db->quote($data); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
return $data; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @param \PDOStatement $stmt |
187
|
|
|
* @param string $index |
188
|
|
|
* @param bool $assoc |
189
|
|
|
* @param bool $fetch_array |
190
|
|
|
* |
191
|
|
|
* @return array |
192
|
|
|
* @internal |
193
|
|
|
*/ |
194
|
|
|
protected function getResult(PDOStatement &$stmt, string $index, bool $assoc, bool $fetch_array):array { |
195
|
|
|
$out = []; |
196
|
|
|
$method = $assoc ? ($fetch_array ? PDO::FETCH_ASSOC : PDO::FETCH_OBJ) : PDO::FETCH_NUM; |
197
|
|
|
$i = 0 ; |
198
|
|
|
|
199
|
|
|
// ok, we have a result with one or more rows, loop out the rows and output as array |
200
|
|
View Code Duplication |
while($row = $stmt->fetch($method)){ |
|
|
|
|
201
|
|
|
$key = $i; |
202
|
|
|
|
203
|
|
|
if($assoc && !empty($index)){ |
204
|
|
|
|
205
|
|
|
if($fetch_array && isset($row[$index])){ |
206
|
|
|
$key = $row[$index]; |
207
|
|
|
} |
208
|
|
|
else if(isset($row->{$index})){ |
209
|
|
|
$key = $row->{$index}; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
$out[$key] = $row; |
215
|
|
|
$i++; |
216
|
|
|
|
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $out; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @param \PDOStatement $stmt |
224
|
|
|
* @param array $values |
225
|
|
|
* @return void |
226
|
|
|
* @internal |
227
|
|
|
*/ |
228
|
|
|
protected function bindParams(PDOStatement &$stmt, array $values){ |
229
|
|
|
$param_no = 1; |
230
|
|
|
|
231
|
|
|
foreach($values as $v){ |
232
|
|
|
|
233
|
|
|
switch(gettype($v)){ |
234
|
|
|
case 'boolean': $type = PDO::PARAM_BOOL; break; |
|
|
|
|
235
|
|
|
case 'integer': $type = PDO::PARAM_INT; break; |
|
|
|
|
236
|
|
|
case 'NULL': $type = PDO::PARAM_NULL; break; |
|
|
|
|
237
|
|
|
default: $type = PDO::PARAM_STR; break; |
|
|
|
|
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
$stmt->bindValue($param_no, $v, $type); |
241
|
|
|
$param_no++; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Returns the last insert id (if present) |
247
|
|
|
* |
248
|
|
|
* @link http://php.net/manual/pdo.lastinsertid.php |
249
|
|
|
* @return string |
250
|
|
|
*/ |
251
|
|
|
protected function insertID():string { |
252
|
|
|
return $this->db->lastInsertId(); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Basic SQL query for non prepared statements |
257
|
|
|
* |
258
|
|
|
* There is no escaping in here, so make sure, your SQL is clean/escaped. |
259
|
|
|
* Also, your SQL should NEVER contain user input, use prepared statements in this case. |
260
|
|
|
* |
261
|
|
|
* If the query was successful it returns either an array of results or true |
262
|
|
|
* if it was a void query. On errors, a false will be returned, obviously. |
263
|
|
|
* |
264
|
|
|
* @param string $sql The SQL statement |
265
|
|
|
* @param string $index [optional] an index column to assingn as the result's keys |
|
|
|
|
266
|
|
|
* @param bool $assoc [optional] If true, the fields are named with the respective column names, otherwise numbered |
267
|
|
|
* @param bool $fetch_array [optional] fetch the vaues as array instead of object |
268
|
|
|
* |
269
|
|
|
* @return array|bool array with results, true on void query success, otherwise false. |
270
|
|
|
* @throws \chillerlan\Database\DBException |
271
|
|
|
*/ |
272
|
|
|
public function raw(string $sql, string $index = null, bool $assoc = true, bool $fetch_array = false){ |
273
|
|
|
|
274
|
|
|
try{ |
275
|
|
|
$stmt = $this->db->query($sql); |
276
|
|
|
|
277
|
|
|
$this->addStats([ |
278
|
|
|
'affected_rows' => $stmt->rowCount(), |
279
|
|
|
'error' => $stmt->errorInfo(), |
280
|
|
|
'insert_id' => $this->insertID(), |
281
|
|
|
'sql' => $sql, |
282
|
|
|
'index' => $index, |
283
|
|
|
'assoc' => $assoc, |
284
|
|
|
'fetch_array' => $fetch_array, |
285
|
|
|
]); |
286
|
|
|
|
287
|
|
|
if(!is_bool($stmt)){ |
288
|
|
|
return $this->getResult($stmt, $index, $assoc, $fetch_array); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
// void result |
292
|
|
|
return $stmt; |
293
|
|
|
} |
294
|
|
|
catch(PDOException $PDOException){ |
295
|
|
|
throw new DBException($PDOException->getMessage()); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Prepared statements wrapper |
302
|
|
|
* |
303
|
|
|
* Does everything for you: prepares the statement and fetches the results as an object or array |
304
|
|
|
* just pass a query along with values and you're done. Not meant for multi-inserts. |
305
|
|
|
* |
306
|
|
|
* @param string $sql The SQL statement to prepare |
307
|
|
|
* @param array $values [optional] the value for each "?" in the statement - in the respective order, of course |
308
|
|
|
* @param string $index [optional] an index column to assingn as the result's keys |
|
|
|
|
309
|
|
|
* @param bool $assoc [optional] If true, the fields are named with the respective column names, otherwise numbered |
310
|
|
|
* @param bool $fetch_array [optional] fetch the vaues as array instead of object |
311
|
|
|
* |
312
|
|
|
* @return array|bool Array with results, true on void query success, otherwise false |
313
|
|
|
* @throws \chillerlan\Database\DBException |
314
|
|
|
*/ |
315
|
|
|
public function prepared(string $sql, array $values = [], string $index = null, bool $assoc = true, bool $fetch_array = false){ |
316
|
|
|
|
317
|
|
|
try{ |
318
|
|
|
$stmt = $this->db->prepare($sql, $this->pdo_stmt_options); |
319
|
|
|
|
320
|
|
|
if(!empty($values)){ |
321
|
|
|
$this->bindParams($stmt, $values); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
$stmt->execute(); |
325
|
|
|
|
326
|
|
|
$this->addStats([ |
327
|
|
|
'affected_rows' => $stmt->rowCount(), |
328
|
|
|
'error' => $stmt->errorInfo(), |
329
|
|
|
'insert_id' => $this->insertID(), |
330
|
|
|
'sql' => $sql, |
331
|
|
|
'values' => $values, |
332
|
|
|
'index' => $index, |
333
|
|
|
'assoc' => $assoc, |
334
|
|
|
'fetch_array' => $fetch_array, |
335
|
|
|
]); |
336
|
|
|
|
337
|
|
|
if(!is_bool($stmt)){ |
338
|
|
|
return $this->getResult($stmt, $index, $assoc, $fetch_array); |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
return $stmt; |
342
|
|
|
} |
343
|
|
|
catch(PDOException $PDOException){ |
344
|
|
|
throw new DBException($PDOException->getMessage()); |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Prepared multi line insert |
350
|
|
|
* |
351
|
|
|
* Prepared statement multi insert/update |
352
|
|
|
* |
353
|
|
|
* @param string $sql The SQL statement to prepare |
354
|
|
|
* @param array $values a multidimensional array with the values, each row represents one line to insert. |
355
|
|
|
* |
356
|
|
|
* @return bool true query success, otherwise false |
357
|
|
|
*/ |
358
|
|
|
public function multi(string $sql, array $values){ |
359
|
|
|
|
360
|
|
|
if(is_array($values) && count($values) > 0 && is_array($values[0]) && count($values[0]) > 0){ |
361
|
|
|
$stmt = $this->db->prepare($sql, $this->pdo_stmt_options); |
362
|
|
|
|
363
|
|
|
foreach($values as $row){ |
364
|
|
|
$this->bindParams($stmt, $row); |
365
|
|
|
$stmt->execute(); |
366
|
|
|
|
367
|
|
|
$this->addStats([ |
368
|
|
|
'affected_rows' => $stmt->rowCount(), |
369
|
|
|
'error' => $stmt->errorInfo(), |
370
|
|
|
'insert_id' => $this->insertID(), |
371
|
|
|
'sql' => $sql, |
372
|
|
|
'values' => $values, |
373
|
|
|
]); |
374
|
|
|
|
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
$stmt = null; |
|
|
|
|
378
|
|
|
|
379
|
|
|
return true; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
return false; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* Prepared multi line insert/update with callback |
387
|
|
|
* |
388
|
|
|
* @todo: multi treading |
389
|
|
|
* @see https://gist.github.com/krakjoe/6437782 |
390
|
|
|
* @see https://gist.github.com/krakjoe/9384409 |
391
|
|
|
* |
392
|
|
|
* @param string $sql The SQL statement to prepare |
393
|
|
|
* @param array $data an array with the (raw) data to insert, each row represents one line to insert. |
394
|
|
|
* @param callable|array $callback a callback that processes the values for each row. |
395
|
|
|
* |
396
|
|
|
* @return bool true query success, otherwise false |
397
|
|
|
* @throws \chillerlan\Database\DBException |
398
|
|
|
*/ |
399
|
|
|
public function multi_callback(string $sql, array $data, $callback){ |
400
|
|
|
// TODO: Implement multi_callback() method. |
401
|
|
|
return false; |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
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.