Elgg  Version 6.1
Database.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
18 
26 class Database {
27 
28  use Profilable;
29  use Loggable;
30 
31  const DELAYED_QUERY = 'q';
32  const DELAYED_HANDLER = 'h';
33 
37  protected $table_prefix;
38 
42  protected array $connections = [];
43 
47  protected int $query_count = 0;
48 
56  protected array $delayed_queries = [];
57 
61  protected $db_config;
62 
70  public function __construct(DbConfig $db_config, protected QueryCache $query_cache, protected Config $config) {
71  $this->resetConnections($db_config);
72  }
73 
81  public function resetConnections(DbConfig $config): void {
82  $this->closeConnections();
83 
84  $this->db_config = $config;
85  $this->table_prefix = $config->getTablePrefix();
86  $this->query_cache->enable();
87  $this->query_cache->clear();
88  }
89 
98  public function closeConnections(): void {
99  foreach ($this->connections as $connection) {
100  $connection->close();
101  }
102 
103  $this->connections = [];
104  }
105 
113  public function getConnection(string $type): Connection {
114  if (isset($this->connections[$type])) {
115  return $this->connections[$type];
116  } else if (isset($this->connections['readwrite'])) {
117  return $this->connections['readwrite'];
118  }
119 
120  $this->setupConnections();
121 
122  return $this->getConnection($type);
123  }
124 
133  public function setupConnections(): void {
134  if ($this->db_config->isDatabaseSplit()) {
135  $this->connect('read');
136  $this->connect('write');
137  } else {
138  $this->connect('readwrite');
139  }
140  }
141 
152  public function connect(string $type = 'readwrite'): void {
153  $conf = $this->db_config->getConnectionConfig($type);
154 
155  $params = [
156  'dbname' => $conf['database'],
157  'user' => $conf['user'],
158  'password' => $conf['password'],
159  'host' => $conf['host'],
160  'port' => $conf['port'],
161  'charset' => $conf['encoding'],
162  'driver' => 'pdo_mysql',
163  ];
164 
165  try {
166  $this->connections[$type] = DriverManager::getConnection($params);
167 
168  // https://github.com/Elgg/Elgg/issues/8121
169  $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
170  $this->connections[$type]->executeStatement("SET SESSION sql_mode=($sub_query);");
171  } catch (\Exception $e) {
172  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
173  $this->log(LogLevel::ERROR, $e);
174 
175  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
176  $msg = "Elgg couldn't select the database '{$conf['database']}'. Please check that the database is created and you have access to it.";
177  } else {
178  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
179  }
180 
181  throw new DatabaseException($msg);
182  }
183  }
184 
200  public function getData(QueryBuilder $query, $callback = null) {
201  return $this->getResults($query, $callback, false);
202  }
203 
216  public function getDataRow(QueryBuilder $query, $callback = null) {
217  return $this->getResults($query, $callback, true);
218  }
219 
229  public function insertData(QueryBuilder $query): int {
230 
231  $params = $query->getParameters();
232  $sql = $query->getSQL();
233 
234  $this->getLogger()->info("DB insert query {$sql} (params: " . print_r($params, true) . ')');
235 
236  $this->query_cache->clear();
237 
238  $this->executeQuery($query);
239 
240  try {
241  return (int) $query->getConnection()->lastInsertId();
242  } catch (DriverException $e) {
243  if ($e->getPrevious() instanceof NoIdentityValue) {
244  return 0;
245  }
246 
247  throw $e;
248  }
249  }
250 
261  public function updateData(QueryBuilder $query, bool $get_num_rows = false): bool|int {
262  $params = $query->getParameters();
263  $sql = $query->getSQL();
264 
265  $this->getLogger()->info("DB update query {$sql} (params: " . print_r($params, true) . ')');
266 
267  $this->query_cache->clear();
268 
269  $result = $this->executeQuery($query);
270  if (!$get_num_rows) {
271  return true;
272  }
273 
274  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
275  }
276 
286  public function deleteData(QueryBuilder $query): int {
287  $params = $query->getParameters();
288  $sql = $query->getSQL();
289 
290  $this->getLogger()->info("DB delete query {$sql} (params: " . print_r($params, true) . ')');
291 
292  $this->query_cache->clear();
293 
294  $result = $this->executeQuery($query);
295  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
296  }
297 
309  protected function fingerprintCallback($callback): string {
310  if (is_string($callback)) {
311  return $callback;
312  }
313 
314  if (is_object($callback)) {
315  return spl_object_hash($callback) . '::__invoke';
316  }
317 
318  if (is_array($callback)) {
319  if (is_string($callback[0])) {
320  return "{$callback[0]}::{$callback[1]}";
321  }
322 
323  return spl_object_hash($callback[0]) . "::{$callback[1]}";
324  }
325 
326  // this should not happen
327  return '';
328  }
329 
342  protected function getResults(QueryBuilder $query, $callback = null, bool $single = false) {
343  $params = $query->getParameters();
344  $sql = $query->getSQL();
345 
346  // Since we want to cache results of running the callback, we need to
347  // namespace the query with the callback and single result request.
348  // https://github.com/elgg/elgg/issues/4049
349  $extras = (int) $single . '|';
350  if ($callback) {
351  if (!is_callable($callback)) {
352  throw new RuntimeException('$callback must be a callable function. Given ' . _elgg_services()->handlers->describeCallable($callback));
353  }
354 
355  $extras .= $this->fingerprintCallback($callback);
356  }
357 
358  $hash = $this->getCacheHash($sql, $params, $extras);
359 
360  $cached_results = $this->query_cache->load($hash);
361  if (isset($cached_results)) {
362  return $cached_results;
363  }
364 
365  $this->getLogger()->info("DB select query {$sql} (params: " . print_r($params, true) . ')');
366 
367  $return = [];
368 
369  $stmt = $this->executeQuery($query);
370 
371  while ($row = $stmt->fetchAssociative()) {
372  $row_obj = (object) $row;
373  if ($callback) {
374  $row_obj = call_user_func($callback, $row_obj);
375  }
376 
377  if ($single) {
378  $return = $row_obj;
379  break;
380  } else {
381  $return[] = $row_obj;
382  }
383  }
384 
385  $this->query_cache->save($hash, $return);
386 
387  return $return;
388  }
389 
401  protected function executeQuery(QueryBuilder $query) {
402 
403  try {
404  $result = $this->trackQuery($query, function() use ($query) {
405  if ($query instanceof \Elgg\Database\Select) {
406  return $query->executeQuery();
407  } else {
408  return $query->executeStatement();
409  }
410  });
411  } catch (\Exception $e) {
412  $ex = new DatabaseException($e->getMessage(), 0, $e);
413  $ex->setParameters($query->getParameters());
414  $ex->setQuery($query->getSQL());
415 
416  throw $ex;
417  }
418 
419  return $result;
420  }
421 
432  protected function getCacheHash(string $sql, array $params = [], string $extras = ''): string {
433  $query_id = $sql . '|';
434  if (!empty($params)) {
435  $query_id .= serialize($params) . '|';
436  }
437 
438  $query_id .= $extras;
439 
440  // MD5 yields smaller mem usage for cache
441  return md5($query_id);
442  }
443 
452  public function trackQuery(QueryBuilder $query, callable $callback) {
453 
454  $params = $query->getParameters();
455  $sql = $query->getSQL();
456 
457  $this->query_count++;
458 
459  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
460  $this->beginTimer(['SQL', $timer_key]);
461 
462  $stop_timer = function() use ($timer_key) {
463  $this->endTimer(['SQL', $timer_key]);
464  };
465 
466  try {
467  $result = $callback();
468  } catch (\Exception $e) {
469  $stop_timer();
470 
471  throw $e;
472  }
473 
474  $stop_timer();
475 
476  return $result;
477  }
478 
490  public function registerDelayedQuery(QueryBuilder $query, $callback = null): void {
491  if (Application::isCli() && !$this->config->testing_mode) {
492  // during CLI execute delayed queries immediately (unless in testing mode, during PHPUnit)
493  // this should prevent OOM during long-running jobs
494  // @see Database::executeDelayedQueries()
495  try {
496  $stmt = $this->executeQuery($query);
497 
498  if (is_callable($callback)) {
499  call_user_func($callback, $stmt);
500  }
501  } catch (\Throwable $t) {
502  // Suppress all exceptions to not allow the application to crash
503  $this->getLogger()->error($t);
504  }
505 
506  return;
507  }
508 
509  $this->delayed_queries[] = [
510  self::DELAYED_QUERY => $query,
511  self::DELAYED_HANDLER => $callback,
512  ];
513  }
514 
521  public function executeDelayedQueries(): void {
522 
523  foreach ($this->delayed_queries as $set) {
524  $query = $set[self::DELAYED_QUERY];
525  $handler = $set[self::DELAYED_HANDLER];
526 
527  try {
528  $stmt = $this->executeQuery($query);
529 
530  if (is_callable($handler)) {
531  call_user_func($handler, $stmt);
532  }
533  } catch (\Throwable $t) {
534  // Suppress all exceptions since page already sent to requestor
535  $this->getLogger()->error($t);
536  }
537  }
538 
539  $this->delayed_queries = [];
540  }
541 
547  public function getQueryCount(): int {
548  return $this->query_count;
549  }
550 
558  public function getServerVersion(string $type = DbConfig::READ_WRITE): string {
559  return $this->getConnection($type)->getServerVersion();
560  }
561 
569  public function isMariaDB(string $type = DbConfig::READ_WRITE): bool {
570  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MariaDBPlatform;
571  }
572 
582  public function isMySQL(string $type = DbConfig::READ_WRITE): bool {
583  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySQLPlatform;
584  }
585 
594  public function __get($name) {
595  if ($name === 'prefix') {
596  return $this->table_prefix;
597  }
598 
599  throw new RuntimeException("Cannot read property '{$name}'");
600  }
601 
611  public function __set($name, $value): void {
612  throw new RuntimeException("Cannot write property '{$name}'");
613  }
614 }
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:547
$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:113
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
updateData(QueryBuilder $query, bool $get_num_rows=false)
Update the database.
Definition: Database.php:261
The Elgg database.
Definition: Database.php:26
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
Database abstraction query builder.
$type
Definition: delete.php:21
getConnection()
Returns the connection.
isMySQL(string $type=DbConfig::READ_WRITE)
Is the database MySQL.
Definition: Database.php:582
deleteData(QueryBuilder $query)
Delete data from the database.
Definition: Database.php:286
getData(QueryBuilder $query, $callback=null)
Retrieve rows from the database.
Definition: Database.php:200
insertData(QueryBuilder $query)
Insert a row into the database.
Definition: Database.php:229
resetConnections(DbConfig $config)
Reset the connections with new credentials.
Definition: Database.php:81
registerDelayedQuery(QueryBuilder $query, $callback=null)
Queue a query for execution upon shutdown.
Definition: Database.php:490
$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:611
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:401
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:452
__get($name)
Handle magic property reads.
Definition: Database.php:594
getTablePrefix()
Get the database table prefix.
Definition: DbConfig.php:99
isMariaDB(string $type=DbConfig::READ_WRITE)
Is the database MariaDB.
Definition: Database.php:569
Volatile cache for select queries.
Definition: QueryCache.php:12
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:342
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:309
log($level, $message, array $context=[])
Log a message.
Definition: Loggable.php:58
getLogger()
Returns logger.
Definition: Loggable.php:37
__construct(DbConfig $db_config, protected QueryCache $query_cache, protected Config $config)
Constructor.
Definition: Database.php:70
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:43
getDataRow(QueryBuilder $query, $callback=null)
Retrieve a single row from the database.
Definition: Database.php:216
$query
getCacheHash(string $sql, array $params=[], string $extras= '')
Returns a hashed key for storage in the cache.
Definition: Database.php:432
_elgg_services()
Get the global service provider.
Definition: elgglib.php:353
$handler
Definition: add.php:7
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:521
getServerVersion(string $type=DbConfig::READ_WRITE)
Get the server version number.
Definition: Database.php:558
closeConnections()
Close all database connections.
Definition: Database.php:98
setupConnections()
Establish database connections.
Definition: Database.php:133
connect(string $type= 'readwrite')
Establish a connection to the database server.
Definition: Database.php:152
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59