1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file contains functionality related to interacting with the database this CMS uses |
4
|
|
|
* |
5
|
|
|
* @package BZiON |
6
|
|
|
* @license https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3 |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
use BZIon\Debug\DatabaseQuery; |
10
|
|
|
use Monolog\Logger; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Database interface class |
14
|
|
|
*/ |
15
|
|
|
class Database |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* The global database connection object |
19
|
|
|
* |
20
|
|
|
* @todo Move this to the Service class |
21
|
|
|
* @var Database |
22
|
|
|
*/ |
23
|
|
|
private static $Database; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* The database object used inside this class |
27
|
|
|
* @var PDO |
28
|
|
|
*/ |
29
|
|
|
private $dbc; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* An instance of the logger |
33
|
|
|
* @var Logger |
34
|
|
|
*/ |
35
|
|
|
private $logger; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* The id of the last row entered |
39
|
|
|
* @var int |
40
|
|
|
*/ |
41
|
|
|
private $last_id; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Create a new connection to the database |
45
|
|
|
* |
46
|
|
|
* @param string $host The MySQL host |
47
|
|
|
* @param string $user The MySQL user |
48
|
|
|
* @param string $password The MySQL password for the user |
49
|
|
|
* @param string $dbName The MySQL database name |
50
|
|
|
*/ |
51
|
1 |
|
public function __construct($host, $user, $password, $dbName) |
52
|
|
|
{ |
53
|
1 |
|
if (Service::getContainer()) { |
54
|
1 |
|
if ($logger = Service::getContainer()->get('monolog.logger.mysql')) { |
55
|
1 |
|
$this->logger = $logger; |
56
|
|
|
} |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
try { |
60
|
|
|
// TODO: Persist |
61
|
1 |
|
$this->dbc = new PDO( |
62
|
1 |
|
'mysql:host=' . $host . ';dbname=' . $dbName . ';charset=utf8', |
63
|
1 |
|
$user, |
64
|
1 |
|
$password, |
65
|
|
|
array( |
66
|
1 |
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, |
67
|
|
|
|
68
|
|
|
// We are using MySQL, so there is no need to emulate |
69
|
|
|
// prepared statements for databases that don't support |
70
|
|
|
// them. This line makes sure all values are returned to PHP |
71
|
|
|
// from MySQL in the correct type, and they are not all |
72
|
|
|
// strings. |
73
|
|
|
PDO::ATTR_EMULATE_PREPARES => false |
74
|
|
|
) |
75
|
|
|
); |
76
|
|
|
} catch (PDOException $e) { |
77
|
|
|
$this->logger->addAlert($e->getMessage()); |
78
|
|
|
throw new Exception($e->getMessage(), $e->getCode()); |
79
|
|
|
} |
80
|
1 |
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Destroy this connection to the database |
84
|
|
|
*/ |
85
|
|
|
public function __destruct() |
86
|
|
|
{ |
87
|
|
|
$this->closeConnection(); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Get an instance of the Database object |
92
|
|
|
* |
93
|
|
|
* This should be the main way to acquire access to the database |
94
|
|
|
* |
95
|
|
|
* @todo Move this to the Service class |
96
|
|
|
* |
97
|
|
|
* @throws \Exception When no testing environment has been specified in the configuation file. |
98
|
|
|
* |
99
|
76 |
|
* @return Database The Database object |
100
|
|
|
*/ |
101
|
76 |
|
public static function getInstance() |
102
|
1 |
|
{ |
103
|
1 |
|
if (!self::$Database) { |
104
|
|
|
if (Service::getEnvironment() == 'test') { |
105
|
|
|
if (!Service::getParameter('bzion.testing.enabled')) { |
106
|
|
|
throw new Exception('You have to specify a MySQL database for testing in the bzion.testing section of your configuration file.'); |
107
|
1 |
|
} |
108
|
1 |
|
|
109
|
1 |
|
self::$Database = new self( |
110
|
1 |
|
Service::getParameter('bzion.testing.host'), |
111
|
1 |
|
Service::getParameter('bzion.testing.username'), |
112
|
|
|
Service::getParameter('bzion.testing.password'), |
113
|
|
|
Service::getParameter('bzion.testing.database') |
114
|
|
|
); |
115
|
|
|
} else { |
116
|
|
|
self::$Database = new self( |
117
|
|
|
Service::getParameter('bzion.mysql.host'), |
118
|
|
|
Service::getParameter('bzion.mysql.username'), |
119
|
|
|
Service::getParameter('bzion.mysql.password'), |
120
|
|
|
Service::getParameter('bzion.mysql.database') |
121
|
|
|
); |
122
|
|
|
} |
123
|
76 |
|
} |
124
|
|
|
|
125
|
|
|
return self::$Database; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Close the current connection to the MySQL database |
130
|
|
|
*/ |
131
|
|
|
public function closeConnection() |
132
|
|
|
{ |
133
|
|
|
$this->dbc = null; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Tests whether or not the connection to the database is still active |
138
|
|
|
* @todo Make this work for PDO, or deprecate it if not needed |
139
|
|
|
* @return bool True if the connection is active |
140
|
|
|
*/ |
141
|
|
|
public function isConnected() |
142
|
|
|
{ |
143
|
|
|
return true; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Get the unique row ID of the last row that was inserted |
148
|
76 |
|
* @return int The ID of the row |
149
|
|
|
*/ |
150
|
76 |
|
public function getInsertId() |
151
|
|
|
{ |
152
|
|
|
return $this->last_id; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Insert an associative array into the database. |
157
|
|
|
* |
158
|
|
|
* @param string $table The table to perform the query on |
159
|
|
|
* @param array $params An associative array, with the keys (columns) pointing to the values you want to put on each |
160
|
76 |
|
* @param array|string|null $now Column(s) to update with the current timestamp |
161
|
|
|
* |
162
|
76 |
|
* @return int |
163
|
76 |
|
*/ |
164
|
|
|
public function insert($table, array $params, $now = null) |
165
|
|
|
{ |
166
|
76 |
|
$columns = implode('`,`', array_keys($params)); |
167
|
|
|
$columns = "`$columns`"; |
168
|
76 |
|
|
169
|
76 |
|
$question_marks = str_repeat('?,', count($params)); |
170
|
|
|
$question_marks = rtrim($question_marks, ','); // Remove last comma |
171
|
76 |
|
|
172
|
|
|
if ($now) { |
173
|
76 |
|
if (!is_array($now)) { |
174
|
|
|
// Convert $now to an array if it's a string |
175
|
|
|
$now = array($now); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
foreach ($now as $column) { |
179
|
|
|
$columns .= ",$column"; |
180
|
|
|
$question_marks .= ",UTC_TIMESTAMP()"; |
181
|
|
|
} |
182
|
|
|
} |
183
|
76 |
|
|
184
|
|
|
$query = "INSERT INTO $table ($columns) VALUES ($question_marks)"; |
185
|
76 |
|
$this->execute($query, array_values($params)); |
186
|
1 |
|
|
187
|
|
|
return $this->getInsertId(); |
188
|
|
|
} |
189
|
76 |
|
|
190
|
|
|
/** |
191
|
76 |
|
* Insert an array of associative arrays into the database. |
192
|
|
|
* |
193
|
76 |
|
* @param string $table The table to perform the query on |
194
|
|
|
* @param array $batch An array of associative arrays that'll be passed on to Database::insert() |
195
|
76 |
|
* @param array|string|null $now Column(s) to update with the current timestamp |
196
|
|
|
*/ |
197
|
|
|
public function insertBatch($table, array $batch, $now = null) |
198
|
|
|
{ |
199
|
|
|
$this->startTransaction(); |
200
|
|
|
|
201
|
|
|
foreach ($batch as $entry) { |
202
|
|
|
$this->insert($table, $entry, $now); |
203
|
|
|
} |
204
|
|
|
|
205
|
76 |
|
$this->commit(); |
206
|
|
|
} |
207
|
|
|
|
208
|
76 |
|
/** |
209
|
|
|
* Prepares and executes a MySQL prepared INSERT/DELETE/UPDATE statement. <em>The second parameter is optional when using this function to execute a query with no placeholders.</em> |
210
|
76 |
|
* |
211
|
76 |
|
* @param string $queryText The prepared SQL statement that will be executed |
212
|
76 |
|
* @param mixed|array $params (Optional) The array of values that will be binded to the prepared statement |
213
|
|
|
* @return int Returns the number of rows affected |
214
|
76 |
|
*/ |
215
|
76 |
View Code Duplication |
public function execute($queryText, $params = false) |
|
|
|
|
216
|
76 |
|
{ |
217
|
76 |
|
if (!is_array($params)) { |
218
|
76 |
|
$params = array($params); |
219
|
76 |
|
} |
220
|
76 |
|
|
221
|
76 |
|
$debug = new DatabaseQuery($queryText, $params); |
222
|
|
|
|
223
|
|
|
$query = $this->doQuery($queryText, $params); |
224
|
|
|
$return = $query->rowCount(); |
225
|
76 |
|
|
226
|
|
|
$debug->finish($return); |
227
|
|
|
|
228
|
76 |
|
return $return; |
229
|
|
|
} |
230
|
|
|
|
231
|
76 |
|
/** |
232
|
|
|
* Prepares and executes a MySQL prepared SELECT statement. <em>The second parameter is optional when using this function to execute a query with no placeholders.</em> |
233
|
|
|
* |
234
|
|
|
* @param string $queryText The prepared SQL statement that will be executed |
235
|
|
|
* @param mixed|array $params (Optional) The array of values that will be binded to the prepared statement |
236
|
76 |
|
* @return array Returns an array of the values received from the query |
237
|
76 |
|
*/ |
238
|
|
View Code Duplication |
public function query($queryText, $params = false) |
|
|
|
|
239
|
|
|
{ |
240
|
|
|
if (!is_array($params)) { |
241
|
76 |
|
$params = array($params); |
242
|
|
|
} |
243
|
76 |
|
|
244
|
1 |
|
$debug = new DatabaseQuery($queryText, $params); |
245
|
1 |
|
|
246
|
|
|
$return = $this->doQuery($queryText, $params)->fetchAll(); |
247
|
|
|
|
248
|
|
|
$debug->finish($return); |
249
|
|
|
|
250
|
|
|
return $return; |
251
|
|
|
} |
252
|
2 |
|
|
253
|
|
|
/** |
254
|
2 |
|
* Perform a query |
255
|
2 |
|
* @param string $queryText The prepared SQL statement that will be executed |
256
|
|
|
* @param null|array $params (Optional) The array of values that will be binded to the prepared statement |
257
|
|
|
* |
258
|
|
|
* @return PDOStatement The PDO statement |
259
|
|
|
*/ |
260
|
|
|
private function doQuery($queryText, $params = null) |
261
|
|
|
{ |
262
|
|
|
try { |
263
|
|
|
$query = $this->dbc->prepare($queryText); |
264
|
|
|
|
265
|
|
|
if ($params !== null) { |
266
|
|
|
$i = 1; |
267
|
|
|
foreach ($params as $name => $param) { |
268
|
|
|
// Guess parameter type |
269
|
|
|
if (is_bool($param)) { |
270
|
|
|
$param = (int) $param; |
271
|
|
|
$type = PDO::PARAM_INT; |
272
|
|
|
} elseif (is_int($param)) { |
273
|
|
|
$type = PDO::PARAM_INT; |
274
|
|
|
} elseif (is_null($param)) { |
275
|
|
|
$type = PDO::PARAM_NULL; |
276
|
|
|
} elseif ($param instanceof ModelInterface) { |
277
|
|
|
$param = (int) $param->getId(); |
278
|
2 |
|
$type = PDO::PARAM_INT; |
279
|
|
|
} else { |
280
|
2 |
|
$type = PDO::PARAM_STR; |
281
|
2 |
|
} |
282
|
|
|
|
283
|
|
|
if (is_string($name)) { |
284
|
|
|
$query->bindValue($name, $param, $type); |
285
|
|
|
} else { |
286
|
|
|
$query->bindValue($i++, $param, $type); |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
$result = $query->execute(); |
292
|
1 |
|
if ($result === false) { |
293
|
|
|
$this->error("Unknown error"); |
294
|
1 |
|
} |
295
|
|
|
|
296
|
|
|
$this->last_id = $this->dbc->lastInsertId(); |
|
|
|
|
297
|
|
|
|
298
|
|
|
return $query; |
299
|
1 |
|
} catch (PDOException $e) { |
300
|
1 |
|
$this->error($e->getMessage(), $e->getCode(), $e); |
301
|
1 |
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
1 |
|
/** |
305
|
1 |
|
* Start a MySQL transaction |
306
|
|
|
*/ |
307
|
|
|
public function startTransaction() |
308
|
|
|
{ |
309
|
|
|
$this->dbc->beginTransaction(); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Commit the stored queries (usable only if a transaction has been started) |
314
|
|
|
* |
315
|
|
|
* This does not show an error if there are no queries to commit |
316
|
|
|
*/ |
317
|
|
|
public function commit() |
318
|
|
|
{ |
319
|
|
|
$this->dbc->commit(); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Cancel all pending queries (does not finish the transaction |
324
|
|
|
*/ |
325
|
|
|
public function rollback() |
326
|
|
|
{ |
327
|
|
|
$this->dbc->rollBack(); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Commit all pending queries and finalise the transaction |
332
|
|
|
*/ |
333
|
|
|
public function finishTransaction() |
334
|
|
|
{ |
335
|
|
|
$this->dbc->commit(); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Uses monolog to log an error message |
340
|
|
|
* |
341
|
|
|
* @param string $error The error string |
342
|
|
|
* @param int $id The error ID |
343
|
|
|
* @param Exception|null $previous The exception that caused the error (if any) |
344
|
|
|
* |
345
|
|
|
* @todo When PHP 5.x support is dropped, replace Exception with Throwable |
346
|
|
|
* |
347
|
|
|
* @throws Exception |
348
|
|
|
*/ |
349
|
|
|
public function error($error, $id = null, Exception $previous = null) |
350
|
|
|
{ |
351
|
|
|
if (empty($error)) { |
352
|
|
|
$error = "Unknown MySQL error - check for warnings generated by PHP"; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
// Create a context array so that we can log the ID, if provided |
356
|
|
|
$context = array(); |
357
|
|
|
if ($id !== null) { |
358
|
|
|
$context['id'] = $id; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
$this->logger->addError($error, $context); |
362
|
|
|
throw new Exception($error, (int) $id, $previous); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* Serialize the object |
367
|
|
|
* |
368
|
|
|
* Prevents PDO from being erroneously serialized |
369
|
|
|
* |
370
|
|
|
* @return array The list of properties that should be serialized |
371
|
|
|
*/ |
372
|
|
|
public function __sleep() |
373
|
|
|
{ |
374
|
|
|
return array('last_id'); |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
|
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.