Elgg  Version 6.0
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 $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) {
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->query_cache->getHash($sql, $params, $extras);
359 
360  $cached_results = $this->query_cache->get($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  // Cache result
386  $this->query_cache->set($hash, $return);
387 
388  return $return;
389  }
390 
402  protected function executeQuery(QueryBuilder $query) {
403 
404  try {
405  $result = $this->trackQuery($query, function() use ($query) {
406  if ($query instanceof \Elgg\Database\Select) {
407  return $query->executeQuery();
408  } else {
409  return $query->executeStatement();
410  }
411  });
412  } catch (\Exception $e) {
413  $ex = new DatabaseException($e->getMessage(), 0, $e);
414  $ex->setParameters($query->getParameters());
415  $ex->setQuery($query->getSQL());
416 
417  throw $ex;
418  }
419 
420  return $result;
421  }
422 
431  public function trackQuery(QueryBuilder $query, callable $callback) {
432 
433  $params = $query->getParameters();
434  $sql = $query->getSQL();
435 
436  $this->query_count++;
437 
438  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
439  $this->beginTimer(['SQL', $timer_key]);
440 
441  $stop_timer = function() use ($timer_key) {
442  $this->endTimer(['SQL', $timer_key]);
443  };
444 
445  try {
446  $result = $callback();
447  } catch (\Exception $e) {
448  $stop_timer();
449 
450  throw $e;
451  }
452 
453  $stop_timer();
454 
455  return $result;
456  }
457 
469  public function registerDelayedQuery(QueryBuilder $query, $callback = null): void {
470  if (Application::isCli() && !$this->config->testing_mode) {
471  // during CLI execute delayed queries immediately (unless in testing mode, during PHPUnit)
472  // this should prevent OOM during long-running jobs
473  // @see Database::executeDelayedQueries()
474  try {
475  $stmt = $this->executeQuery($query);
476 
477  if (is_callable($callback)) {
478  call_user_func($callback, $stmt);
479  }
480  } catch (\Throwable $t) {
481  // Suppress all exceptions to not allow the application to crash
482  $this->getLogger()->error($t);
483  }
484 
485  return;
486  }
487 
488  $this->delayed_queries[] = [
489  self::DELAYED_QUERY => $query,
490  self::DELAYED_HANDLER => $callback,
491  ];
492  }
493 
500  public function executeDelayedQueries(): void {
501 
502  foreach ($this->delayed_queries as $set) {
503  $query = $set[self::DELAYED_QUERY];
504  $handler = $set[self::DELAYED_HANDLER];
505 
506  try {
507  $stmt = $this->executeQuery($query);
508 
509  if (is_callable($handler)) {
510  call_user_func($handler, $stmt);
511  }
512  } catch (\Throwable $t) {
513  // Suppress all exceptions since page already sent to requestor
514  $this->getLogger()->error($t);
515  }
516  }
517 
518  $this->delayed_queries = [];
519  }
520 
528  public function enableQueryCache(): void {
529  $this->query_cache->enable();
530  }
531 
540  public function disableQueryCache(): void {
541  $this->query_cache->disable();
542  }
543 
549  public function getQueryCount(): int {
550  return $this->query_count;
551  }
552 
560  public function getServerVersion(string $type = DbConfig::READ_WRITE): string {
561  return $this->getConnection($type)->getServerVersion();
562  }
563 
571  public function isMariaDB(string $type = DbConfig::READ_WRITE): bool {
572  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MariaDBPlatform;
573  }
574 
584  public function isMySQL(string $type = DbConfig::READ_WRITE): bool {
585  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySQLPlatform;
586  }
587 
596  public function __get($name) {
597  if ($name === 'prefix') {
598  return $this->table_prefix;
599  }
600 
601  throw new RuntimeException("Cannot read property '{$name}'");
602  }
603 
613  public function __set($name, $value): void {
614  throw new RuntimeException("Cannot write property '{$name}'");
615  }
616 }
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:549
$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:584
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:469
$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:613
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:402
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:431
__get($name)
Handle magic property reads.
Definition: Database.php:596
getTablePrefix()
Get the database table prefix.
Definition: DbConfig.php:99
isMariaDB(string $type=DbConfig::READ_WRITE)
Is the database MariaDB.
Definition: Database.php:571
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: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
disableQueryCache()
Disable the query cache.
Definition: Database.php:540
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
enableQueryCache()
Enable the query cache.
Definition: Database.php:528
_elgg_services()
Get the global service provider.
Definition: elgglib.php:351
$handler
Definition: add.php:7
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:500
getServerVersion(string $type=DbConfig::READ_WRITE)
Get the server version number.
Definition: Database.php:560
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
Query builder for fetching data from the database.
Definition: Select.php:8
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59