Elgg  Version 6.2
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  // type is configured
116  return $this->connections[$type];
117  } elseif (isset($this->connections['readwrite'])) {
118  // fallback, for request of read/write but no split db
119  return $this->connections['readwrite'];
120  } elseif (isset($this->connections['read'])) {
121  // split db configured, readwrite requested
122  return $this->connections['read'];
123  }
124 
125  $this->setupConnections();
126 
127  return $this->getConnection($type);
128  }
129 
138  public function setupConnections(): void {
139  if ($this->db_config->isDatabaseSplit()) {
140  $this->connect('read');
141  $this->connect('write');
142  } else {
143  $this->connect('readwrite');
144  }
145  }
146 
157  public function connect(string $type = 'readwrite'): void {
158  $conf = $this->db_config->getConnectionConfig($type);
159 
160  $params = [
161  'dbname' => $conf['database'],
162  'user' => $conf['user'],
163  'password' => $conf['password'],
164  'host' => $conf['host'],
165  'port' => $conf['port'],
166  'charset' => $conf['encoding'],
167  'driver' => 'pdo_mysql',
168  ];
169 
170  try {
171  $this->connections[$type] = DriverManager::getConnection($params);
172 
173  // https://github.com/Elgg/Elgg/issues/8121
174  $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
175  $this->connections[$type]->executeStatement("SET SESSION sql_mode=($sub_query);");
176  } catch (\Exception $e) {
177  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
178  $this->log(LogLevel::ERROR, $e);
179 
180  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
181  $msg = "Elgg couldn't select the database '{$conf['database']}'. Please check that the database is created and you have access to it.";
182  } else {
183  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
184  }
185 
186  throw new DatabaseException($msg);
187  }
188  }
189 
205  public function getData(QueryBuilder $query, $callback = null) {
206  return $this->getResults($query, $callback, false);
207  }
208 
221  public function getDataRow(QueryBuilder $query, $callback = null) {
222  return $this->getResults($query, $callback, true);
223  }
224 
234  public function insertData(QueryBuilder $query): int {
235 
236  $params = $query->getParameters();
237  $sql = $query->getSQL();
238 
239  $this->getLogger()->info("DB insert query {$sql} (params: " . print_r($params, true) . ')');
240 
241  $this->query_cache->clear();
242 
243  $this->executeQuery($query);
244 
245  try {
246  return (int) $query->getConnection()->lastInsertId();
247  } catch (DriverException $e) {
248  if ($e->getPrevious() instanceof NoIdentityValue) {
249  return 0;
250  }
251 
252  throw $e;
253  }
254  }
255 
266  public function updateData(QueryBuilder $query, bool $get_num_rows = false): bool|int {
267  $params = $query->getParameters();
268  $sql = $query->getSQL();
269 
270  $this->getLogger()->info("DB update query {$sql} (params: " . print_r($params, true) . ')');
271 
272  $this->query_cache->clear();
273 
274  $result = $this->executeQuery($query);
275  if (!$get_num_rows) {
276  return true;
277  }
278 
279  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
280  }
281 
291  public function deleteData(QueryBuilder $query): int {
292  $params = $query->getParameters();
293  $sql = $query->getSQL();
294 
295  $this->getLogger()->info("DB delete query {$sql} (params: " . print_r($params, true) . ')');
296 
297  $this->query_cache->clear();
298 
299  $result = $this->executeQuery($query);
300  return ($result instanceof Result) ? (int) $result->rowCount() : $result;
301  }
302 
314  protected function fingerprintCallback($callback): string {
315  if (is_string($callback)) {
316  return $callback;
317  }
318 
319  if (is_object($callback)) {
320  return spl_object_hash($callback) . '::__invoke';
321  }
322 
323  if (is_array($callback)) {
324  if (is_string($callback[0])) {
325  return "{$callback[0]}::{$callback[1]}";
326  }
327 
328  return spl_object_hash($callback[0]) . "::{$callback[1]}";
329  }
330 
331  // this should not happen
332  return '';
333  }
334 
347  protected function getResults(QueryBuilder $query, $callback = null, bool $single = false) {
348  $params = $query->getParameters();
349  $sql = $query->getSQL();
350 
351  // Since we want to cache results of running the callback, we need to
352  // namespace the query with the callback and single result request.
353  // https://github.com/elgg/elgg/issues/4049
354  $extras = (int) $single . '|';
355  if ($callback) {
356  if (!is_callable($callback)) {
357  throw new RuntimeException('$callback must be a callable function. Given ' . _elgg_services()->handlers->describeCallable($callback));
358  }
359 
360  $extras .= $this->fingerprintCallback($callback);
361  }
362 
363  $hash = $this->getCacheHash($sql, $params, $extras);
364 
365  $cached_results = $this->query_cache->load($hash);
366  if (isset($cached_results)) {
367  return $cached_results;
368  }
369 
370  $this->getLogger()->info("DB select query {$sql} (params: " . print_r($params, true) . ')');
371 
372  $return = [];
373 
374  $stmt = $this->executeQuery($query);
375 
376  while ($row = $stmt->fetchAssociative()) {
377  $row_obj = (object) $row;
378  if ($callback) {
379  $row_obj = call_user_func($callback, $row_obj);
380  }
381 
382  if ($single) {
383  $return = $row_obj;
384  break;
385  } else {
386  $return[] = $row_obj;
387  }
388  }
389 
390  $this->query_cache->save($hash, $return);
391 
392  return $return;
393  }
394 
406  protected function executeQuery(QueryBuilder $query) {
407 
408  try {
409  $result = $this->trackQuery($query, function() use ($query) {
410  if ($query instanceof \Elgg\Database\Select) {
411  return $query->executeQuery();
412  } else {
413  return $query->executeStatement();
414  }
415  });
416  } catch (\Exception $e) {
417  $ex = new DatabaseException($e->getMessage(), 0, $e);
418  $ex->setParameters($query->getParameters());
419  $ex->setQuery($query->getSQL());
420 
421  throw $ex;
422  }
423 
424  return $result;
425  }
426 
437  protected function getCacheHash(string $sql, array $params = [], string $extras = ''): string {
438  $query_id = $sql . '|';
439  if (!empty($params)) {
440  $query_id .= serialize($params) . '|';
441  }
442 
443  $query_id .= $extras;
444 
445  // MD5 yields smaller mem usage for cache
446  return md5($query_id);
447  }
448 
457  public function trackQuery(QueryBuilder $query, callable $callback) {
458 
459  $params = $query->getParameters();
460  $sql = $query->getSQL();
461 
462  $this->query_count++;
463 
464  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
465  $this->beginTimer(['SQL', $timer_key]);
466 
467  $stop_timer = function() use ($timer_key) {
468  $this->endTimer(['SQL', $timer_key]);
469  };
470 
471  try {
472  $result = $callback();
473  } catch (\Exception $e) {
474  $stop_timer();
475 
476  throw $e;
477  }
478 
479  $stop_timer();
480 
481  return $result;
482  }
483 
495  public function registerDelayedQuery(QueryBuilder $query, $callback = null): void {
496  if (Application::isCli() && !$this->config->testing_mode) {
497  // during CLI execute delayed queries immediately (unless in testing mode, during PHPUnit)
498  // this should prevent OOM during long-running jobs
499  // @see Database::executeDelayedQueries()
500  try {
501  $stmt = $this->executeQuery($query);
502 
503  if (is_callable($callback)) {
504  call_user_func($callback, $stmt);
505  }
506  } catch (\Throwable $t) {
507  // Suppress all exceptions to not allow the application to crash
508  $this->getLogger()->error($t);
509  }
510 
511  return;
512  }
513 
514  $this->delayed_queries[] = [
515  self::DELAYED_QUERY => $query,
516  self::DELAYED_HANDLER => $callback,
517  ];
518  }
519 
526  public function executeDelayedQueries(): void {
527 
528  foreach ($this->delayed_queries as $set) {
529  $query = $set[self::DELAYED_QUERY];
530  $handler = $set[self::DELAYED_HANDLER];
531 
532  try {
533  $stmt = $this->executeQuery($query);
534 
535  if (is_callable($handler)) {
536  call_user_func($handler, $stmt);
537  }
538  } catch (\Throwable $t) {
539  // Suppress all exceptions since page already sent to requestor
540  $this->getLogger()->error($t);
541  }
542  }
543 
544  $this->delayed_queries = [];
545  }
546 
552  public function getQueryCount(): int {
553  return $this->query_count;
554  }
555 
563  public function getServerVersion(string $type = DbConfig::READ_WRITE): string {
564  return $this->getConnection($type)->getServerVersion();
565  }
566 
574  public function isMariaDB(string $type = DbConfig::READ_WRITE): bool {
575  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MariaDBPlatform;
576  }
577 
587  public function isMySQL(string $type = DbConfig::READ_WRITE): bool {
588  return $this->getConnection($type)->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySQLPlatform;
589  }
590 
599  public function __get($name) {
600  if ($name === 'prefix') {
601  return $this->table_prefix;
602  }
603 
604  throw new RuntimeException("Cannot read property '{$name}'");
605  }
606 
616  public function __set($name, $value): void {
617  throw new RuntimeException("Cannot write property '{$name}'");
618  }
619 }
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:552
$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:266
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:587
deleteData(QueryBuilder $query)
Delete data from the database.
Definition: Database.php:291
getData(QueryBuilder $query, $callback=null)
Retrieve rows from the database.
Definition: Database.php:205
insertData(QueryBuilder $query)
Insert a row into the database.
Definition: Database.php:234
resetConnections(DbConfig $config)
Reset the connections with new credentials.
Definition: Database.php:81
if($item instanceof\ElggEntity) elseif($item instanceof\ElggRiverItem) elseif($item instanceof\ElggRelationship) elseif(is_callable([$item, 'getType']))
Definition: item.php:48
registerDelayedQuery(QueryBuilder $query, $callback=null)
Queue a query for execution upon shutdown.
Definition: Database.php:495
$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:616
executeQuery(QueryBuilder $query)
Execute a query.
Definition: Database.php:406
trackQuery(QueryBuilder $query, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:457
__get($name)
Handle magic property reads.
Definition: Database.php:599
getTablePrefix()
Get the database table prefix.
Definition: DbConfig.php:99
isMariaDB(string $type=DbConfig::READ_WRITE)
Is the database MariaDB.
Definition: Database.php:574
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:347
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:314
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:221
$query
getCacheHash(string $sql, array $params=[], string $extras= '')
Returns a hashed key for storage in the cache.
Definition: Database.php:437
_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:526
getServerVersion(string $type=DbConfig::READ_WRITE)
Get the server version number.
Definition: Database.php:563
closeConnections()
Close all database connections.
Definition: Database.php:98
setupConnections()
Establish database connections.
Definition: Database.php:138
connect(string $type= 'readwrite')
Establish a connection to the database server.
Definition: Database.php:157
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59