Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

Database   F

Complexity

Total Complexity 85

Size/Duplication

Total Lines 716
Duplicated Lines 0 %

Test Coverage

Coverage 86.34%

Importance

Changes 0
Metric Value
dl 0
loc 716
ccs 196
cts 227
cp 0.8634
rs 1.263
c 0
b 0
f 0
wmc 85

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A resetConnections() 0 5 1
A getConnection() 0 8 3
A getDataRow() 0 2 1
A getData() 0 2 1
A setupConnections() 0 6 2
A registerDelayedQuery() 0 13 3
F getResults() 0 72 15
C executeQuery() 0 35 7
A updateData() 0 18 4
B connect() 0 28 4
C runSqlScript() 0 34 7
A getQueryCount() 0 2 1
A __set() 0 2 1
A enableQueryCache() 0 4 3
A sanitizeString() 0 9 4
A __get() 0 6 2
A disableQueryCache() 0 2 1
A sanitizeInt() 0 10 3
A invalidateQueryCache() 0 5 3
A getServerVersion() 0 7 2
B executeDelayedQueries() 0 23 5
A insertData() 0 17 3
B fingerprintCallback() 0 15 5
A deleteData() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like Database often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Database, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Elgg;
3
4
use Doctrine\DBAL\DriverManager;
5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\Driver\Statement;
7
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
8
use Doctrine\DBAL\Query\QueryBuilder;
9
use Elgg\Database\DbConfig as DbConfig;
10
11
/**
12
 * The Elgg database
13
 *
14
 * @access private
15
 * @internal Use the public API functions in engine/lib/database.php
16
 *
17
 * @property-read string $prefix Elgg table prefix (read only)
18
 */
