Elgg  Version master
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 $db_config;
68 
69  protected Config $config;
70 
78  public function __construct(DbConfig $db_config, QueryCache $query_cache, Config $config) {
79  $this->query_cache = $query_cache;
80  $this->config = $config;
81 
82  $this->resetConnections($db_config);
83  }
84 
92  public function resetConnections(DbConfig $config) {
93  $this->closeConnections();
94 
95  $this->db_config = $config;
96  $this->table_prefix = $config->getTablePrefix();
97  $this->query_cache->enable();
98  $this->query_cache->clear();
99  }
100 
109  public function closeConnections(): void {
110  foreach ($this->connections as $connection) {
111  $connection->close();
112  }
113 
114  $this->connections = [];
115  }
116 
124  public function getConnection(string $type): Connection {
125  if (isset($this->connections[$type])) {
126  return $this->connections[$type];
127  } else if (isset($this->connections['readwrite'])) {
128  return $this->connections['readwrite'];
129  }
130 
131  $this->setupConnections();
132 
133  return $this->getConnection($type);
134  }
135 
144  public function setupConnections(): void {
145  if ($this->db_config->isDatabaseSplit()) {
146  $this->connect('read');
147  $this->connect('write');
148  } else {
149  $this->connect('readwrite');
150  }
151  }
152 
163  public function connect(string $type = 'readwrite'): void {
164  $conf = $this->db_config->getConnectionConfig($type);
165 
166  $params = [
167  'dbname' => $conf['database'],
168  'user' => $conf['user'],
169  'password' => $conf['password'],
170  'host' => $conf['host'],
171  'port' => $conf['port'],
172  'charset' => $conf['encoding'],
173  'driver' => 'pdo_mysql',
174  ];
175 
176  try {
177  $this->connections[$type] = DriverManager::getConnection($params);
178 
179  // https://github.com/Elgg/Elgg/issues/8121
180  $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
181  $this->connections[$type]->executeStatement("SET SESSION sql_mode=($sub_query);");
182  } catch (\Exception $e) {
183  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
184  $this->log(LogLevel::ERROR, $e);
185 
186  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
187  $msg = "Elgg couldn't select the database '{$conf['database']}'. Please check that the database is created and you have access to it.";
188  } else {
189  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
190  }
191 
192  throw new DatabaseException($msg);
193  }
194  }
195 
211  public function getData(QueryBuilder $query, $callback = null) {
212  return $this->getResults($query, $callback, false);
213  }
214 
227  public function getDataRow(QueryBuilder $query, $callback = null) {
228  return $this->getResults($query, $callback, true);
229  }
230 
240  public function insertData(QueryBuilder $query): int {
241 
242  $params = $query->getParameters();
243  $sql = $query->getSQL();
244 
245  $this->getLogger()->info("DB insert query {$sql} (params: " . print_r($params, true) . ')');
246 
247  $this->query_cache->clear();
248 
249  $this->executeQuery($query);
250  return (int) $query->getConnection()->lastInsertId();
251  }
252 
263  public function updateData(QueryBuilder $query, bool $get_num_rows = false) {
264  $params = $query->getParameters();
265  $sql = $query->getSQL();
266 
267  $this->getLogger()->info("DB update query {$sql} (params: " . print_r($params, true) . ')');
268 
269  $this->query_cache->clear();
270 
271  $result = $this->executeQuery($query);
272  if (!$get_num_rows) {
273  return true;
274  }
275 
276  return ($result instanceof Result) ? $result->rowCount() : $result;
277  }
278 
288  public function deleteData(QueryBuilder $query): int {
289  $params = $query->getParameters();
290  $sql = $query->getSQL();
291 
292  $this->getLogger()->info("DB delete query {$sql} (params: " . print_r($params, true) . ')');
293 
294  $this->query_cache->clear();
295 
296  $result = $this->executeQuery($query);
297  return ($result instanceof Result) ? $result->rowCount() : $result;
298  }
299 
311  protected function fingerprintCallback($callback): string {
312  if (is_string($callback)) {
313  return $callback;
314  }
315 
316  if (is_object($callback)) {
317  return spl_object_hash($callback) . '::__invoke';
318  }
319 
320  if (is_array($callback)) {
321  if (is_string($callback[0])) {
322  return "{$callback[0]}::{$callback[1]}";
323  }
324 
325  return spl_object_hash($callback[0]) . "::{$callback[1]}";
326  }
327 
328  // this should not happen
329  return '';
330  }
331 
344  protected function getResults(QueryBuilder $query, $callback = null, bool $single = false) {
345  $params = $query->getParameters();
346  $sql = $query->getSQL();
347 
348  // Since we want to cache results of running the callback, we need to
349  // namespace the query with the callback and single result request.
350  // https://github.com/elgg/elgg/issues/4049
351  $extras = (int) $single . '|';
352  if ($callback) {
353  if (!is_callable($callback)) {
354  throw new RuntimeException('$callback must be a callable function. Given ' . _elgg_services()->handlers->describeCallable($callback));
355  }
356 
357  $extras .= $this->fingerprintCallback($callback);
358  }
359 
360  $hash = $this->query_cache->getHash($sql, $params, $extras);
361 
362  $cached_results = $this->query_cache->get($hash);
363  if (isset($cached_results)) {
364  return $cached_results;
365  }
366 
367  $this->getLogger()->info("DB select query {$sql} (params: " . print_r($params, true) . ')');
368 
369  $return = [];
370 
371  $stmt = $this->executeQuery($query);
372 
373  while ($row = $stmt->fetchAssociative()) {
374  $row_obj = (object) $row;
375  if ($callback) {
376  $row_obj = call_user_func($callback, $row_obj);
377  }
378 
379  if ($single) {
380  $return = $row_obj;
381  break;
382  } else {
383  $return[] = $row_obj;
384  }
385  }
386 
387  // Cache result
388  $this->query_cache->set($hash, $return);
389 
390  return $return;
391  }
392 
404  protected function executeQuery(QueryBuilder $query) {
405 
406  try {
407  $result = $this->trackQuery($query, function() use ($query) {
408  if ($query instanceof \Elgg\Database\Select) {
409  return $query->executeQuery();
410  } else {
411  return $query->executeStatement();
412  }
413  });
414  } catch (\Exception $e) {
415  $ex = new DatabaseException($e->getMessage(), 0, $e);
416  $ex->setParameters($query->getParameters());
417  $ex->setQuery($query->getSQL());
418 
419  throw $ex;
420  }
421 
422  return $result;
423  }
424 
433  public function trackQuery(QueryBuilder $query, callable $callback) {
434 
435  $params = $query->getParameters();
436  $sql = $query->getSQL();
437 
438  $this->query_count++;
439 
440  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
441  $this->beginTimer(['SQL', $timer_key]);
442 
443  $stop_timer = function() use ($timer_key) {
444  $this->endTimer(['SQL', $timer_key]);
445  };
446 
447  try {
448  $result = $callback();
449  } catch (\Exception $e) {
450  $stop_timer();
451 
452  throw $e;
453  }
454 
455  $stop_timer();
456 
457  return $result;
458  }
459 
471  public function registerDelayedQuery(QueryBuilder $query, $callback = null): void {
472  if (Application::isCli() && !$this->config->testing_mode) {
473  // during CLI execute delayed queries immediately (unless in testing mode, during PHPUnit)
474  // this should prevent OOM during long-running jobs
475  // @see Database::executeDelayedQueries()
476  try {
477  $stmt = $this->executeQuery($query);
478 
479  if (is_callable($callback)) {
480  call_user_func($callback, $stmt);
481  }
482  } catch (\Throwable $t) {
483  // Suppress all exceptions to not allow the application to crash
484  $this->getLogger()->error($t);
485  }
486 
487  return;
488  }
489 
490  $this->delayed_queries[] = [
491  self::DELAYED_QUERY => $query,
492  self::DELAYED_HANDLER => $callback,
493  ];
494  }
495 
502  public function executeDelayedQueries(): void {
503 
504  foreach ($this->delayed_queries as $set) {
505  $query = $set[self::DELAYED_QUERY];
506  $handler = $set[self::DELAYED_HANDLER];
507 
508  try {
509  $stmt = $this->executeQuery($query);
510 
511  if (is_callable($handler)) {
512  call_user_func($handler, $stmt);
513  }
514  } catch (\Throwable $t) {
515  // Suppress all exceptions since page already sent to requestor
516  $this->getLogger()->error($t);
517  }
518  }
519 
520  $this->delayed_queries = [];
521  }
522 
530  public function enableQueryCache(): void {
531  $this->query_cache->enable();
532  }
533 
542  public function disableQueryCache(): void {
543  $this->query_cache->disable();
544  }
545 
551  public function getQueryCount(): int {
552  return $this->query_count;
553  }
554 
562  public function getServerVersion(string $type = DbConfig::READ_WRITE): string {
563  $driver = $this->getConnection($type)->getWrappedConnection();
564  if ($driver instanceof ServerInfoAwareConnection) {
565  $version = $driver->getServerVersion();
566 
567  if ($this->isMariaDB($type)) {
568  if (str_starts_with($version, '5.5.5-')) {
569  $version = substr($version, 6);
570  }
571  }
572 
573  return $version;
574  }
575 
576  return '';
577  }
578 
586  public function isMariaDB(string $type = DbConfig::READ_WRITE): bool {
587  $driver = $this->getConnection($type)->getWrappedConnection();
588  if ($driver instanceof ServerInfoAwareConnection) {
589  $version = $driver->getServerVersion();
590 
591  return stristr($version, 'mariadb') !== false;
592  }
593 
594  return false;
595  }
596 
605  public function __get($name) {
606  if ($name === 'prefix') {
607  return $this->table_prefix;
608  }
609 
610  throw new RuntimeException("Cannot read property '{$name}'");
611  }
612 
622  public function __set($name, $value): void {
623  throw new RuntimeException("Cannot write property '{$name}'");
624  }
625 }
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:551
$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:124
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
updateData(QueryBuilder $query, bool $get_num_rows=false)
Update the database.
Definition: Database.php:263
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:288
getData(QueryBuilder $query, $callback=null)
Retrieve rows from the database.
Definition: Database.php:211
insertData(QueryBuilder $query)
Insert a row into the database.
Definition: Database.php:240
resetConnections(DbConfig $config)
Reset the connections with new credentials.
Definition: Database.php:92
registerDelayedQuery(QueryBuilder $query, $callback=null)
Queue a query for execution upon shutdown.
Definition: Database.php:471
$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:622
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:404
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:433
__get($name)
Handle magic property reads.
Definition: Database.php:605
getTablePrefix()
Get the database table prefix.
Definition: DbConfig.php:99
isMariaDB(string $type=DbConfig::READ_WRITE)
Is the database MariaDB.
Definition: Database.php:586
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:344
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:311
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:542
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:227
$query
enableQueryCache()
Enable the query cache.
Definition: Database.php:530
Config $config
Definition: Database.php:69
_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:502
getServerVersion(string $type=DbConfig::READ_WRITE)
Get the server version number.
Definition: Database.php:562
closeConnections()
Close all database connections.
Definition: Database.php:109
setupConnections()
Establish database connections.
Definition: Database.php:144
connect(string $type= 'readwrite')
Establish a connection to the database server.
Definition: Database.php:163
Query builder for fetching data from the database.
Definition: Select.php:8
__construct(DbConfig $db_config, QueryCache $query_cache, Config $config)
Constructor.
Definition: Database.php:78
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59