1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @package toolkit |
5
|
|
|
*/ |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* The Database class acts as a wrapper for connecting to the Database |
9
|
|
|
* in Symphony. |
10
|
|
|
* |
11
|
|
|
* It provides many methods that maps directly to their PDO equivalent. |
12
|
|
|
* It also provides many factory methods to help developers creates instances |
13
|
|
|
* of `DatabaseStatement` and their specialized child classes. |
14
|
|
|
* |
15
|
|
|
* Symphony uses a prefix to namespace it's tables in a |
16
|
|
|
* database, allowing it play nice with other applications installed on the |
17
|
|
|
* database. |
18
|
|
|
* |
19
|
|
|
* An error that occur during a query throw a `DatabaseException`. |
20
|
|
|
* By default, Symphony logs all queries to be used for Profiling and Debug |
21
|
|
|
* devkit extensions when a Developer is logged in. When a developer is not |
22
|
|
|
* logged in, all queries and errors are made available with delegates. |
23
|
|
|
*/ |
24
|
|
|
class Database |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* Constant to indicate whether the query is a write operation. |
28
|
|
|
* |
29
|
|
|
* @deprecated @since Symphony 3.0.0 |
30
|
|
|
* @var int |
31
|
|
|
*/ |
32
|
|
|
const __WRITE_OPERATION__ = 0; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Constant to indicate whether the query is a write operation |
36
|
|
|
* |
37
|
|
|
* @deprecated @since Symphony 3.0.0 |
38
|
|
|
* @var int |
39
|
|
|
*/ |
40
|
|
|
const __READ_OPERATION__ = 1; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* An instance of the current PDO object |
44
|
|
|
* |
45
|
|
|
* @var PDO |
46
|
|
|
*/ |
47
|
|
|
private $conn = null; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The array of log messages |
51
|
|
|
* |
52
|
|
|
* @var array |
53
|
|
|
*/ |
54
|
|
|
private $log = []; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* The number of queries this class has executed, defaults to 0. |
58
|
|
|
* |
59
|
|
|
* @var int |
60
|
|
|
*/ |
61
|
|
|
private $queryCount = 0; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* The default configuration values |
65
|
|
|
* |
66
|
|
|
* @var array |
67
|
|
|
*/ |
68
|
|
|
private $config = [ |
69
|
|
|
'host' => null, |
70
|
|
|
'port' => null, |
71
|
|
|
'user' => null, |
72
|
|
|
'password' => null, |
73
|
|
|
'db' => null, |
74
|
|
|
'driver' => null, |
75
|
|
|
'charset' => null, |
76
|
|
|
'collate' => null, |
77
|
|
|
'engine' => null, |
78
|
|
|
'tbl_prefix' => null, |
79
|
|
|
'query_caching' => null, |
80
|
|
|
'query_logging' => null, |
81
|
|
|
'options' => [], |
82
|
|
|
]; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* The last executed query |
86
|
|
|
* @var string; |
87
|
|
|
*/ |
88
|
|
|
private $lastQuery; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* The md5 hash of the last executed query |
92
|
|
|
* @var string; |
93
|
|
|
*/ |
94
|
|
|
private $lastQueryHash; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* The values used with the last executed query |
98
|
|
|
* @var array |
99
|
|
|
*/ |
100
|
|
|
private $lastQueryValues; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* The unsafe mode of the last executed query |
104
|
|
|
* @var bool |
105
|
|
|
*/ |
106
|
|
|
private $lastQuerySafe; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* The version of the SQL server |
110
|
|
|
* @var string |
111
|
|
|
*/ |
112
|
|
|
private $version; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Creates a new Database object given an associative array of configuration |
116
|
|
|
* parameters in `$config`, which should include |
117
|
|
|
* `driver`, `host`, `port`, `user`, `password`, `db` and an optional |
118
|
|
|
* array of PDO options in `options`. |
119
|
|
|
* |
120
|
|
|
* @param array $config |
121
|
|
|
*/ |
122
|
|
|
public function __construct(array $config = []) |
123
|
|
|
{ |
124
|
|
|
$this->config = array_merge($this->config, $config); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Magic function that will flush the logs and close the underlying database |
129
|
|
|
* connection when the Database class is destroyed. |
130
|
|
|
* |
131
|
|
|
* @link http://php.net/manual/en/language.oop5.decon.php |
132
|
|
|
*/ |
133
|
|
|
public function __destruct() |
134
|
|
|
{ |
135
|
|
|
unset($this->conn); |
136
|
|
|
$this->flush(); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Getter for all the log entries. |
141
|
|
|
* |
142
|
|
|
* @return array |
143
|
|
|
*/ |
144
|
|
|
public function getLogs() |
145
|
|
|
{ |
146
|
|
|
return $this->log; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Resets `$this->lastQuery`, `$this->lastQueryHash`, `$this->lastQueryValues` and |
151
|
|
|
* `$this->lastQuerySafe` to their empty values. |
152
|
|
|
* Called on each query and when the class is destroyed. |
153
|
|
|
* |
154
|
|
|
* @return Database |
155
|
|
|
* The current instance. |
156
|
|
|
*/ |
157
|
|
|
public function flush() |
158
|
|
|
{ |
159
|
|
|
$this->lastQuery = null; |
160
|
|
|
$this->lastQueryHash = null; |
161
|
|
|
$this->lastQueryValues = null; |
162
|
|
|
$this->lastQuerySafe = null; |
163
|
|
|
return $this; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Based on the configuration values set in the constructor, |
168
|
|
|
* this method will properly format the values to get a valid DSN |
169
|
|
|
* connection string. |
170
|
|
|
* |
171
|
|
|
* @return string |
172
|
|
|
* The generated DNS connection string |
173
|
|
|
*/ |
174
|
|
|
public function getDSN() |
175
|
|
|
{ |
176
|
|
|
$config = &$this->config; |
177
|
|
|
if ($config['host'] === 'unix_socket') { |
178
|
|
|
return sprintf( |
179
|
|
|
'%s:unix_socket=%s;dbname=%s;charset=%s', |
180
|
|
|
$config['driver'], |
181
|
|
|
General::intval($config['port']) === -1 ? $config['port'] : '', |
182
|
|
|
$config['db'], |
183
|
|
|
$config['charset'] |
184
|
|
|
); |
185
|
|
|
} |
186
|
|
|
return sprintf( |
187
|
|
|
'%s:dbname=%s;host=%s;port=%d;charset=%s', |
188
|
|
|
$config['driver'], |
189
|
|
|
$config['db'], |
190
|
|
|
$config['host'], |
191
|
|
|
General::intval($config['port']), |
192
|
|
|
$config['charset'] |
193
|
|
|
); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Getter for the version of the SQL server. |
198
|
|
|
* |
199
|
|
|
* @return string |
200
|
|
|
*/ |
201
|
|
|
public function getVersion() |
202
|
|
|
{ |
203
|
|
|
return $this->version; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Creates a PDO connection to the desired database given the current config. |
208
|
|
|
* This will also set the error mode to be exceptions, |
209
|
|
|
* which are handled by this class. |
210
|
|
|
* |
211
|
|
|
* @link http://www.php.net/manual/en/pdo.drivers.php |
212
|
|
|
* @param array $options |
213
|
|
|
* @return Database |
214
|
|
|
* The current instance if connection was successful. |
215
|
|
|
* @throws DatabaseException |
216
|
|
|
*/ |
217
|
|
|
public function connect() |
218
|
|
|
{ |
219
|
|
|
try { |
220
|
|
|
$config = $this->config; |
221
|
|
|
$this->conn = new PDO( |
222
|
|
|
$this->getDSN(), |
223
|
|
|
$config['user'], |
224
|
|
|
$config['password'], |
225
|
|
|
is_array($config['options']) ? $config['options'] : [] |
226
|
|
|
); |
227
|
|
|
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); |
228
|
|
|
$this->conn->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); |
229
|
|
|
$this->version = $this->select(['VERSION()'])->execute()->variable(0); |
230
|
|
|
} catch (PDOException $ex) { |
231
|
|
|
$this->throwDatabaseError($ex); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return $this; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Checks if the connection was already made successfully. |
239
|
|
|
* |
240
|
|
|
* @return boolean |
241
|
|
|
* true if the connection was made, false otherwise |
242
|
|
|
*/ |
243
|
|
|
public function isConnected() |
244
|
|
|
{ |
245
|
|
|
return $this->conn && $this->conn instanceof PDO; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Issues a call to `connect()` if the current instance is not already |
250
|
|
|
* connected. Does nothing if already connected. |
251
|
|
|
* |
252
|
|
|
* @see isConnected() |
253
|
|
|
* @return Database |
254
|
|
|
* The current instance. |
255
|
|
|
* @throws DatabaseException |
256
|
|
|
*/ |
257
|
|
|
private function autoConnect() |
258
|
|
|
{ |
259
|
|
|
if (!$this->isConnected()) { |
260
|
|
|
$this->connect(); |
261
|
|
|
} |
262
|
|
|
return $this; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Returns the number of queries that has been executed since |
267
|
|
|
* the creation of the object. |
268
|
|
|
* |
269
|
|
|
* @return int |
270
|
|
|
* The total number of query executed. |
271
|
|
|
*/ |
272
|
|
|
public function queryCount() |
273
|
|
|
{ |
274
|
|
|
return $this->queryCount; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Sets query caching to true. This will prepend all SELECT |
279
|
|
|
* queries with SQL_CACHE. Symphony by default enables caching. It |
280
|
|
|
* can be turned off by setting the query_cache parameter to 'off' in the |
281
|
|
|
* Symphony config file. |
282
|
|
|
* |
283
|
|
|
* @link https://dev.mysql.com/doc/refman/5.1/en/query-cache.html |
284
|
|
|
* @deprecated The query cache is deprecated as of MySQL 5.7.20, |
285
|
|
|
* and is removed in MySQL 8.0. |
286
|
|
|
* @link https://dev.mysql.com/doc/refman/5.7/en/query-cache-in-select.html |
287
|
|
|
* @return Database |
288
|
|
|
* The current instance |
289
|
|
|
*/ |
290
|
|
|
public function enableCaching() |
291
|
|
|
{ |
292
|
|
|
$this->config['query_caching'] = true; |
293
|
|
|
return $this; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Sets query caching to false. This will prepend all SELECT |
298
|
|
|
* queries will SQL_NO_CACHE. |
299
|
|
|
* |
300
|
|
|
* @deprecated The query cache is deprecated as of MySQL 5.7.20, |
301
|
|
|
* and is removed in MySQL 8.0. |
302
|
|
|
* @link https://dev.mysql.com/doc/refman/5.7/en/query-cache-in-select.html |
303
|
|
|
* @return Database |
304
|
|
|
* The current instance |
305
|
|
|
*/ |
306
|
|
|
public function disableCaching() |
307
|
|
|
{ |
308
|
|
|
$this->config['query_caching'] = false; |
309
|
|
|
return $this; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Returns boolean if query caching is enabled or not. |
314
|
|
|
* |
315
|
|
|
* @deprecated The query cache is deprecated as of MySQL 5.7.20, |
316
|
|
|
* and is removed in MySQL 8.0. |
317
|
|
|
* @link https://dev.mysql.com/doc/refman/5.7/en/query-cache-in-select.html |
318
|
|
|
* @return boolean |
319
|
|
|
*/ |
320
|
|
|
public function isCachingEnabled() |
321
|
|
|
{ |
322
|
|
|
return in_array($this->config['query_caching'], ['on', true], true); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Symphony uses a prefix for all it's database tables so it can live peacefully |
327
|
|
|
* on the same database as other applications. By default this is sym_, but it |
328
|
|
|
* can be changed when Symphony is installed. |
329
|
|
|
* |
330
|
|
|
* @param string $prefix |
331
|
|
|
* The table prefix for Symphony, by default this is sym_ |
332
|
|
|
* @return Database |
333
|
|
|
* The current instance |
334
|
|
|
*/ |
335
|
|
|
public function setPrefix($prefix) |
336
|
|
|
{ |
337
|
|
|
$this->config['tbl_prefix'] = $prefix; |
338
|
|
|
return $this; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Returns the prefix used by Symphony for this Database instance. |
343
|
|
|
* |
344
|
|
|
* @see __construct() |
345
|
|
|
* @since Symphony 2.4 |
346
|
|
|
* @return string |
347
|
|
|
*/ |
348
|
|
|
public function getPrefix() |
349
|
|
|
{ |
350
|
|
|
return $this->config['tbl_prefix']; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Sets query logging to true. |
355
|
|
|
* |
356
|
|
|
* @return Database |
357
|
|
|
* The current instance |
358
|
|
|
*/ |
359
|
|
|
public function enableLogging() |
360
|
|
|
{ |
361
|
|
|
$this->config['query_logging'] = true; |
362
|
|
|
return $this; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* Sets query logging to false. |
367
|
|
|
* |
368
|
|
|
* @return Database |
369
|
|
|
* The current instance |
370
|
|
|
*/ |
371
|
|
|
public function disableLogging() |
372
|
|
|
{ |
373
|
|
|
$this->config['query_logging'] = false; |
374
|
|
|
return $this; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Returns true if logging of queries is enabled. |
379
|
|
|
* |
380
|
|
|
* @return boolean |
381
|
|
|
*/ |
382
|
|
|
public function isLoggingEnabled() |
383
|
|
|
{ |
384
|
|
|
return in_array($this->config['query_logging'], ['on', true], true); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Sets the Database connection to use this timezone instead of the default |
389
|
|
|
* Database server timezone. |
390
|
|
|
* |
391
|
|
|
* @throws DatabaseException |
392
|
|
|
* @link https://dev.mysql.com/doc/refman/5.6/en/time-zone-support.html |
393
|
|
|
* @link https://github.com/symphonycms/symphony-2/issues/1726 |
394
|
|
|
* @since Symphony 2.3.3 |
395
|
|
|
* @param string $timezone |
396
|
|
|
* PHP's Human readable timezone, such as Australia/Brisbane. |
397
|
|
|
* @return boolean |
398
|
|
|
*/ |
399
|
|
|
public function setTimeZone($timezone = null) |
400
|
|
|
{ |
401
|
|
|
// This should throw, default value should be removed |
402
|
|
|
if (!$timezone) { |
403
|
|
|
return true; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
// What is the time now in the install timezone |
407
|
|
|
$symphony_date = new DateTime('now', new DateTimeZone($timezone)); |
408
|
|
|
|
409
|
|
|
// MySQL wants the offset to be in the format +/-H:I, getOffset returns offset in seconds |
410
|
|
|
$utc = new DateTime('now ' . $symphony_date->getOffset() . ' seconds', new DateTimeZone("UTC")); |
411
|
|
|
|
412
|
|
|
// Get the difference between the symphony install timezone and UTC |
413
|
|
|
$offset = $symphony_date->diff($utc)->format('%R%H:%I'); |
414
|
|
|
|
415
|
|
|
return $this->set('time_zone') |
416
|
|
|
->value((string)$offset) |
417
|
|
|
->execute() |
418
|
|
|
->success(); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* This function will clean a string using the `PDO::quote` function |
423
|
|
|
* taking into account the current database character encoding. |
424
|
|
|
* |
425
|
|
|
* If not connected to the database, it will default to PHP's `addslashes`. |
426
|
|
|
* This is useful for unit tests. |
427
|
|
|
* |
428
|
|
|
* This function does not encode _ or %. |
429
|
|
|
* |
430
|
|
|
* This function should not be used. Instead, pass your data in the proper |
431
|
|
|
* function that will delegate to SQL parameters. |
432
|
|
|
* |
433
|
|
|
* @deprecated @since Symphony 3.0.0 |
434
|
|
|
* @see DatabaseStatement::appendValues() |
435
|
|
|
* @param string $value |
436
|
|
|
* The string to be encoded into an escaped SQL string |
437
|
|
|
* @return string |
438
|
|
|
* The escaped SQL string |
439
|
|
|
*/ |
440
|
|
|
public function quote($value) |
441
|
|
|
{ |
442
|
|
|
if (!$this->isConnected()) { |
443
|
|
|
return "'" . addslashes($value) . "'"; |
444
|
|
|
} |
445
|
|
|
return $this->conn->quote($value); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* This function will apply the `quote` function to an associative |
450
|
|
|
* array of data, encoding only the value, not the key. This function |
451
|
|
|
* can handle recursive arrays. This function manipulates the given |
452
|
|
|
* parameter by reference. |
453
|
|
|
* |
454
|
|
|
* This function should not be used. Instead, pass your data in the proper |
455
|
|
|
* function that will delegate to SQL parameters. |
456
|
|
|
* |
457
|
|
|
* @deprecated @since Symphony 3.0.0 |
458
|
|
|
* @see quote |
459
|
|
|
* @param array $array |
460
|
|
|
* The associative array of data to encode, this parameter is manipulated |
461
|
|
|
* by reference. |
462
|
|
|
*/ |
463
|
|
|
public function quoteFields(array &$array) |
464
|
|
|
{ |
465
|
|
|
foreach ($array as $key => $val) { |
466
|
|
|
// Handle arrays with more than 1 level |
467
|
|
|
if (is_array($val)) { |
468
|
|
|
$this->quoteFields($val); |
|
|
|
|
469
|
|
|
} elseif (!$val || strlen(trim($val)) === 0) { |
470
|
|
|
$array[$key] = 'NULL'; |
471
|
|
|
} else { |
472
|
|
|
$array[$key] = $this->quote($val); |
|
|
|
|
473
|
|
|
} |
474
|
|
|
} |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* This function takes `$table` and `$field` names and returns true |
479
|
|
|
* if the `$table` contains a column named `$field`. |
480
|
|
|
* |
481
|
|
|
* @since Symphony 2.3 |
482
|
|
|
* @see describe |
483
|
|
|
* @link https://dev.mysql.com/doc/refman/en/describe.html |
484
|
|
|
* @param string $table |
485
|
|
|
* The table name |
486
|
|
|
* @param string $field |
487
|
|
|
* The field name |
488
|
|
|
* @throws DatabaseException |
489
|
|
|
* @return boolean |
490
|
|
|
* true if `$table` contains `$field`, false otherwise |
491
|
|
|
*/ |
492
|
|
|
public function tableContainsField($table, $field) |
493
|
|
|
{ |
494
|
|
|
return $this->describe($table) |
495
|
|
|
->field($field) |
496
|
|
|
->execute() |
497
|
|
|
->next() !== null; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* This function takes `$table` and returns boolean |
502
|
|
|
* if it exists or not. |
503
|
|
|
* |
504
|
|
|
* @since Symphony 2.3.4 |
505
|
|
|
* @see show |
506
|
|
|
* @link https://dev.mysql.com/doc/refman/en/show-tables.html |
507
|
|
|
* @param string $table |
508
|
|
|
* The table name |
509
|
|
|
* @throws DatabaseException |
510
|
|
|
* @return boolean |
511
|
|
|
* true if `$table` exists, false otherwise |
512
|
|
|
*/ |
513
|
|
|
public function tableExists($table) |
514
|
|
|
{ |
515
|
|
|
return $this->show() |
516
|
|
|
->like($table) |
517
|
|
|
->execute() |
518
|
|
|
->next() !== null; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
/** |
522
|
|
|
* Factory method that creates a new, empty statement. |
523
|
|
|
* |
524
|
|
|
* @param string $action |
525
|
|
|
* The SQL clause name. Default to empty string. |
526
|
|
|
* @return DatabaseStatement |
527
|
|
|
*/ |
528
|
|
|
public function statement($action = '') |
529
|
|
|
{ |
530
|
|
|
return new DatabaseStatement($this, $action); |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
/** |
534
|
|
|
* Factory method that creates a new `SELECT ...` statement. |
535
|
|
|
* |
536
|
|
|
* @param array $projection |
537
|
|
|
* The columns to select. |
538
|
|
|
* If no projection gets added, it defaults to `DatabaseQuery::getDefaultProjection()`. |
539
|
|
|
* @return DatabaseQuery |
540
|
|
|
*/ |
541
|
|
|
public function select(array $projection = []) |
542
|
|
|
{ |
543
|
|
|
return new DatabaseQuery($this, $projection); |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
/** |
547
|
|
|
* Factory method that creates a new `SHOW TABLES` statement. |
548
|
|
|
* |
549
|
|
|
* @return DatabaseShow |
550
|
|
|
*/ |
551
|
|
|
public function show() |
552
|
|
|
{ |
553
|
|
|
return new DatabaseShow($this); |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
/** |
557
|
|
|
* Factory method that creates a new `SHOW COLUMNS` statement. |
558
|
|
|
* |
559
|
|
|
* @return DatabaseShow |
560
|
|
|
*/ |
561
|
|
|
public function showColumns() |
562
|
|
|
{ |
563
|
|
|
return new DatabaseShow($this, 'COLUMNS'); |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
/** |
567
|
|
|
* Factory method that creates a new `SHOW INDEX` statement. |
568
|
|
|
* |
569
|
|
|
* @return DatabaseShow |
570
|
|
|
*/ |
571
|
|
|
public function showIndex() |
572
|
|
|
{ |
573
|
|
|
return new DatabaseShow($this, 'INDEX'); |
574
|
|
|
} |
575
|
|
|
|
576
|
|
|
/** |
577
|
|
|
* Factory method that creates a new `INSERT` statement. |
578
|
|
|
* |
579
|
|
|
* @param string $table |
580
|
|
|
* The name of the table to act on, including the tbl prefix which will be changed |
581
|
|
|
* to the Database table prefix. |
582
|
|
|
* @deprecated Symphony 3.0.0 |
583
|
|
|
* If $table is an array, it is treated as the fields values |
584
|
|
|
* Use DatabaseInsert::values() |
585
|
|
|
* @param string $table |
586
|
|
|
* The name of the table to act on, including the tbl prefix which will be changed |
587
|
|
|
* to the Database table prefix. |
588
|
|
|
* @deprecated Symphony 3.0.0 |
589
|
|
|
* This parameter is deprecated and will be removed. |
590
|
|
|
* Use the first parameter and DatabaseInsert::values() |
591
|
|
|
* @param bool $updateOnDuplicate |
592
|
|
|
* If set to true, data will updated if any key constraints are found that cause |
593
|
|
|
* conflicts. Defaults to false |
594
|
|
|
* @deprecated Symphony 3.0.0 |
595
|
|
|
* This parameter is deprecated and will be removed. |
596
|
|
|
* Use DatabaseInsert::updateOnDuplicateKey() |
597
|
|
|
* @return DatabaseInsert |
598
|
|
|
*/ |
599
|
|
|
public function insert($table, ...$oldParams) |
600
|
|
|
{ |
601
|
|
|
// Compat layer |
602
|
|
|
if (is_array($table)) { |
|
|
|
|
603
|
|
|
if (isset($oldParams[0]) && isset($oldParams[1])) { |
604
|
|
|
return $this->_insert($table, $oldParams[0], $oldParams[1]); |
605
|
|
|
} |
606
|
|
|
return $this->_insert($table, $oldParams[0]); |
607
|
|
|
} |
608
|
|
|
return new DatabaseInsert($this, $table); |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
/** |
612
|
|
|
* Returns the last insert ID from the previous query. This is |
613
|
|
|
* the value from an auto_increment field. |
614
|
|
|
* If the lastInsertId is empty or not a valid integer, -1 is returned. |
615
|
|
|
* |
616
|
|
|
* @return int |
617
|
|
|
* The last interested row's ID |
618
|
|
|
*/ |
619
|
|
|
public function getInsertID() |
620
|
|
|
{ |
621
|
|
|
return General::intval($this->conn->lastInsertId()); |
622
|
|
|
} |
623
|
|
|
|
624
|
|
|
/** |
625
|
|
|
* Factory method that creates a new `UPDATE` statement. |
626
|
|
|
* |
627
|
|
|
* @param string $table |
628
|
|
|
* The name of the table to act on, including the tbl prefix which will be changed |
629
|
|
|
* to the Database table prefix. |
630
|
|
|
* @param string $where |
631
|
|
|
* An unsanitized WHERE condition. |
632
|
|
|
* @deprecated Symphony 3.0.0 |
633
|
|
|
* This parameter is deprecated and will be removed. |
634
|
|
|
* Use DatabaseUpdate::where() |
635
|
|
|
* @return DatabaseUpdate |
636
|
|
|
*/ |
637
|
|
|
public function update($table, ...$oldParams) |
638
|
|
|
{ |
639
|
|
|
// Compat layer |
640
|
|
|
if (is_array($table)) { |
|
|
|
|
641
|
|
|
if (isset($oldParams[0]) && isset($oldParams[1])) { |
642
|
|
|
return $this->_update($table, $oldParams[0], $oldParams[1]); |
643
|
|
|
} |
644
|
|
|
return $this->_update($table, $oldParams[0]); |
645
|
|
|
} |
646
|
|
|
return new DatabaseUpdate($this, $table); |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
/** |
650
|
|
|
* Factory method that creates a new `DELETE` statement. |
651
|
|
|
* |
652
|
|
|
* @param string $table |
653
|
|
|
* The name of the table to act on, including the tbl prefix which will be changed |
654
|
|
|
* to the Database table prefix. |
655
|
|
|
* @param string $where |
656
|
|
|
* An unsanitized WHERE condition. |
657
|
|
|
* @deprecated Symphony 3.0.0 |
658
|
|
|
* This parameter is deprecated and will be removed. |
659
|
|
|
* Use DatabaseDelete::where() |
660
|
|
|
* @return DatabaseDelete |
661
|
|
|
*/ |
662
|
|
|
public function delete($table, $where = null) |
663
|
|
|
{ |
664
|
|
|
$stm = new DatabaseDelete($this, $table); |
665
|
|
|
// Compat layer |
666
|
|
|
if ($where) { |
667
|
|
|
$where = $stm->replaceTablePrefix($where); |
668
|
|
|
$stm->unsafe()->unsafeAppendSQLPart('where', "WHERE $where"); |
669
|
|
|
return $stm->execute()->success(); |
|
|
|
|
670
|
|
|
} |
671
|
|
|
return $stm; |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
/** |
675
|
|
|
* Factory method that creates a new `DROP` statement. |
676
|
|
|
* |
677
|
|
|
* @param string $table |
678
|
|
|
* @return DatabaseDrop |
679
|
|
|
*/ |
680
|
|
|
public function drop($table) |
681
|
|
|
{ |
682
|
|
|
return new DatabaseDrop($this, $table); |
683
|
|
|
} |
684
|
|
|
|
685
|
|
|
/** |
686
|
|
|
* Factory method that creates a new `DESCRIBE` statement. |
687
|
|
|
* |
688
|
|
|
* @param string $table |
689
|
|
|
* @return DatabaseDescribe |
690
|
|
|
*/ |
691
|
|
|
public function describe($table) |
692
|
|
|
{ |
693
|
|
|
return new DatabaseDescribe($this, $table); |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
/** |
697
|
|
|
* Factory method that creates a new `CREATE TABLE` statement. |
698
|
|
|
* Also sets the charset, collate and engine values using the |
699
|
|
|
* instance configuration. |
700
|
|
|
* |
701
|
|
|
* @param string $table |
702
|
|
|
* @return DatabaseCreate |
703
|
|
|
*/ |
704
|
|
|
public function create($table) |
705
|
|
|
{ |
706
|
|
|
return (new DatabaseCreate($this, $table)) |
707
|
|
|
->charset($this->config['charset']) |
708
|
|
|
->collate($this->config['collate']) |
709
|
|
|
->engine($this->config['engine']); |
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
/** |
713
|
|
|
* Factory method that creates a new `ALTER TABLE` statement. |
714
|
|
|
* Also sets the collate value using the instance configuration. |
715
|
|
|
* |
716
|
|
|
* @param string $table |
717
|
|
|
* @return DatabaseAlter |
718
|
|
|
*/ |
719
|
|
|
public function alter($table) |
720
|
|
|
{ |
721
|
|
|
return (new DatabaseAlter($this, $table)) |
722
|
|
|
->collate($this->config['collate']); |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
/** |
726
|
|
|
* Factory method that creates a new `OPTIMIZE TABLE` statement. |
727
|
|
|
* |
728
|
|
|
* @param string $table |
729
|
|
|
* @return DatabaseOptimize |
730
|
|
|
*/ |
731
|
|
|
public function optimize($table) |
732
|
|
|
{ |
733
|
|
|
return new DatabaseOptimize($this, $table); |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
/** |
737
|
|
|
* Factory method that creates a new `TRUNCATE TABLE` statement. |
738
|
|
|
* |
739
|
|
|
* @param string $table |
740
|
|
|
* @return DatabaseTruncate |
741
|
|
|
*/ |
742
|
|
|
public function truncate($table) |
743
|
|
|
{ |
744
|
|
|
return new DatabaseTruncate($this, $table); |
745
|
|
|
} |
746
|
|
|
|
747
|
|
|
/** |
748
|
|
|
* Factory method that creates a new `SET` statement. |
749
|
|
|
* |
750
|
|
|
* @param string $variable |
751
|
|
|
* @return DatabaseSet |
752
|
|
|
*/ |
753
|
|
|
public function set($variable) |
754
|
|
|
{ |
755
|
|
|
return new DatabaseSet($this, $variable); |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
/** |
759
|
|
|
* Begins a new transaction. |
760
|
|
|
* This method calls `autoConnect()` before forwarding the call to PDO. |
761
|
|
|
* |
762
|
|
|
* @return boolean |
763
|
|
|
*/ |
764
|
|
|
public function beginTransaction() |
765
|
|
|
{ |
766
|
|
|
$this->autoConnect(); |
767
|
|
|
return $this->conn->beginTransaction(); |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
/** |
771
|
|
|
* Commits the lastly created transaction. |
772
|
|
|
* This method calls `autoConnect()` before forwarding the call to PDO. |
773
|
|
|
* |
774
|
|
|
* @return boolean |
775
|
|
|
*/ |
776
|
|
|
public function commit() |
777
|
|
|
{ |
778
|
|
|
$this->autoConnect(); |
779
|
|
|
return $this->conn->commit(); |
780
|
|
|
} |
781
|
|
|
|
782
|
|
|
/** |
783
|
|
|
* Rollbacks the lastly created transaction. |
784
|
|
|
* This method calls `autoConnect()` before forwarding the call to PDO. |
785
|
|
|
* |
786
|
|
|
* @return boolean |
787
|
|
|
*/ |
788
|
|
|
public function rollBack() |
789
|
|
|
{ |
790
|
|
|
$this->autoConnect(); |
791
|
|
|
return $this->conn->rollBack(); |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
/** |
795
|
|
|
* Check if we are currently in a transaction. |
796
|
|
|
* This method calls `autoConnect()` before forwarding the call to PDO. |
797
|
|
|
* |
798
|
|
|
* @return boolean |
799
|
|
|
*/ |
800
|
|
|
public function inTransaction() |
801
|
|
|
{ |
802
|
|
|
$this->autoConnect(); |
803
|
|
|
return $this->conn->inTransaction(); |
804
|
|
|
} |
805
|
|
|
|
806
|
|
|
/** |
807
|
|
|
* Factory method that creates a new DatabaseTransaction object. |
808
|
|
|
* $tx will be called with a single parameter: the instance of the current Database object. |
809
|
|
|
* |
810
|
|
|
* @param callable $tx |
811
|
|
|
* The code to execute in the transaction |
812
|
|
|
* @return DatabaseTransaction |
813
|
|
|
*/ |
814
|
|
|
public function transaction($tx) |
815
|
|
|
{ |
816
|
|
|
return new DatabaseTransaction($this, $tx); |
817
|
|
|
} |
818
|
|
|
|
819
|
|
|
/** |
820
|
|
|
* @internal |
821
|
|
|
* Finds the best possible PDO::PARAM_* value to bind with, based on the PHP type. |
822
|
|
|
* |
823
|
|
|
* @param mixed $value |
824
|
|
|
* The value on which to deduce its PDO type |
825
|
|
|
* @return int |
826
|
|
|
* Either PDO::PARAM_NULL, PDO::PARAM_INT, PDO::PARAM_BOOL or PDO::PARAM_STR |
827
|
|
|
*/ |
828
|
|
|
public function deducePDOParamType($value) |
829
|
|
|
{ |
830
|
|
|
if ($value === null) { |
831
|
|
|
return PDO::PARAM_NULL; |
832
|
|
|
} elseif (is_numeric($value)) { |
833
|
|
|
return PDO::PARAM_INT; |
834
|
|
|
} elseif (is_bool($value)) { |
835
|
|
|
return PDO::PARAM_BOOL; |
836
|
|
|
} |
837
|
|
|
return PDO::PARAM_STR; |
838
|
|
|
} |
839
|
|
|
|
840
|
|
|
/** |
841
|
|
|
* Given a DatabaseStatement, it will execute it and return |
842
|
|
|
* its result, by calling `DatabaseStatement::result()`. |
843
|
|
|
* Any error will throw a DatabaseException. |
844
|
|
|
* |
845
|
|
|
* Developers are encouraged to call `DatabaseStatement::execute()` instead, |
846
|
|
|
* because it will make sure to set required state properly. |
847
|
|
|
* |
848
|
|
|
* @see validateSQLQuery() |
849
|
|
|
* @see DatabaseStatement::execute() |
850
|
|
|
* @see DatabaseStatement::result() |
851
|
|
|
* @param string $query |
852
|
|
|
* @return DatabaseStatementResult |
853
|
|
|
* @throws DatabaseException |
854
|
|
|
*/ |
855
|
|
|
public function execute(DatabaseStatement $stm) |
856
|
|
|
{ |
857
|
|
|
$this->autoConnect(); |
858
|
|
|
|
859
|
|
|
if ($this->isLoggingEnabled()) { |
860
|
|
|
$start = precision_timer(); |
861
|
|
|
} |
862
|
|
|
|
863
|
|
|
$query = $stm->generateSQL(); |
864
|
|
|
$values = $stm->getValues(); |
865
|
|
|
$result = null; |
|
|
|
|
866
|
|
|
|
867
|
|
|
// Cleanup from last time, set some logging parameters |
868
|
|
|
$this->flush(); |
869
|
|
|
$this->lastQuery = $stm->generateFormattedSQL(); |
870
|
|
|
$this->lastQueryHash = md5($query . $start); |
|
|
|
|
871
|
|
|
$this->lastQueryValues = $values; |
872
|
|
|
$this->lastQuerySafe = $stm->isSafe(); |
873
|
|
|
|
874
|
|
|
try { |
875
|
|
|
// Validate the query |
876
|
|
|
$this->validateSQLQuery($query, $stm->isSafe()); |
877
|
|
|
// Prepare the query |
878
|
|
|
$pstm = $this->conn->prepare($query); |
879
|
|
|
// Bind all values |
880
|
|
|
foreach ($values as $param => $value) { |
881
|
|
|
if (General::intval($param) !== -1) { |
882
|
|
|
$param = $param + 1; |
883
|
|
|
} else { |
884
|
|
|
$param = ":$param"; |
885
|
|
|
} |
886
|
|
|
$pstm->bindValue($param, $value, $this->deducePDOParamType($value)); |
887
|
|
|
} |
888
|
|
|
// Execute it |
889
|
|
|
$result = $pstm->execute(); |
890
|
|
|
$this->queryCount++; |
891
|
|
|
} catch (PDOException $ex) { |
892
|
|
|
$this->throwDatabaseError($ex); |
893
|
|
|
return; |
894
|
|
|
} |
895
|
|
|
|
896
|
|
|
// Check for errors |
897
|
|
|
if ($this->conn->errorCode() !== PDO::ERR_NONE) { |
898
|
|
|
$this->throwDatabaseError(); |
899
|
|
|
return; |
900
|
|
|
} |
901
|
|
|
|
902
|
|
|
// Log the query |
903
|
|
|
if ($this->isLoggingEnabled()) { |
904
|
|
|
$this->logLastQuery(precision_timer('stop', $start)); |
905
|
|
|
} |
906
|
|
|
|
907
|
|
|
return $stm->results($result, $pstm); |
|
|
|
|
908
|
|
|
} |
909
|
|
|
|
910
|
|
|
/** |
911
|
|
|
* @internal |
912
|
|
|
* This method checks for common pattern of SQL injection, like `--`, `'` and `;`. |
913
|
|
|
* |
914
|
|
|
* @see execute() |
915
|
|
|
* @param string $query |
916
|
|
|
* The query to test. |
917
|
|
|
* @param boolean $strict |
918
|
|
|
* Perform extra validation. True by default. |
919
|
|
|
* @return void |
920
|
|
|
* @throws DatabaseStatementException |
921
|
|
|
*/ |
922
|
|
|
final public function validateSQLQuery($query, $strict = true) |
923
|
|
|
{ |
924
|
|
|
if (strpos($query, '\'--;') !== false) { |
925
|
|
|
throw (new DatabaseStatementException('Query contains SQL injection.'))->sql($query); |
926
|
|
|
} elseif ($strict && strpos($query, '--') !== false) { |
927
|
|
|
throw (new DatabaseStatementException('Query contains illegal characters: `--`.'))->sql($query); |
928
|
|
|
} elseif ($strict && strpos($query, '\'') !== false) { |
929
|
|
|
throw (new DatabaseStatementException('Query contains illegal character: `\'`.'))->sql($query); |
930
|
|
|
} elseif ($strict && strpos($query, ';') !== false) { |
931
|
|
|
throw (new DatabaseStatementException('Query contains illegal character: `;`.'))->sql($query); |
932
|
|
|
} |
933
|
|
|
} |
934
|
|
|
|
935
|
|
|
/** |
936
|
|
|
* Convenience function to allow you to execute multiple SQL queries at once |
937
|
|
|
* by providing a string with the queries delimited with a `;` |
938
|
|
|
* |
939
|
|
|
* @throws DatabaseException |
940
|
|
|
* @throws Exception |
941
|
|
|
* @param string $sql |
942
|
|
|
* A string containing SQL queries delimited by `;` |
943
|
|
|
* @param boolean $force_engine |
944
|
|
|
* @deprecated @since Symphony 3.0.0 |
945
|
|
|
* The default engine is now InnoDb. |
946
|
|
|
* The import script should use InnoDb as well. |
947
|
|
|
* The import script is also run in a transaction, resulting in a all or nothing import. |
948
|
|
|
* Before 3.0.0: |
949
|
|
|
* If set to true, this will set MySQL's default storage engine to MyISAM. |
950
|
|
|
* Defaults to false, which will use MySQL's default storage engine when |
951
|
|
|
* tables don't explicitly define which engine they should be created with |
952
|
|
|
* @return boolean |
953
|
|
|
* If one of the queries fails, false will be returned and no further queries |
954
|
|
|
* will be executed, otherwise true will be returned. |
955
|
|
|
*/ |
956
|
|
|
public function import($sql, $force_engine = false) |
957
|
|
|
{ |
958
|
|
|
General::ensureType([ |
959
|
|
|
'sql' => ['var' => $sql, 'type' => 'string'], |
960
|
|
|
]); |
961
|
|
|
$queries = preg_split('/;[\\r\\n]+/', $sql, -1, PREG_SPLIT_NO_EMPTY); |
962
|
|
|
|
963
|
|
|
if (!is_array($queries) || empty($queries) || count($queries) <= 0) { |
964
|
|
|
throw new Exception('The SQL string contains no queries.'); |
965
|
|
|
} |
966
|
|
|
|
967
|
|
|
return $this->transaction(function (Database $db) use ($queries) { |
968
|
|
|
foreach ($queries as $sql) { |
969
|
|
|
if (trim($sql) !== '') { |
970
|
|
|
$stm = $db->statement(); |
971
|
|
|
$sql = $stm->replaceTablePrefix($sql); |
972
|
|
|
$stm->unsafe()->unsafeAppendSQLPart('statement', $sql); |
973
|
|
|
if (!$stm->execute()->success()) { |
974
|
|
|
throw new DatabaseException('Failed to execute import statement'); |
975
|
|
|
} |
976
|
|
|
} |
977
|
|
|
} |
978
|
|
|
})->execute()->success(); |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
/** |
982
|
|
|
* Given an Exception, or called when an error occurs, this function will |
983
|
|
|
* fire the `QueryExecutionError` delegate and then raise a `DatabaseException` |
984
|
|
|
* |
985
|
|
|
* @uses QueryExecutionError |
986
|
|
|
* @throws DatabaseException |
987
|
|
|
* @param Exception $ex |
988
|
|
|
* The exception thrown while doing something with the Database |
989
|
|
|
*/ |
990
|
|
|
private function throwDatabaseError(Exception $ex = null) |
991
|
|
|
{ |
992
|
|
|
if (isset($ex) && $ex) { |
993
|
|
|
$msg = $ex->getMessage(); |
994
|
|
|
$errornum = (int)$ex->getCode(); |
995
|
|
|
} else { |
996
|
|
|
$error = $this->conn->errorInfo(); |
997
|
|
|
$msg = $error[2]; |
998
|
|
|
$errornum = $error[0]; |
999
|
|
|
} |
1000
|
|
|
|
1001
|
|
|
/** |
1002
|
|
|
* After a query execution has failed this delegate will provide the query, |
1003
|
|
|
* query hash, error message and the error number. |
1004
|
|
|
* |
1005
|
|
|
* Note that this function only starts logging once the `ExtensionManager` |
1006
|
|
|
* is available, which means it will not fire for the first couple of |
1007
|
|
|
* queries that set the character set. |
1008
|
|
|
* |
1009
|
|
|
* @since Symphony 2.3 |
1010
|
|
|
* @delegate QueryExecutionError |
1011
|
|
|
* @param string $context |
1012
|
|
|
* '/frontend/' or '/backend/' |
1013
|
|
|
* @param string $query |
1014
|
|
|
* The query that has just been executed |
1015
|
|
|
* @param string $query_hash |
1016
|
|
|
* The hash used by Symphony to uniquely identify this query |
1017
|
|
|
* @param string $msg |
1018
|
|
|
* The error message provided by MySQL which includes information on why the execution failed |
1019
|
|
|
* @param int $num |
1020
|
|
|
* The error number that corresponds with the MySQL error message |
1021
|
|
|
* @param Exception $exception |
1022
|
|
|
* @since Symphony 3.0.0 |
1023
|
|
|
* The raised exception, if any |
1024
|
|
|
*/ |
1025
|
|
|
if (Symphony::ExtensionManager() instanceof ExtensionManager) { |
|
|
|
|
1026
|
|
|
Symphony::ExtensionManager()->notifyMembers( |
1027
|
|
|
'QueryExecutionError', |
1028
|
|
|
class_exists('Administration') ? '/backend/' : '/frontend/', |
1029
|
|
|
[ |
1030
|
|
|
'query' => $this->lastQuery, |
1031
|
|
|
'query_hash' => $this->lastQueryHash, |
1032
|
|
|
'msg' => $msg, |
1033
|
|
|
'num' => $errornum, |
1034
|
|
|
'exception' => $ex, |
1035
|
|
|
] |
1036
|
|
|
); |
1037
|
|
|
} |
1038
|
|
|
|
1039
|
|
|
throw new DatabaseException( |
1040
|
|
|
__( |
1041
|
|
|
'Database Error (%1$s): %2$s in query:%4$s%3$s', |
1042
|
|
|
[$errornum, $msg, $this->lastQuery, PHP_EOL] |
1043
|
|
|
), |
1044
|
|
|
[ |
1045
|
|
|
'msg' => $msg, |
1046
|
|
|
'num' => $errornum, |
1047
|
|
|
'query' => $this->lastQuery |
1048
|
|
|
], |
1049
|
|
|
$ex |
1050
|
|
|
); |
1051
|
|
|
} |
1052
|
|
|
|
1053
|
|
|
/** |
1054
|
|
|
* Function is called every time a query is executed to log it for |
1055
|
|
|
* basic profiling/debugging purposes |
1056
|
|
|
* |
1057
|
|
|
* @uses PostQueryExecution |
1058
|
|
|
* @param int $stop |
1059
|
|
|
*/ |
1060
|
|
|
private function logLastQuery($stop) |
1061
|
|
|
{ |
1062
|
|
|
/** |
1063
|
|
|
* After a query has successfully executed, that is it was considered |
1064
|
|
|
* valid SQL, this delegate will provide the query, the query_hash and |
1065
|
|
|
* the execution time of the query. |
1066
|
|
|
* |
1067
|
|
|
* Note that this function only starts logging once the ExtensionManager |
1068
|
|
|
* is available, which means it will not fire for the first couple of |
1069
|
|
|
* queries that set the character set. |
1070
|
|
|
* |
1071
|
|
|
* @since Symphony 2.3 |
1072
|
|
|
* @delegate PostQueryExecution |
1073
|
|
|
* @param string $context |
1074
|
|
|
* '/frontend/' or '/backend/' |
1075
|
|
|
* @param string $query |
1076
|
|
|
* The query that has just been executed |
1077
|
|
|
* @param string $query_hash |
1078
|
|
|
* The hash used by Symphony to uniquely identify this query |
1079
|
|
|
* @param array $query_values |
1080
|
|
|
* @since Symphony 3.0.0 |
1081
|
|
|
* The values passed by Symphony to the database |
1082
|
|
|
* @param bool $query_safe |
1083
|
|
|
* @since Symphony 3.0.0 |
1084
|
|
|
* If the query was using the unsafe mode |
1085
|
|
|
* @param float $execution_time |
1086
|
|
|
* The time that it took to run `$query` |
1087
|
|
|
*/ |
1088
|
|
|
if (Symphony::ExtensionManager() instanceof ExtensionManager) { |
|
|
|
|
1089
|
|
|
// TODO: Log unlogged queries |
1090
|
|
|
Symphony::ExtensionManager()->notifyMembers( |
1091
|
|
|
'PostQueryExecution', |
1092
|
|
|
class_exists('Administration', false) ? '/backend/' : '/frontend/', |
1093
|
|
|
[ |
1094
|
|
|
'query' => $this->lastQuery, // TODO: Format |
1095
|
|
|
'query_hash' => $this->lastQueryHash, |
1096
|
|
|
'query_values' => $this->lastQueryValues, |
1097
|
|
|
'query_safe' => $this->lastQuerySafe, |
1098
|
|
|
'execution_time' => $stop |
1099
|
|
|
] |
1100
|
|
|
); |
1101
|
|
|
} |
1102
|
|
|
|
1103
|
|
|
// Keep internal log for easy debugging |
1104
|
|
|
$this->log[] = [ |
1105
|
|
|
'query' => $this->lastQuery, // TODO: Format |
1106
|
|
|
'query_hash' => $this->lastQueryHash, |
1107
|
|
|
'query_values' => $this->lastQueryValues, |
1108
|
|
|
'query_safe' => $this->lastQuerySafe, |
1109
|
|
|
'execution_time' => $stop |
1110
|
|
|
]; |
1111
|
|
|
} |
1112
|
|
|
|
1113
|
|
|
/** |
1114
|
|
|
* Returns some basic statistics from the Database class about the |
1115
|
|
|
* number of queries, the time it took to query and any slow queries. |
1116
|
|
|
* A slow query is defined as one that took longer than 0.0999 seconds |
1117
|
|
|
* This function is used by the Profile devkit |
1118
|
|
|
* |
1119
|
|
|
* @return array |
1120
|
|
|
* An associative array with the number of queries, an array of slow |
1121
|
|
|
* queries and the total query time. |
1122
|
|
|
*/ |
1123
|
|
|
public function getStatistics() |
1124
|
|
|
{ |
1125
|
|
|
$stats = []; |
|
|
|
|
1126
|
|
|
$query_timer = 0.0; |
1127
|
|
|
$slow_queries = []; |
1128
|
|
|
|
1129
|
|
|
foreach ($this->log as $key => $val) { |
1130
|
|
|
$query_timer += $val['execution_time']; |
1131
|
|
|
if ($val['execution_time'] > 0.0999) { |
1132
|
|
|
$slow_queries[] = $val; |
1133
|
|
|
} |
1134
|
|
|
} |
1135
|
|
|
|
1136
|
|
|
return [ |
1137
|
|
|
'queries' => $this->queryCount(), |
1138
|
|
|
'slow-queries' => $slow_queries, |
1139
|
|
|
'total-query-time' => number_format($query_timer, 5, '.', '') |
1140
|
|
|
]; |
1141
|
|
|
} |
1142
|
|
|
|
1143
|
|
|
//-------------------------------------------------------------------------- |
1144
|
|
|
// COMPAT LAYER |
1145
|
|
|
// All the following methods are deprecated and are there for |
1146
|
|
|
// backward compatibility only. |
1147
|
|
|
//-------------------------------------------------------------------------- |
1148
|
|
|
|
1149
|
|
|
/** |
1150
|
|
|
* Returns all the log entries by type. There are two valid types, |
1151
|
|
|
* error and debug. If no type is given, the entire log is returned, |
1152
|
|
|
* otherwise only log messages for that type are returned |
1153
|
|
|
* |
1154
|
|
|
* @deprecated @since Symphony 3.0.0 |
1155
|
|
|
* @see Database::getLogs() |
1156
|
|
|
* @param null|string $type |
1157
|
|
|
* @return array |
1158
|
|
|
* An array of associative array's. Log entries of the error type |
1159
|
|
|
* return the query the error occurred on and the error number and |
1160
|
|
|
* message from MySQL. Log entries of the debug type return the |
1161
|
|
|
* the query and the start/stop time to indicate how long it took |
1162
|
|
|
* to run |
1163
|
|
|
*/ |
1164
|
|
|
public function debug($type = null) |
1165
|
|
|
{ |
1166
|
|
|
return $this->getLogs(); |
1167
|
|
|
} |
1168
|
|
|
|
1169
|
|
|
/** |
1170
|
|
|
* This function will clean a string using the `PDO::quote` function |
1171
|
|
|
* taking into account the current database character encoding. Note that this |
1172
|
|
|
* function does not encode _ or %. |
1173
|
|
|
* |
1174
|
|
|
* @deprecated @since Symphony 3.0.0 |
1175
|
|
|
* @see quote |
1176
|
|
|
* @param string $value |
1177
|
|
|
* The string to be encoded into an escaped SQL string |
1178
|
|
|
* @return string |
1179
|
|
|
* The escaped SQL string |
1180
|
|
|
*/ |
1181
|
|
|
public function cleanValue($value) |
1182
|
|
|
{ |
1183
|
|
|
if (Symphony::Log()) { |
1184
|
|
|
Symphony::Log()->pushDeprecateWarningToLog('Database::cleanValue()', 'The new API'); |
1185
|
|
|
} |
1186
|
|
|
return trim($this->quote($value), "'"); |
|
|
|
|
1187
|
|
|
} |
1188
|
|
|
|
1189
|
|
|
/** |
1190
|
|
|
* This function will apply the `quote` function to an associative |
1191
|
|
|
* array of data, encoding only the value, not the key. This function |
1192
|
|
|
* can handle recursive arrays. This function manipulates the given |
1193
|
|
|
* parameter by reference. |
1194
|
|
|
* |
1195
|
|
|
* @deprecated @since Symphony 3.0.0 |
1196
|
|
|
* @see quoteFields |
1197
|
|
|
* @param array $array |
1198
|
|
|
* The associative array of data to encode, this parameter is manipulated |
1199
|
|
|
* by reference. |
1200
|
|
|
*/ |
1201
|
|
|
public function cleanFields(array &$array) |
1202
|
|
|
{ |
1203
|
|
|
$this->quoteFields($array); |
|
|
|
|
1204
|
|
|
} |
1205
|
|
|
|
1206
|
|
|
/** |
1207
|
|
|
* Determines whether this query is a read operation, or if it is a write operation. |
1208
|
|
|
* A write operation is determined as any query that starts with CREATE, INSERT, |
1209
|
|
|
* REPLACE, ALTER, DELETE, UPDATE, OPTIMIZE, TRUNCATE, DROP, LOCK or UNLOCK. Anything else is |
1210
|
|
|
* considered to be a read operation which are subject to query caching. |
1211
|
|
|
* |
1212
|
|
|
* @deprecated @since Symphony 3.0.0 |
1213
|
|
|
* @param string $query |
1214
|
|
|
* @return int |
1215
|
|
|
* `self::__WRITE_OPERATION__` or `self::__READ_OPERATION__` |
1216
|
|
|
*/ |
1217
|
|
|
public function determineQueryType($query) |
1218
|
|
|
{ |
1219
|
|
|
return preg_match( |
1220
|
|
|
'/^(create|insert|replace|alter|delete|update|optimize|truncate|drop|lock|unlock)/i', |
1221
|
|
|
$query |
1222
|
|
|
) === 1 ? self::__WRITE_OPERATION__ : self::__READ_OPERATION__; |
|
|
|
|
1223
|
|
|
} |
1224
|
|
|
|
1225
|
|
|
/** |
1226
|
|
|
* Takes an SQL string and executes it. This function will apply query |
1227
|
|
|
* caching if it is a read operation and if query caching is set. Symphony |
1228
|
|
|
* will convert the `tbl_` prefix of tables to be the one set during installation. |
1229
|
|
|
* A type parameter is provided to specify whether `$this->_lastResult` will be an array |
1230
|
|
|
* of objects or an array of associative arrays. The default is objects. This |
1231
|
|
|
* function will return boolean, but set `$this->_lastResult` to the result. |
1232
|
|
|
* |
1233
|
|
|
* @deprecated @since Symphony 3.0.0 |
1234
|
|
|
* @see select() |
1235
|
|
|
* @see insert() |
1236
|
|
|
* @see update() |
1237
|
|
|
* @see delete() |
1238
|
|
|
* @see create() |
1239
|
|
|
* @see alter() |
1240
|
|
|
* @see drop() |
1241
|
|
|
* @see truncate() |
1242
|
|
|
* @see optimize() |
1243
|
|
|
* @see set() |
1244
|
|
|
* @see autoConnect() |
1245
|
|
|
* @uses PostQueryExecution |
1246
|
|
|
* @param string $query |
1247
|
|
|
* The full SQL query to execute. |
1248
|
|
|
* @param string $type |
1249
|
|
|
* Whether to return the result as objects or associative array. Defaults |
1250
|
|
|
* to OBJECT which will return objects. The other option is ASSOC. If $type |
1251
|
|
|
* is not either of these, it will return objects. |
1252
|
|
|
* @throws DatabaseException |
1253
|
|
|
* @return boolean|Traversable |
1254
|
|
|
* true if the query executed without errors, false otherwise |
1255
|
|
|
*/ |
1256
|
|
|
public function query($query, $type = "OBJECT") |
1257
|
|
|
{ |
1258
|
|
|
General::ensureType([ |
1259
|
|
|
'query' => ['var' => $query, 'type' => 'string'], |
1260
|
|
|
'type' => ['var' => $type, 'type' => 'string'], |
1261
|
|
|
]); |
1262
|
|
|
$this->autoConnect(); |
1263
|
|
|
|
1264
|
|
|
if ($this->isLoggingEnabled()) { |
1265
|
|
|
$start = precision_timer(); |
1266
|
|
|
} |
1267
|
|
|
|
1268
|
|
|
$result = null; |
1269
|
|
|
// Format SQL because PDO does not seem to like it |
1270
|
|
|
$query = trim(str_replace(PHP_EOL, ' ', $query)); |
1271
|
|
|
$query = trim(str_replace('\t', ' ', $query)); |
1272
|
|
|
while (strpos($query, ' ') !== false) { |
1273
|
|
|
$query = str_replace(' ', ' ', $query); |
1274
|
|
|
} |
1275
|
|
|
if ($this->getPrefix() !== 'tbl_') { |
1276
|
|
|
$query = preg_replace('/tbl_(\S+?)([\s\.,]|$)/', $this->getPrefix().'\\1\\2', $query); |
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
// Cleanup from last time, set some logging parameters |
1280
|
|
|
$this->flush(); |
1281
|
|
|
$this->lastQuery = $query; |
1282
|
|
|
$this->lastQueryHash = md5($query . $start); |
|
|
|
|
1283
|
|
|
$this->lastQueryValues = null; |
1284
|
|
|
$this->lastQuerySafe = false; |
1285
|
|
|
$query_type = $this->determineQueryType($query); |
|
|
|
|
1286
|
|
|
|
1287
|
|
|
// TYPE is deprecated since MySQL 4.0.18, ENGINE is preferred |
1288
|
|
|
if ($query_type == self::__WRITE_OPERATION__) { |
|
|
|
|
1289
|
|
|
$query = preg_replace('/MyISAM/i', 'InnoDB', $query); |
1290
|
|
|
$query = preg_replace('/TYPE=InnoDB/i', 'ENGINE=InnoDB', $query); |
1291
|
|
|
} elseif ($query_type == self::__READ_OPERATION__) { |
|
|
|
|
1292
|
|
|
if (!preg_match('/^\s*SELECT\s+SQL(_NO)?_CACHE/i', $query)) { |
1293
|
|
|
if ($this->isCachingEnabled()) { |
|
|
|
|
1294
|
|
|
$query = preg_replace('/^SELECT\s+/i', 'SELECT SQL_CACHE ', $query); |
1295
|
|
|
} else { |
1296
|
|
|
$query = preg_replace('/^SELECT\s+/i', 'SELECT SQL_NO_CACHE ', $query); |
1297
|
|
|
} |
1298
|
|
|
} |
1299
|
|
|
$fetchType = $type == "OBJECT" ? PDO::FETCH_OBJ : PDO::FETCH_ASSOC; |
1300
|
|
|
} |
1301
|
|
|
|
1302
|
|
|
try { |
1303
|
|
|
$resultPdo = $this->conn->query($query); |
1304
|
|
|
// Execute it |
1305
|
|
|
if ($fetchType) { |
|
|
|
|
1306
|
|
|
$result = $resultPdo->fetchAll($fetchType); |
1307
|
|
|
} else { |
1308
|
|
|
$result = $resultPdo->execute(); |
1309
|
|
|
} |
1310
|
|
|
$resultPdo->closeCursor(); |
1311
|
|
|
$this->queryCount++; |
1312
|
|
|
$this->_lastResult = $result; |
|
|
|
|
1313
|
|
|
} catch (PDOException $ex) { |
1314
|
|
|
$this->throwDatabaseError($ex); |
1315
|
|
|
return; |
1316
|
|
|
} |
1317
|
|
|
|
1318
|
|
|
// Check for errors |
1319
|
|
|
if ($this->conn->errorCode() !== PDO::ERR_NONE) { |
1320
|
|
|
$this->throwDatabaseError(); |
1321
|
|
|
return; |
1322
|
|
|
} |
1323
|
|
|
|
1324
|
|
|
// Log the query |
1325
|
|
|
if ($this->isLoggingEnabled()) { |
1326
|
|
|
$this->logLastQuery(precision_timer('stop', $start)); |
1327
|
|
|
} |
1328
|
|
|
|
1329
|
|
|
return $result !== false; |
1330
|
|
|
} |
1331
|
|
|
|
1332
|
|
|
/** |
1333
|
|
|
* A convenience method to insert data into the Database. This function |
1334
|
|
|
* takes an associative array of data to input, with the keys being the column |
1335
|
|
|
* names and the table. An optional parameter exposes MySQL's ON DUPLICATE |
1336
|
|
|
* KEY UPDATE functionality, which will update the values if a duplicate key |
1337
|
|
|
* is found. |
1338
|
|
|
* |
1339
|
|
|
* @deprecated @since Symphony 3.0.0 |
1340
|
|
|
* @param array $fields |
1341
|
|
|
* An associative array of data to input, with the key's mapping to the |
1342
|
|
|
* column names. Alternatively, an array of associative array's can be |
1343
|
|
|
* provided, which will perform multiple inserts |
1344
|
|
|
* @param string $table |
1345
|
|
|
* The table name, including the tbl prefix which will be changed |
1346
|
|
|
* to this Symphony's table prefix in the query function |
1347
|
|
|
* @param boolean $updateOnDuplicate |
1348
|
|
|
* If set to true, data will updated if any key constraints are found that cause |
1349
|
|
|
* conflicts. By default this is set to false, which will not update the data and |
1350
|
|
|
* would return an SQL error |
1351
|
|
|
* @throws DatabaseException |
1352
|
|
|
* @return boolean |
1353
|
|
|
*/ |
1354
|
|
|
public function _insert(array $fields, $table, $updateOnDuplicate = false) // @codingStandardsIgnoreLine |
1355
|
|
|
{ |
1356
|
|
|
$success = true; |
1357
|
|
|
// Multiple inserts |
1358
|
|
|
if (!is_array(current($fields))) { |
1359
|
|
|
$fields = [$fields]; |
1360
|
|
|
} |
1361
|
|
|
foreach ($fields as $field) { |
1362
|
|
|
$stm = $this->insert($table)->values($field); |
|
|
|
|
1363
|
|
|
if ($updateOnDuplicate) { |
1364
|
|
|
$stm->updateOnDuplicateKey(); |
1365
|
|
|
} |
1366
|
|
|
$success = $success && $stm->execute()->success(); |
1367
|
|
|
} |
1368
|
|
|
return $success; |
1369
|
|
|
} |
1370
|
|
|
|
1371
|
|
|
/** |
1372
|
|
|
* A convenience method to update data that exists in the Database. This function |
1373
|
|
|
* takes an associative array of data to input, with the keys being the column |
1374
|
|
|
* names and the table. A WHERE statement can be provided to select the rows |
1375
|
|
|
* to update |
1376
|
|
|
* |
1377
|
|
|
* @deprecated @since Symphony 3.0.0 |
1378
|
|
|
* @param array $fields |
1379
|
|
|
* An associative array of data to input, with the key's mapping to the |
1380
|
|
|
* column names. |
1381
|
|
|
* @param string $table |
1382
|
|
|
* The table name, including the tbl prefix which will be changed |
1383
|
|
|
* to this Symphony's table prefix in the query function |
1384
|
|
|
* @param string $where |
1385
|
|
|
* A WHERE statement for this UPDATE statement, defaults to null |
1386
|
|
|
* which will update all rows in the $table |
1387
|
|
|
* @throws DatabaseException |
1388
|
|
|
* @return boolean |
1389
|
|
|
*/ |
1390
|
|
|
public function _update(array $fields, $table, $where = null) // @codingStandardsIgnoreLine |
1391
|
|
|
{ |
1392
|
|
|
$stm = $this->update($table)->set($fields); |
|
|
|
|
1393
|
|
|
if ($where) { |
1394
|
|
|
$stm->unsafeAppendSQLPart('where', "WHERE $where"); |
1395
|
|
|
} |
1396
|
|
|
return $stm->execute()->success(); |
1397
|
|
|
} |
1398
|
|
|
|
1399
|
|
|
/** |
1400
|
|
|
* Returns an associative array that contains the results of the |
1401
|
|
|
* given `$query`. Optionally, the resulting array can be indexed |
1402
|
|
|
* by a particular column. |
1403
|
|
|
* |
1404
|
|
|
* @deprecated @since Symphony 3.0.0 |
1405
|
|
|
* @param string $query |
1406
|
|
|
* The full SQL query to execute. Defaults to null, which will |
1407
|
|
|
* use the _lastResult |
1408
|
|
|
* @param string $index_by_column |
1409
|
|
|
* The name of a column in the table to use it's value to index |
1410
|
|
|
* the result by. If this is omitted (and it is by default), an |
1411
|
|
|
* array of associative arrays is returned, with the key being the |
1412
|
|
|
* column names |
1413
|
|
|
* @throws DatabaseException |
1414
|
|
|
* @return array |
1415
|
|
|
* An associative array with the column names as the keys |
1416
|
|
|
*/ |
1417
|
|
|
public function fetch($query = null, $index_by_column = null) |
1418
|
|
|
{ |
1419
|
|
|
if (!is_null($query)) { |
1420
|
|
|
$this->query($query, "ASSOC"); |
|
|
|
|
1421
|
|
|
} elseif (is_null($this->_lastResult)) { |
1422
|
|
|
return array(); |
1423
|
|
|
} |
1424
|
|
|
|
1425
|
|
|
$result = $this->_lastResult; |
1426
|
|
|
|
1427
|
|
|
if (!is_null($index_by_column) && isset($result[0][$index_by_column])) { |
1428
|
|
|
$n = array(); |
1429
|
|
|
|
1430
|
|
|
foreach ($result as $ii) { |
1431
|
|
|
$n[$ii[$index_by_column]] = $ii; |
1432
|
|
|
} |
1433
|
|
|
|
1434
|
|
|
$result = $n; |
1435
|
|
|
} |
1436
|
|
|
|
1437
|
|
|
return $result; |
1438
|
|
|
} |
1439
|
|
|
|
1440
|
|
|
/** |
1441
|
|
|
* Returns the row at the specified index from the given query. If no |
1442
|
|
|
* query is given, it will use the `$this->_lastResult`. If no offset is provided, |
1443
|
|
|
* the function will return the first row. This function does not imply any |
1444
|
|
|
* LIMIT to the given `$query`, so for the more efficient use, it is recommended |
1445
|
|
|
* that the `$query` have a LIMIT set. |
1446
|
|
|
* |
1447
|
|
|
* @deprecated @since Symphony 3.0.0 |
1448
|
|
|
* @param int $offset |
1449
|
|
|
* The row to return from the SQL query. For instance, if the second |
1450
|
|
|
* row from the result was required, the offset would be 1, because it |
1451
|
|
|
* is zero based. |
1452
|
|
|
* @param string $query |
1453
|
|
|
* The full SQL query to execute. Defaults to null, which will |
1454
|
|
|
* use the `$this->_lastResult` |
1455
|
|
|
* @throws DatabaseException |
1456
|
|
|
* @return array |
1457
|
|
|
* If there is no row at the specified `$offset`, an empty array will be returned |
1458
|
|
|
* otherwise an associative array of that row will be returned. |
1459
|
|
|
*/ |
1460
|
|
|
public function fetchRow($offset = 0, $query = null) |
1461
|
|
|
{ |
1462
|
|
|
$result = $this->fetch($query); |
|
|
|
|
1463
|
|
|
return (empty($result) ? array() : $result[$offset]); |
1464
|
|
|
} |
1465
|
|
|
|
1466
|
|
|
/** |
1467
|
|
|
* Returns an array of values for a specified column in a given query. |
1468
|
|
|
* If no query is given, it will use the `$this->_lastResult`. |
1469
|
|
|
* |
1470
|
|
|
* @deprecated @since Symphony 3.0.0 |
1471
|
|
|
* @param string $column |
1472
|
|
|
* The column name in the query to return the values for |
1473
|
|
|
* @param string $query |
1474
|
|
|
* The full SQL query to execute. Defaults to null, which will |
1475
|
|
|
* use the `$this->_lastResult` |
1476
|
|
|
* @throws DatabaseException |
1477
|
|
|
* @return array |
1478
|
|
|
* If there is no results for the `$query`, an empty array will be returned |
1479
|
|
|
* otherwise an array of values for that given `$column` will be returned |
1480
|
|
|
*/ |
1481
|
|
|
public function fetchCol($column, $query = null) |
1482
|
|
|
{ |
1483
|
|
|
$result = $this->fetch($query); |
|
|
|
|
1484
|
|
|
|
1485
|
|
|
if (empty($result)) { |
1486
|
|
|
return array(); |
1487
|
|
|
} |
1488
|
|
|
|
1489
|
|
|
$rows = array(); |
1490
|
|
|
foreach ($result as $row) { |
1491
|
|
|
$rows[] = $row[$column]; |
1492
|
|
|
} |
1493
|
|
|
|
1494
|
|
|
return $rows; |
1495
|
|
|
} |
1496
|
|
|
|
1497
|
|
|
/** |
1498
|
|
|
* Returns the value for a specified column at a specified offset. If no |
1499
|
|
|
* offset is provided, it will return the value for column of the first row. |
1500
|
|
|
* If no query is given, it will use the `$this->_lastResult`. |
1501
|
|
|
* |
1502
|
|
|
* @deprecated @since Symphony 3.0.0 |
1503
|
|
|
* @see select |
1504
|
|
|
* @param string $column |
1505
|
|
|
* The column name in the query to return the values for |
1506
|
|
|
* @param int $offset |
1507
|
|
|
* The row to use to return the value for the given `$column` from the SQL |
1508
|
|
|
* query. For instance, if `$column` form the second row was required, the |
1509
|
|
|
* offset would be 1, because it is zero based. |
1510
|
|
|
* @param string $query |
1511
|
|
|
* The full SQL query to execute. Defaults to null, which will |
1512
|
|
|
* use the `$this->_lastResult` |
1513
|
|
|
* @throws DatabaseException |
1514
|
|
|
* @return string|null |
1515
|
|
|
* Returns the value of the given column, if it doesn't exist, null will be |
1516
|
|
|
* returned |
1517
|
|
|
*/ |
1518
|
|
|
public function fetchVar($column, $offset = 0, $query = null) |
1519
|
|
|
{ |
1520
|
|
|
$result = $this->fetch($query); |
|
|
|
|
1521
|
|
|
return (empty($result) ? null : $result[$offset][$column]); |
1522
|
|
|
} |
1523
|
|
|
} |
1524
|
|
|
|
1525
|
|
|
/** |
1526
|
|
|
* Compat layer: Recreate the old MySQL class by extending our new Database class |
1527
|
|
|
*/ |
1528
|
|
|
class MySQL extends Database |
1529
|
|
|
{ |
1530
|
|
|
public function cleanValue($value) |
1531
|
|
|
{ |
1532
|
|
|
return (new Database())->cleanValue($value); |
|
|
|
|
1533
|
|
|
} |
1534
|
|
|
} |
1535
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.