19
class Database {
20
	use Profilable;
21
	use Loggable;
22
23
	const DELAYED_QUERY = 'q';
24
	const DELAYED_TYPE = 't';
25
	const DELAYED_HANDLER = 'h';
26
	const DELAYED_PARAMS = 'p';
27
28
	/**
29
	 * @var string $table_prefix Prefix for database tables
30
	 */
31
	private $table_prefix;
32
33
	/**
34
	 * @var Connection[]
35
	 */
36
	private $connections = [];
37
38
	/**
39
	 * @var int $query_count The number of queries made
40
	 */
41
	private $query_count = 0;
42
43
	/**
44
	 * Query cache for select queries.
45
	 *
46
	 * Queries and their results are stored in this cache as:
47
	 * <code>
48
	 * $DB_QUERY_CACHE[query hash] => array(result1, result2, ... resultN)
49
	 * </code>
50
	 * @see \Elgg\Database::getResults() for details on the hash.
51
	 *
52
	 * @var \Elgg\Cache\LRUCache $query_cache The cache
53
	 */
54
	private $query_cache = null;
55
56
	/**
57
	 * @var int $query_cache_size The number of queries to cache
58
	 */
59
	private $query_cache_size = 50;
60
61
	/**
62
	 * Queries are saved as an array with the DELAYED_* constants as keys.
63
	 *
64
	 * @see registerDelayedQuery()
65
	 *
66
	 * @var array $delayed_queries Queries to be run during shutdown
67
	 */
68
	private $delayed_queries = [];
69
70
	/**
71
	 * @var \Elgg\Database\DbConfig $config Database configuration
72
	 */
73
	private $config;
74
75
	/**
76
	 * Constructor
77
	 *
78
	 * @param DbConfig $config DB configuration
79
	 */
80 4417
	public function __construct(DbConfig $config) {
81 4417
		$this->resetConnections($config);
82 4417
	}
83
84
	/**
85
	 * Reset the connections with new credentials
86
	 *
87
	 * @param DbConfig $config DB config
88
	 *
89
	 * @return void
90
	 */
91 4417
	public function resetConnections(DbConfig $config) {
92 4417
		$this->connections = [];
93 4417
		$this->config = $config;
94 4417
		$this->table_prefix = $config->getTablePrefix();
95 4417
		$this->enableQueryCache();
96 4417
	}
97
98
	/**
99
	 * Gets (if required, also creates) a DB connection.
100
	 *
101
	 * @param string $type The type of link we want: "read", "write" or "readwrite".
102
	 *
103
	 * @return Connection
104
	 * @throws \DatabaseException
105
	 * @access private
106
	 */
107 601
	public function getConnection($type) {
108 601
		if (isset($this->connections[$type])) {
109 18
			return $this->connections[$type];
110 601
		} else if (isset($this->connections['readwrite'])) {
111 601
			return $this->connections['readwrite'];
112
		} else {
113 18
			$this->setupConnections();
114 18
			return $this->getConnection($type);
115
		}
116
	}
117
118
	/**
119
	 * Establish database connections
120
	 *
121
	 * If the configuration has been set up for multiple read/write databases, set those
122
	 * links up separately; otherwise just create the one database link.
123
	 *
124
	 * @return void
125
	 * @throws \DatabaseException
126
	 * @access private
127
	 */
128 18
	public function setupConnections() {
129 18
		if ($this->config->isDatabaseSplit()) {
130
			$this->connect('read');
131
			$this->connect('write');
132
		} else {
133 18
			$this->connect('readwrite');
134
		}
135 18
	}
136
137
	/**
138
	 * Establish a connection to the database server
139
	 *
140
	 * Connect to the database server and use the Elgg database for a particular database link
141
	 *
142
	 * @param string $type The type of database connection. "read", "write", or "readwrite".
143
	 *
144
	 * @return void
145
	 * @throws \DatabaseException
146
	 * @access private
147
	 */
148 18
	public function connect($type = "readwrite") {
149 18
		$conf = $this->config->getConnectionConfig($type);
150
151
		$params = [
152 18
			'dbname' => $conf['database'],
153 18
			'user' => $conf['user'],
154 18
			'password' => $conf['password'],
155 18
			'host' => $conf['host'],
156 18
			'charset' => $conf['encoding'],
157 18
			'driver' => 'pdo_mysql',
158
		];
159
160
		try {
161 18
			$this->connections[$type] = DriverManager::getConnection($params);
162 18
			$this->connections[$type]->setFetchMode(\PDO::FETCH_OBJ);
163
164
			// https://github.com/Elgg/Elgg/issues/8121
165 18
			$sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
166 18
			$this->connections[$type]->exec("SET SESSION sql_mode=($sub_query);");
167
		} catch (\Exception $e) {
168
			// http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
169
			if ($e->getCode() == 1102 || $e->getCode() == 1049) {
170
				$msg = "Elgg couldn't select the database '{$conf['database']}'. "
171
					. "Please check that the database is created and you have access to it.";
172
			} else {
173
				$msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
174
			}
175
			throw new \DatabaseException($msg);
176
		}
177 18
	}
178
179
	/**
180
	 * Retrieve rows from the database.
181
	 *
182
	 * Queries are executed with {@link \Elgg\Database::executeQuery()} and results
183
	 * are retrieved with {@link \PDO::fetchObject()}.  If a callback
184
	 * function $callback is defined, each row will be passed as a single
185
	 * argument to $callback.  If no callback function is defined, the
186
	 * entire result set is returned as an array.
187
	 *
188
	 * @param QueryBuilder|string $query    The query being passed.
189
	 * @param callable            $callback Optionally, the name of a function to call back to on each row
190
	 * @param array               $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
191
	 *
192
	 * @return array An array of database result objects or callback function results. If the query
193
	 *               returned nothing, an empty array.
194
	 * @throws \DatabaseException
195
	 */
196 1211
	public function getData($query, $callback = null, array $params = []) {
197 1211
		return $this->getResults($query, $callback, false, $params);
0 ignored issues
show
Bug introduced by
It seems like $callback can also be of type callable; however, parameter $callback of Elgg\Database::getResults() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

197
		return $this->getResults($query, /** @scrutinizer ignore-type */ $callback, false, $params);
Loading history...
198
	}
199
200
	/**
201
	 * Retrieve a single row from the database.
202
	 *
203
	 * Similar to {@link \Elgg\Database::getData()} but returns only the first row
204
	 * matched.  If a callback function $callback is specified, the row will be passed
205
	 * as the only argument to $callback.
206
	 *
207
	 * @param QueryBuilder|string $query    The query to execute.
208
	 * @param callable            $callback A callback function to apply to the row
209
	 * @param array               $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
210
	 *
211
	 * @return mixed A single database result object or the result of the callback function.
212
	 * @throws \DatabaseException
213
	 */
214 682
	public function getDataRow($query, $callback = null, array $params = []) {
215 682
		return $this->getResults($query, $callback, true, $params);
0 ignored issues
show
Bug introduced by
It seems like $callback can also be of type callable; however, parameter $callback of Elgg\Database::getResults() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

215
		return $this->getResults($query, /** @scrutinizer ignore-type */ $callback, true, $params);
Loading history...
216
	}
217
218
	/**
219
	 * Insert a row into the database.
220
	 *
221
	 * @note Altering the DB invalidates all queries in the query cache.
222
	 *
223
	 * @param QueryBuilder|string $query  The query to execute.
224
	 * @param array               $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
225
	 *
226
	 * @return int|false The database id of the inserted row if a AUTO_INCREMENT field is
227
	 *                   defined, 0 if not, and false on failure.
228
	 * @throws \DatabaseException
229
	 */
230 1077
	public function insertData($query, array $params = []) {
231
232 1077
		if ($query instanceof QueryBuilder) {
233 1044
			$params = $query->getParameters();
234 1044
			$query = $query->getSQL();
235
		}
236
237 1077
		if ($this->logger) {
238 1077
			$this->logger->info("DB insert query $query (params: " . print_r($params, true) . ")");
239
		}
240
241 1077
		$connection = $this->getConnection('write');
242
243 1077
		$this->invalidateQueryCache();
244
245 1077
		$this->executeQuery($query, $connection, $params);
246 1076
		return (int) $connection->lastInsertId();
247
	}
248
249
	/**
250
	 * Update the database.
251
	 *
252
	 * @note Altering the DB invalidates all queries in the query cache.
253
	 *
254
	 * @note WARNING! update_data() has the 2nd and 3rd arguments reversed.
255
	 *
256
	 * @param QueryBuilder|string $query        The query to run.
257
	 * @param bool                $get_num_rows Return the number of rows affected (default: false).
258
	 * @param array               $params       Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
259
	 *
260
	 * @return bool|int
261
	 * @throws \DatabaseException
262
	 */
263 316
	public function updateData($query, $get_num_rows = false, array $params = []) {
264
265 316
		if ($query instanceof QueryBuilder) {
266 212
			$params = $query->getParameters();
267 212
			$query = $query->getSQL();
268
		}
269
270 316
		if ($this->logger) {
271 316
			$this->logger->info("DB update query $query (params: " . print_r($params, true) . ")");
272
		}
273
274 316
		$this->invalidateQueryCache();
275
276 316
		$stmt = $this->executeQuery($query, $this->getConnection('write'), $params);
277 315
		if ($get_num_rows) {
278 128
			return $stmt->rowCount();
279
		} else {
280 228
			return true;
281
		}
282
	}
283
284
	/**
285
	 * Delete data from the database
286
	 *
287
	 * @note Altering the DB invalidates all queries in query cache.
288
	 *
289
	 * @param QueryBuilder|string $query  The SQL query to run
290
	 * @param array               $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
291
	 *
292
	 * @return int The number of affected rows
293
	 * @throws \DatabaseException
294
	 */
295 245
	public function deleteData($query, array $params = []) {
296
297 245
		if ($query instanceof QueryBuilder) {
298 228
			$params = $query->getParameters();
299 228
			$query = $query->getSQL();
300
		}
301
302 245
		if ($this->logger) {
303 245
			$this->logger->info("DB delete query $query (params: " . print_r($params, true) . ")");
304
		}
305
306 245
		$connection = $this->getConnection('write');
307
308 245
		$this->invalidateQueryCache();
309
310 245
		$stmt = $this->executeQuery("$query", $connection, $params);
311 244
		return (int) $stmt->rowCount();
312
	}
313
314
	/**
315
	 * Get a string that uniquely identifies a callback during the current request.
316
	 *
317
	 * This is used to cache queries whose results were transformed by the callback. If the callback involves
318
	 * object method calls of the same class, different instances will return different values.
319
	 *
320
	 * @param callable $callback The callable value to fingerprint
321
	 *
322
	 * @return string A string that is unique for each callable passed in
323
	 * @since 1.9.4
324
	 * @access private
325
	 */
326 1154
	protected function fingerprintCallback($callback) {
327 1154
		if (is_string($callback)) {
328 1151
			return $callback;
329
		}
330 1058
		if (is_object($callback)) {
331 1037
			return spl_object_hash($callback) . "::__invoke";
332
		}
333 220
		if (is_array($callback)) {
334 220
			if (is_string($callback[0])) {
335 1
				return "{$callback[0]}::{$callback[1]}";
336
			}
337 220
			return spl_object_hash($callback[0]) . "::{$callback[1]}";
338
		}
339
		// this should not happen
340
		return "";
341
	}
342
343
	/**
344
	 * Handles queries that return results, running the results through a
345
	 * an optional callback function. This is for R queries (from CRUD).
346
	 *
347
	 * @param QueryBuilder|string $query    The select query to execute
348
	 * @param string              $callback An optional callback function to run on each row
349
	 * @param bool                $single   Return only a single result?
350
	 * @param array               $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
351
	 *
352
	 * @return array An array of database result objects or callback function results. If the query
353
	 *               returned nothing, an empty array.
354
	 * @throws \DatabaseException
355
	 */
356 1276
	protected function getResults($query, $callback = null, $single = false, array $params = []) {
357
358 1276
		if ($query instanceof QueryBuilder) {
359 1242
			$params = $query->getParameters();
360 1242
			$sql = $query->getSQL();
361
		} else {
362 383
			$sql = $query;
363
		}
364
365
		// Since we want to cache results of running the callback, we need to
366
		// namespace the query with the callback and single result request.
367
		// https://github.com/elgg/elgg/issues/4049
368 1276
		$query_id = (int) $single . $sql . '|';
369 1276
		if ($params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
370 1272
			$query_id .= serialize($params) . '|';
371
		}
372
373 1276
		if ($callback) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $callback of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
374 1154
			if (!is_callable($callback)) {
375 1
				throw new \RuntimeException('$callback must be a callable function. Given '
376 1
											. _elgg_services()->handlers->describeCallable($callback));
377
			}
378 1153
			$query_id .= $this->fingerprintCallback($callback);
379
		}
380
381 1275
		if ($this->logger) {
382 1274
			$this->logger->info("DB select query $sql (params: " . print_r($params, true) . ")");
383
		}
384
385
		// MD5 yields smaller mem usage for cache and cleaner logs
386 1275
		$hash = md5($query_id);
387
388
		// Is cached?
389 1275
		if ($this->query_cache) {
390 1275
			if (isset($this->query_cache[$hash])) {
391 620
				if ($this->logger) {
392 620
					$this->logger->info("DB query results returned from cache (hash: $hash)");
393
				}
394 620
				return $this->query_cache[$hash];
395
			}
396
		}
397
398 1210
		$return = [];
399
400 1210
		if ($query instanceof QueryBuilder) {
401 1197
			$stmt = $query->execute();
402
		} else {
403 361
			$stmt = $this->executeQuery($sql, $this->getConnection('read'), $params);
404
		}
405
406 1210
		while ($row = $stmt->fetch()) {
407 649
			if ($callback) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $callback of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
408 450
				$row = call_user_func($callback, $row);
409
			}
410
411 649
			if ($single) {
412 573
				$return = $row;
413 573
				break;
414
			} else {
415 588
				$return[] = $row;
416
			}
417
		}
