Elgg  Version 5.0
Database.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
17 
25 class Database {
26 
27  use Profilable;
28  use Loggable;
29 
30  const DELAYED_QUERY = 'q';
31  const DELAYED_HANDLER = 'h';
32 
36  private $table_prefix;
37 
41  private $connections = [];
42 
46  private $query_count = 0;
47 
53  protected $query_cache;
54 
62  protected $delayed_queries = [];
63 
67  private $config;
68 
75  public function __construct(DbConfig $config, QueryCache $query_cache) {
76  $this->query_cache = $query_cache;
77 
78  $this->resetConnections($config);
79  }
80 
88  public function resetConnections(DbConfig $config) {
89  $this->closeConnections();
90 
91  $this->config = $config;
92  $this->table_prefix = $config->getTablePrefix();
93  $this->query_cache->enable();
94  $this->query_cache->clear();
95  }
96 
105  public function closeConnections(): void {
106  foreach ($this->connections as $connection) {
107  $connection->close();
108  }
109 
110  $this->connections = [];
111  }
112 
120  public function getConnection(string $type): Connection {
121  if (isset($this->connections[$type])) {
122  return $this->connections[$type];
123  } else if (isset($this->connections['readwrite'])) {
124  return $this->connections['readwrite'];
125  }
126 
127  $this->setupConnections();
128 
129  return $this->getConnection($type);
130  }
131 
140  public function setupConnections(): void {
141  if ($this->config->isDatabaseSplit()) {
142  $this->connect('read');
143  $this->connect('write');
144  } else {
145  $this->connect('readwrite');
146  }
147  }
148 
159  public function connect(string $type = 'readwrite'): void {
160  $conf = $this->config->getConnectionConfig($type);
161 
162  $params = [
163  'dbname' => $conf['database'],
164  'user' => $conf['user'],
165  'password' => $conf['password'],
166  'host' => $conf['host'],
167  'port' => $conf['port'],
168  'charset' => $conf['encoding'],
169  'driver' => 'pdo_mysql',
170  ];
171 
172  try {
173  $this->connections[$type] = DriverManager::getConnection($params);
174 
175  // https://github.com/Elgg/Elgg/issues/8121
176  $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
177  $this->connections[$type]->executeStatement("SET SESSION sql_mode=($sub_query);");
178  } catch (\Exception $e) {
179  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
180  $this->log(LogLevel::ERROR, $e);
181 
182  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
183  $msg = "Elgg couldn't select the database '{$conf['database']}'. Please check that the database is created and you have access to it.";
184  } else {
185  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
186  }
187 
188  throw new DatabaseException($msg);
189  }
190  }
191 
207  public function getData(QueryBuilder $query, $callback = null) {
208  return $this->getResults($query, $callback, false);
209  }
210 
223  public function getDataRow(QueryBuilder $query, $callback = null) {
224  return $this->getResults($query, $callback, true);
225  }
226 
236  public function insertData(QueryBuilder $query): int {
237 
238  $params = $query->getParameters();
239  $sql = $query->getSQL();
240 
241  $this->getLogger()->info("DB insert query {$sql} (params: " . print_r($params, true) . ')');
242 
243  $this->query_cache->clear();
244 
245  $this->executeQuery($query);
246  return (int) $query->getConnection()->lastInsertId();
247  }
248 
259  public function updateData(QueryBuilder $query, bool $get_num_rows = false) {
260  $params = $query->getParameters();
261  $sql = $query->getSQL();
262 
263  $this->getLogger()->info("DB update query {$sql} (params: " . print_r($params, true) . ')');
264 
265  $this->query_cache->clear();
266 
267  $result = $this->executeQuery($query);
268  if (!$get_num_rows) {
269  return true;
270  }
271 
272  return ($result instanceof Result) ? $result->rowCount() : $result;
273  }
274 
284  public function deleteData(QueryBuilder $query): int {
285  $params = $query->getParameters();
286  $sql = $query->getSQL();
287 
288  $this->getLogger()->info("DB delete query {$sql} (params: " . print_r($params, true) . ')');
289 
290  $this->query_cache->clear();
291 
292  $result = $this->executeQuery($query);
293  return ($result instanceof Result) ? $result->rowCount() : $result;
294  }
295 
307  protected function fingerprintCallback($callback): string {
308  if (is_string($callback)) {
309  return $callback;
310  }
311 
312  if (is_object($callback)) {
313  return spl_object_hash($callback) . '::__invoke';
314  }
315 
316  if (is_array($callback)) {
317  if (is_string($callback[0])) {
318  return "{$callback[0]}::{$callback[1]}";
319  }
320 
321  return spl_object_hash($callback[0]) . "::{$callback[1]}";
322  }
323 
324  // this should not happen
325  return '';
326  }
327 
340  protected function getResults(QueryBuilder $query, $callback = null, bool $single = false) {
341  $params = $query->getParameters();
342  $sql = $query->getSQL();
343 
344  // Since we want to cache results of running the callback, we need to
345  // namespace the query with the callback and single result request.
346  // https://github.com/elgg/elgg/issues/4049
347  $extras = (int) $single . '|';
348  if ($callback) {
349  if (!is_callable($callback)) {
350  throw new RuntimeException('$callback must be a callable function. Given ' . _elgg_services()->handlers->describeCallable($callback));
351  }
352 
353  $extras .= $this->fingerprintCallback($callback);
354  }
355 
356  $hash = $this->query_cache->getHash($sql, $params, $extras);
357 
358  $cached_results = $this->query_cache->get($hash);
359  if (isset($cached_results)) {
360  return $cached_results;
361  }
362 
363  $this->getLogger()->info("DB select query {$sql} (params: " . print_r($params, true) . ')');
364 
365  $return = [];
366 
367  $stmt = $this->executeQuery($query);
368 
369  while ($row = $stmt->fetchAssociative()) {
370  $row_obj = (object) $row;
371  if ($callback) {
372  $row_obj = call_user_func($callback, $row_obj);
373  }
374 
375  if ($single) {
376  $return = $row_obj;
377  break;
378  } else {
379  $return[] = $row_obj;
380  }
381  }
382 
383  // Cache result
384  $this->query_cache->set($hash, $return);
385 
386  return $return;
387  }
388 
400  protected function executeQuery(QueryBuilder $query) {
401 
402  try {
403  $result = $this->trackQuery($query, function() use ($query) {
404  if ($query instanceof \Elgg\Database\Select) {
405  return $query->executeQuery();
406  } else {
407  return $query->executeStatement();
408  }
409  });
410  } catch (\Exception $e) {
411  $ex = new DatabaseException($e->getMessage(), 0, $e);
412  $ex->setParameters($query->getParameters());
413  $ex->setQuery($query->getSQL());
414 
415  throw $ex;
416  }
417 
418  return $result;
419  }
420 
429  public function trackQuery(QueryBuilder $query, callable $callback) {
430 
431  $params = $query->getParameters();
432  $sql = $query->getSQL();
433 
434  $this->query_count++;
435 
436  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
437  $this->beginTimer(['SQL', $timer_key]);
438 
439  $stop_timer = function() use ($timer_key) {
440  $this->endTimer(['SQL', $timer_key]);
441  };
442 
443  try {
444  $result = $callback();
445  } catch (\Exception $e) {
446  $stop_timer();
447 
448  throw $e;
449  }
450 
451  $stop_timer();
452 
453  return $result;
454  }
455 
467  public function registerDelayedQuery(QueryBuilder $query, $callback = null): void {
468  $this->delayed_queries[] = [
469  self::DELAYED_QUERY => $query,
470  self::DELAYED_HANDLER => $callback,
471  ];
472  }
473 
480  public function executeDelayedQueries(): void {
481 
482  foreach ($this->delayed_queries as $set) {
483  $query = $set[self::DELAYED_QUERY];
484  $handler = $set[self::DELAYED_HANDLER];
485 
486  try {
487  $stmt = $this->executeQuery($query);
488 
489  if (is_callable($handler)) {
490  call_user_func($handler, $stmt);
491  }
492  } catch (\Exception $e) {
493  // Suppress all exceptions since page already sent to requestor
494  $this->getLogger()->error($e);
495  }
496  }
497 
498  $this->delayed_queries = [];
499  }
500 
508  public function enableQueryCache(): void {
509  $this->query_cache->enable();
510  }
511 
520  public function disableQueryCache(): void {
521  $this->query_cache->disable();
522  }
523 
529  public function getQueryCount(): int {
530  return $this->query_count;
531  }
532 
540  public function getServerVersion(string $type = DbConfig::READ_WRITE): string {
541  $driver = $this->getConnection($type)->getWrappedConnection();
542  if ($driver instanceof ServerInfoAwareConnection) {
543  $version = $driver->getServerVersion();
544 
545  if ($this->isMariaDB($type)) {
546  if (str_starts_with($version, '5.5.5-')) {
547  $version = substr($version, 6);
548  }
549  }
550 
551  return $version;
552  }
553 
554  return '';
555  }
556 
564  public function isMariaDB(string $type = DbConfig::READ_WRITE): bool {
565  $driver = $this->getConnection($type)->getWrappedConnection();
566  if ($driver instanceof ServerInfoAwareConnection) {
567  $version = $driver->getServerVersion();
568 
569  return stristr($version, 'mariadb') !== false;
570  }
571 
572  return false;
573  }
574 
583  public function __get($name) {
584  if ($name === 'prefix') {
585  return $this->table_prefix;
586  }
587 
588  throw new RuntimeException("Cannot read property '{$name}'");
589  }
590 
600  public function __set($name, $value): void {
601  throw new RuntimeException("Cannot write property '{$name}'");
602  }
603 }
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:12
getQueryCount()
Get the number of queries made to the database.
Definition: Database.php:529
$params
Saves global plugin settings.
Definition: save.php:13
Exception thrown if an error which can only be found on runtime occurs.
Database configuration service.
Definition: DbConfig.php:13
getConnection(string $type)
Gets (if required, also creates) a DB connection.
Definition: Database.php:120
__construct(DbConfig $config, QueryCache $query_cache)
Constructor.
Definition: Database.php:75
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
updateData(QueryBuilder $query, bool $get_num_rows=false)
Update the database.
Definition: Database.php:259
The Elgg database.
Definition: Database.php:25
$version
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
$type
Definition: delete.php:22
deleteData(QueryBuilder $query)
Delete data from the database.
Definition: Database.php:284
getData(QueryBuilder $query, $callback=null)
Retrieve rows from the database.
Definition: Database.php:207
insertData(QueryBuilder $query)
Insert a row into the database.
Definition: Database.php:236
resetConnections(DbConfig $config)
Reset the connections with new credentials.
Definition: Database.php:88
registerDelayedQuery(QueryBuilder $query, $callback=null)
Queue a query for execution upon shutdown.
Definition: Database.php:467
$value
Definition: generic.php:51
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
setParameters(array $params)
Set query parameters.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
__set($name, $value)
Handle magic property writes.
Definition: Database.php:600
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:400
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:429
__get($name)
Handle magic property reads.
Definition: Database.php:583
getTablePrefix()
Get the database table prefix.
Definition: DbConfig.php:99
isMariaDB(string $type=DbConfig::READ_WRITE)
Is the database MariaDB.
Definition: Database.php:564
Volatile cache for select queries.
Definition: QueryCache.php:17
A generic parent class for database exceptions.
getResults(QueryBuilder $query, $callback=null, bool $single=false)
Handles queries that return results, running the results through a an optional callback function...
Definition: Database.php:340
Result of a single BatchUpgrade run.
Definition: Result.php:10
fingerprintCallback($callback)
Get a string that uniquely identifies a callback during the current request.
Definition: Database.php:307
log($level, $message, array $context=[])
Log a message.
Definition: Loggable.php:58
getLogger()
Returns logger.
Definition: Loggable.php:37
disableQueryCache()
Disable the query cache.
Definition: Database.php:520
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:46
getDataRow(QueryBuilder $query, $callback=null)
Retrieve a single row from the database.
Definition: Database.php:223
$query
enableQueryCache()
Enable the query cache.
Definition: Database.php:508
_elgg_services()
Get the global service provider.
Definition: elgglib.php:346
$handler
Definition: add.php:7
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:480
getServerVersion(string $type=DbConfig::READ_WRITE)
Get the server version number.
Definition: Database.php:540
closeConnections()
Close all database connections.
Definition: Database.php:105
setupConnections()
Establish database connections.
Definition: Database.php:140
connect(string $type= 'readwrite')
Establish a connection to the database server.
Definition: Database.php:159
Query builder for fetching data from the database.
Definition: Select.php:8
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:62