Elgg  Version master
Database.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
5 use Doctrine\DBAL\Connection;
6 use Doctrine\DBAL\Driver\Exception\NoIdentityValue;
7 use Doctrine\DBAL\DriverManager;
8 use Doctrine\DBAL\Exception\DriverException;
9 use Doctrine\DBAL\Result;
16 use Elgg\Traits\Loggable;
17 use Psr\Log\LogLevel;
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 }
if(! $user||! $user->canDelete()) $name
Definition: delete.php:22
$type
Definition: delete.php:21
$params
Saves global plugin settings.
Definition: save.php:13
$handler
Definition: add.php:7
return[ 'admin/delete_admin_notices'=>['access'=> 'admin'], 'admin/menu/save'=>['access'=> 'admin'], 'admin/plugins/activate'=>['access'=> 'admin'], 'admin/plugins/activate_all'=>['access'=> 'admin'], 'admin/plugins/deactivate'=>['access'=> 'admin'], 'admin/plugins/deactivate_all'=>['access'=> 'admin'], 'admin/plugins/set_priority'=>['access'=> 'admin'], 'admin/security/security_txt'=>['access'=> 'admin'], 'admin/security/settings'=>['access'=> 'admin'], 'admin/security/regenerate_site_secret'=>['access'=> 'admin'], 'admin/site/cache/invalidate'=>['access'=> 'admin'], 'admin/site/flush_cache'=>['access'=> 'admin'], 'admin/site/icons'=>['access'=> 'admin'], 'admin/site/set_maintenance_mode'=>['access'=> 'admin'], 'admin/site/set_robots'=>['access'=> 'admin'], 'admin/site/theme'=>['access'=> 'admin'], 'admin/site/unlock_upgrade'=>['access'=> 'admin'], 'admin/site/settings'=>['access'=> 'admin'], 'admin/upgrade'=>['access'=> 'admin'], 'admin/upgrade/reset'=>['access'=> 'admin'], 'admin/user/ban'=>['access'=> 'admin'], 'admin/user/bulk/ban'=>['access'=> 'admin'], 'admin/user/bulk/delete'=>['access'=> 'admin'], 'admin/user/bulk/unban'=>['access'=> 'admin'], 'admin/user/bulk/validate'=>['access'=> 'admin'], 'admin/user/change_email'=>['access'=> 'admin'], 'admin/user/delete'=>['access'=> 'admin'], 'admin/user/login_as'=>['access'=> 'admin'], 'admin/user/logout_as'=>[], 'admin/user/makeadmin'=>['access'=> 'admin'], 'admin/user/resetpassword'=>['access'=> 'admin'], 'admin/user/removeadmin'=>['access'=> 'admin'], 'admin/user/unban'=>['access'=> 'admin'], 'admin/user/validate'=>['access'=> 'admin'], 'annotation/delete'=>[], 'avatar/upload'=>[], 'comment/save'=>[], 'diagnostics/download'=>['access'=> 'admin'], 'entity/chooserestoredestination'=>[], 'entity/delete'=>[], 'entity/mute'=>[], 'entity/restore'=>[], 'entity/subscribe'=>[], 'entity/trash'=>[], 'entity/unmute'=>[], 'entity/unsubscribe'=>[], 'login'=>['access'=> 'logged_out'], 'logout'=>[], 'notifications/mute'=>['access'=> 'public'], 'plugins/settings/remove'=>['access'=> 'admin'], 'plugins/settings/save'=>['access'=> 'admin'], 'plugins/usersettings/save'=>[], 'register'=>['access'=> 'logged_out', 'middleware'=>[\Elgg\Router\Middleware\RegistrationAllowedGatekeeper::class,],], 'river/delete'=>[], 'settings/notifications'=>[], 'settings/notifications/subscriptions'=>[], 'user/changepassword'=>['access'=> 'public'], 'user/requestnewpassword'=>['access'=> 'public'], 'useradd'=>['access'=> 'admin'], 'usersettings/save'=>[], 'widgets/add'=>[], 'widgets/delete'=>[], 'widgets/move'=>[], 'widgets/save'=>[],]
Definition: actions.php:73
foreach( $paths as $path)
Definition: autoloader.php:12
$query
Load, boot, and implement a front controller for an Elgg application.
Definition: Application.php:47
Volatile cache for select queries.
Definition: QueryCache.php:12
Database configuration service.
Definition: DbConfig.php:13
Database abstraction query builder.
The Elgg database.
Definition: Database.php:26
insertData(QueryBuilder $query)
Insert a row into the database.
Definition: Database.php:229
getData(QueryBuilder $query, $callback=null)
Retrieve rows from the database.
Definition: Database.php:200
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:401
connect(string $type='readwrite')
Establish a connection to the database server.
Definition: Database.php:152
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
isMySQL(string $type=DbConfig::READ_WRITE)
Is the database MySQL.
Definition: Database.php:582
getCacheHash(string $sql, array $params=[], string $extras='')
Returns a hashed key for storage in the cache.
Definition: Database.php:432
registerDelayedQuery(QueryBuilder $query, $callback=null)
Queue a query for execution upon shutdown.
Definition: Database.php:490
__set($name, $value)
Handle magic property writes.
Definition: Database.php:611
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:452
getQueryCount()
Get the number of queries made to the database.
Definition: Database.php:547
getConnection(string $type)
Gets (if required, also creates) a DB connection.
Definition: Database.php:113
deleteData(QueryBuilder $query)
Delete data from the database.
Definition: Database.php:286
setupConnections()
Establish database connections.
Definition: Database.php:133
resetConnections(DbConfig $config)
Reset the connections with new credentials.
Definition: Database.php:81
getDataRow(QueryBuilder $query, $callback=null)
Retrieve a single row from the database.
Definition: Database.php:216
closeConnections()
Close all database connections.
Definition: Database.php:98
fingerprintCallback($callback)
Get a string that uniquely identifies a callback during the current request.
Definition: Database.php:309
isMariaDB(string $type=DbConfig::READ_WRITE)
Is the database MariaDB.
Definition: Database.php:569
__get($name)
Handle magic property reads.
Definition: Database.php:594
updateData(QueryBuilder $query, bool $get_num_rows=false)
Update the database.
Definition: Database.php:261
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
__construct(DbConfig $db_config, protected QueryCache $query_cache, protected Config $config)
Constructor.
Definition: Database.php:70
A generic parent class for database exceptions.
Exception thrown if an error which can only be found on runtime occurs.
Result of a single BatchUpgrade run.
Definition: Result.php:10
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
_elgg_services()
Get the global service provider.
Definition: elgglib.php:353
$value
Definition: generic.php:51
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:12
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:43
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10