418
419
		// Cache result
420 1210
		if ($this->query_cache) {
421 1210
			$this->query_cache[$hash] = $return;
422 1210
			if ($this->logger) {
423 1209
				$this->logger->info("DB query results cached (hash: $hash)");
424
			}
425
		}
426
427 1210
		return $return;
428
	}
429
430
	/**
431
	 * Execute a query.
432
	 *
433
	 * $query is executed via {@link Connection::query}. If there is an SQL error,
434
	 * a {@link DatabaseException} is thrown.
435
	 *
436
	 * @param QueryBuilder|string $query      The query
437
	 * @param Connection          $connection The DB connection
438
	 * @param array               $params     Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
439
	 *
440
	 * @return Statement The result of the query
441
	 * @throws \DatabaseException
442
	 */
443 1102
	protected function executeQuery($query, Connection $connection, array $params = []) {
444 1102
		if ($query == null) {
445
			throw new \DatabaseException("Query cannot be null");
446
		}
447
448 1102
		if ($query instanceof QueryBuilder) {
449
			$params = $query->getParameters();
450
			$query = $query->getSQL();
451
		}
452
453 1102
		$this->query_count++;
454
455 1102
		if ($this->timer) {
456
			$timer_key = preg_replace('~\\s+~', ' ', trim($query . '|' . serialize($params)));
457
			$this->timer->begin(['SQL', $timer_key]);
458
		}
459
460
		try {
461 1102
			if ($params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
462 1092
				$value = $connection->executeQuery($query, $params);
463
			} else {
464
				// faster
465 1102
				$value = $connection->query($query);
466
			}
467 3
		} catch (\Exception $e) {
468 3
			throw new \DatabaseException($e->getMessage() . "\n\n"
469 3
			. "QUERY: $query \n\n"
470 3
			. "PARAMS: " . print_r($params, true));
471
		}
472
473 1099
		if ($this->timer) {
474
			$this->timer->end(['SQL', $timer_key]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $timer_key does not seem to be defined for all execution paths leading up to this point.
Loading history...
475
		}
476
477 1099
		return $value;
478
	}
479
480
	/**
481
	 * Runs a full database script from disk.
482
	 *
483
	 * The file specified should be a standard SQL file as created by
484
	 * mysqldump or similar.  Statements must be terminated with ;
485
	 * and a newline character (\n or \r\n).
486
	 *
487
	 * The special string 'prefix_' is replaced with the database prefix
488
	 * as defined in {@link $this->tablePrefix}.
489
	 *
490
	 * @warning Only single line comments are supported. A comment
491
	 * must start with '-- ' or '# ', where the comment sign is at the
492
	 * very beginning of each line.
493
	 *
494
	 * @warning Errors do not halt execution of the script.  If a line
495
	 * generates an error, the error message is saved and the
496
	 * next line is executed.  After the file is run, any errors
497
	 * are displayed as a {@link DatabaseException}
498
	 *
499
	 * @param string $scriptlocation The full path to the script
500
	 *
501
	 * @return void
502
	 * @throws \DatabaseException
503
	 * @access private
504
	 */
505 5
	public function runSqlScript($scriptlocation) {
506 5
		$script = file_get_contents($scriptlocation);
507 5
		if ($script) {
508 5
			$errors = [];
509
510
			// Remove MySQL '-- ' and '# ' style comments
511 5
			$script = preg_replace('/^(?:--|#) .*$/m', '', $script);
512
513
			// Statements must end with ; and a newline
514 5
			$sql_statements = preg_split('/;[\n\r]+/', "$script\n");
515
516 5
			foreach ($sql_statements as $statement) {
517 5
				$statement = trim($statement);
518 5
				$statement = str_replace("prefix_", $this->table_prefix, $statement);
519 5
				if (!empty($statement)) {
520
					try {
521 5
						$this->updateData($statement);
522
					} catch (\DatabaseException $e) {
523 5
						$errors[] = $e->getMessage();
524
					}
525
				}
526
			}
527 5
			if (!empty($errors)) {
528
				$errortxt = "";
529
				foreach ($errors as $error) {
530
					$errortxt .= " {$error};";
531
				}
532
533
				$msg = "There were a number of issues: " . $errortxt;
534 5
				throw new \DatabaseException($msg);
535
			}
536
		} else {
537
			$msg = "Elgg couldn't find the requested database script at " . $scriptlocation . ".";
538
			throw new \DatabaseException($msg);
539
		}
540 5
	}
541
542
	/**
543
	 * Queue a query for execution upon shutdown.
544
	 *
545
	 * You can specify a callback if you care about the result. This function will always
546
	 * be passed a \Doctrine\DBAL\Driver\Statement.
547
	 *
548
	 * @param string   $query    The query to execute
549
	 * @param string   $type     The query type ('read' or 'write')
550
	 * @param callable $callback A callback function to pass the results array to
551
	 * @param array    $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
552
	 *
553
	 * @return boolean Whether registering was successful.
554
	 * @access private
555
	 */
556 427
	public function registerDelayedQuery($query, $type, $callback = null, array $params = []) {
557 427
		if ($type != 'read' && $type != 'write') {
558
			return false;
559
		}
560
561 427
		$this->delayed_queries[] = [
562 427
			self::DELAYED_QUERY => $query,
563 427
			self::DELAYED_TYPE => $type,
564 427
			self::DELAYED_HANDLER => $callback,
565 427
			self::DELAYED_PARAMS => $params,
566
		];
567
568 427
		return true;
569
	}
570
571
	/**
572
	 * Trigger all queries that were registered as "delayed" queries. This is
573
	 * called by the system automatically on shutdown.
574
	 *
575
	 * @return void
576
	 * @access private
577
	 * @todo make protected once this class is part of public API
578
	 */
579 3
	public function executeDelayedQueries() {
580
581 3
		foreach ($this->delayed_queries as $set) {
582 3
			$query = $set[self::DELAYED_QUERY];
583 3
			$type = $set[self::DELAYED_TYPE];
584 3
			$handler = $set[self::DELAYED_HANDLER];
585 3
			$params = $set[self::DELAYED_PARAMS];
586
587
			try {
588 3
				$stmt = $this->executeQuery($query, $this->getConnection($type), $params);
589
590 3
				if (is_callable($handler)) {
591 3
					call_user_func($handler, $stmt);
592
				}
593
			} catch (\Exception $e) {
594
				if ($this->logger) {
595
					// Suppress all exceptions since page already sent to requestor
596 3
					$this->logger->error($e);
597
				}
598
			}
599
		}
600
601 3
		$this->delayed_queries = [];
602 3
	}
603
604
	/**
605
	 * Enable the query cache
606
	 *
607
	 * This does not take precedence over the \Elgg\Database\Config setting.
608
	 *
609
	 * @return void
610
	 * @access private
611
	 */
612 4665
	public function enableQueryCache() {
613 4665
		if ($this->config->isQueryCacheEnabled() && $this->query_cache === null) {
614
			// @todo if we keep this cache, expose the size as a config parameter
615 4665
			$this->query_cache = new \Elgg\Cache\LRUCache($this->query_cache_size);
616
		}
617 4665
	}
618
619
	/**
620
	 * Disable the query cache
621
	 *
622
	 * This is useful for special scripts that pull large amounts of data back
623
	 * in single queries.
624
	 *
625
	 * @return void
626
	 * @access private
627
	 */
628 259
	public function disableQueryCache() {
629 259
		$this->query_cache = null;
630 259
	}
631
632
	/**
633
	 * Invalidate the query cache
634
	 *
635
	 * @return void
636
	 */
637 1085
	protected function invalidateQueryCache() {
638 1085
		if ($this->query_cache) {
639 1085
			$this->query_cache->clear();
640 1085
			if ($this->logger) {
641 1085
				$this->logger->info("Query cache invalidated");
642
			}
643
		}
644 1085
	}
645
646
	/**
647
	 * Get the number of queries made to the database
648
	 *
649
	 * @return int
650
	 * @access private
651
	 */
652 1
	public function getQueryCount() {
653 1
		return $this->query_count;
654
	}
655
656
	/**
657
	 * Sanitizes an integer value for use in a query
658
	 *
659
	 * @param int  $value  Value to sanitize
660
	 * @param bool $signed Whether negative values are allowed (default: true)
661
	 * @return int
662
	 * @deprecated Use query parameters where possible
663
	 */
664 1
	public function sanitizeInt($value, $signed = true) {
665 1
		$value = (int) $value;
666
667 1
		if ($signed === false) {
668 1
			if ($value < 0) {
669
				$value = 0;
670
			}
671
		}
672
673 1
		return $value;
674
	}
675
676
	/**
677
	 * Sanitizes a string for use in a query
678
	 *
679
	 * @param string $value Value to escape
680
	 * @return string
681
	 * @throws \DatabaseException
682
	 * @deprecated Use query parameters where possible
683
	 */
684 15
	public function sanitizeString($value) {
685 15
		if (is_array($value)) {
686 1
			throw new \DatabaseException(__METHOD__ . '() and serialize_string() cannot accept arrays.');
687
		}
688 14
		$quoted = $this->getConnection('read')->quote($value);
689 14
		if ($quoted[0] !== "'" || substr($quoted, -1) !== "'") {
690
			throw new \DatabaseException("PDO::quote did not return surrounding single quotes.");
691
		}
692 14
		return substr($quoted, 1, -1);
693
	}
694
695
	/**
696
	 * Get the server version number
697
	 *
698
	 * @param string $type Connection type (Config constants, e.g. Config::READ_WRITE)
699
	 *
700
	 * @return string Empty if version cannot be determined
701
	 * @access private
702
	 */
703 5
	public function getServerVersion($type) {
704 5
		$driver = $this->getConnection($type)->getWrappedConnection();
705 5
		if ($driver instanceof ServerInfoAwareConnection) {
706 5
			return $driver->getServerVersion();
707
		}
708
709
		return null;
710
	}
711
712
	/**
713
	 * Handle magic property reads
714
	 *
715
	 * @param string $name Property name
716
	 * @return mixed
717
	 */
718 5233
	public function __get($name) {
719 5233
		if ($name === 'prefix') {
720 5233
			return $this->table_prefix;
721
		}
722
723
		throw new \RuntimeException("Cannot read property '$name'");
724
	}
725
726
	/**
727
	 * Handle magic property writes
728
	 *
729
	 * @param string $name  Property name
730
	 * @param mixed  $value Value
731
	 * @return void
732
	 */
733
	public function __set($name, $value) {
734
		throw new \RuntimeException("Cannot write property '$name'");
735
	}
736
}
737