Elgg  Version 4.x
Database.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
16 
24 class Database {
25 
26  use Profilable;
27  use Loggable;
28 
29  const DELAYED_QUERY = 'q';
30  const DELAYED_TYPE = 't';
31  const DELAYED_HANDLER = 'h';
32  const DELAYED_PARAMS = 'p';
33 
37  private $table_prefix;
38 
42  private $connections = [];
43 
47  private $query_count = 0;
48 
54  protected $query_cache;
55 
63  protected $delayed_queries = [];
64 
68  private $config;
69 
76  public function __construct(DbConfig $config, QueryCache $query_cache) {
77  $this->query_cache = $query_cache;
78 
79  $this->resetConnections($config);
80  }
81 
89  public function resetConnections(DbConfig $config) {
90  $this->closeConnections();
91 
92  $this->config = $config;
93  $this->table_prefix = $config->getTablePrefix();
94  $this->query_cache->enable();
95  $this->query_cache->clear();
96  }
97 
106  public function closeConnections(): void {
107  foreach ($this->connections as $connection) {
108  $connection->close();
109  }
110 
111  $this->connections = [];
112  }
113 
121  public function getConnection(string $type): Connection {
122  if (isset($this->connections[$type])) {
123  return $this->connections[$type];
124  } else if (isset($this->connections['readwrite'])) {
125  return $this->connections['readwrite'];
126  }
127 
128  $this->setupConnections();
129 
130  return $this->getConnection($type);
131  }
132 
141  public function setupConnections(): void {
142  if ($this->config->isDatabaseSplit()) {
143  $this->connect('read');
144  $this->connect('write');
145  } else {
146  $this->connect('readwrite');
147  }
148  }
149 
160  public function connect(string $type = 'readwrite'): void {
161  $conf = $this->config->getConnectionConfig($type);
162 
163  $params = [
164  'dbname' => $conf['database'],
165  'user' => $conf['user'],
166  'password' => $conf['password'],
167  'host' => $conf['host'],
168  'port' => $conf['port'],
169  'charset' => $conf['encoding'],
170  'driver' => 'pdo_mysql',
171  ];
172 
173  try {
174  $this->connections[$type] = DriverManager::getConnection($params);
175 
176  // https://github.com/Elgg/Elgg/issues/8121
177  $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
178  $this->connections[$type]->executeStatement("SET SESSION sql_mode=($sub_query);");
179  } catch (\Exception $e) {
180  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
181  $this->log(LogLevel::ERROR, $e);
182 
183  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
184  $msg = "Elgg couldn't select the database '{$conf['database']}'. "
185  . "Please check that the database is created and you have access to it.";
186  } else {
187  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
188  }
189  throw new DatabaseException($msg);
190  }
191  }
192 
209  public function getData($query, $callback = null, array $params = []) {
210  if (!$query instanceof QueryBuilder) {
211  $this->logDeprecatedMessage('The use of non ' . QueryBuilder::class . ' queries in ' . __METHOD__ . ' has been deprecated', '4.0');
212  }
213 
214  return $this->getResults($query, $callback, false, $params);
215  }
216 
230  public function getDataRow($query, $callback = null, array $params = []) {
231  if (!$query instanceof QueryBuilder) {
232  $this->logDeprecatedMessage('The use of non ' . QueryBuilder::class . ' queries in ' . __METHOD__ . ' has been deprecated', '4.0');
233  }
234 
235  return $this->getResults($query, $callback, true, $params);
236  }
237 
248  public function insertData($query, array $params = []): int {
249  if (!$query instanceof QueryBuilder) {
250  $this->logDeprecatedMessage('The use of non ' . QueryBuilder::class . ' queries in ' . __METHOD__ . ' has been deprecated', '4.0');
251  }
252 
253  $sql = $query;
254  $connection = $this->getConnection('write');
255  if ($query instanceof QueryBuilder) {
256  $params = $query->getParameters();
257  $sql = $query->getSQL();
258  $connection = $query->getConnection();
259  }
260 
261  $this->getLogger()->info("DB insert query {$sql} (params: " . print_r($params, true) . ")");
262 
263  $this->query_cache->clear();
264 
265  $this->executeQuery($query, $connection, $params);
266  return (int) $connection->lastInsertId();
267  }
268 
280  public function updateData($query, bool $get_num_rows = false, array $params = []) {
281  if (!$query instanceof QueryBuilder) {
282  $this->logDeprecatedMessage('The use of non ' . QueryBuilder::class . ' queries in ' . __METHOD__ . ' has been deprecated', '4.0');
283  }
284 
285  $sql = $query;
286  if ($query instanceof QueryBuilder) {
287  $params = $query->getParameters();
288  $sql = $query->getSQL();
289  }
290 
291  $this->getLogger()->info("DB update query {$sql} (params: " . print_r($params, true) . ")");
292 
293  $this->query_cache->clear();
294 
295  $result = $this->executeQuery($query, $this->getConnection('write'), $params);
296  if (!$get_num_rows) {
297  return true;
298  }
299 
300  return ($result instanceof Result) ? $result->rowCount() : $result;
301  }
302 
313  public function deleteData($query, array $params = []): int {
314  if (!$query instanceof QueryBuilder) {
315  $this->logDeprecatedMessage('The use of non ' . QueryBuilder::class . ' queries in ' . __METHOD__ . ' has been deprecated', '4.0');
316  }
317 
318  $sql = $query;
319  if ($query instanceof QueryBuilder) {
320  $params = $query->getParameters();
321  $sql = $query->getSQL();
322  }
323 
324  $this->getLogger()->info("DB delete query {$sql} (params: " . print_r($params, true) . ")");
325 
326  $this->query_cache->clear();
327 
328  $result = $this->executeQuery($query, $this->getConnection('write'), $params);
329  return ($result instanceof Result) ? $result->rowCount() : $result;
330  }
331 
343  protected function fingerprintCallback($callback): string {
344  if (is_string($callback)) {
345  return $callback;
346  }
347 
348  if (is_object($callback)) {
349  return spl_object_hash($callback) . '::__invoke';
350  }
351 
352  if (is_array($callback)) {
353  if (is_string($callback[0])) {
354  return "{$callback[0]}::{$callback[1]}";
355  }
356 
357  return spl_object_hash($callback[0]) . "::{$callback[1]}";
358  }
359 
360  // this should not happen
361  return '';
362  }
363 
377  protected function getResults($query, $callback = null, bool $single = false, array $params = []) {
378 
379  if ($query instanceof QueryBuilder) {
380  $params = $query->getParameters();
381  $sql = $query->getSQL();
382  } else {
383  $sql = $query;
384  }
385 
386  // Since we want to cache results of running the callback, we need to
387  // namespace the query with the callback and single result request.
388  // https://github.com/elgg/elgg/issues/4049
389  $extras = (int) $single . '|';
390  if ($callback) {
391  if (!is_callable($callback)) {
392  throw new \RuntimeException('$callback must be a callable function. Given '
393  . _elgg_services()->handlers->describeCallable($callback));
394  }
395  $extras .= $this->fingerprintCallback($callback);
396  }
397 
398  $hash = $this->query_cache->getHash($sql, $params, $extras);
399 
400  $cached_results = $this->query_cache->get($hash);
401  if (isset($cached_results)) {
402  return $cached_results;
403  }
404 
405  $this->getLogger()->info("DB select query {$sql} (params: " . print_r($params, true) . ")");
406 
407  $return = [];
408 
409  if ($query instanceof QueryBuilder) {
410  $stmt = $this->executeQuery($query, $query->getConnection());
411  } else {
412  $stmt = $this->executeQuery($query, $this->getConnection('read'), $params);
413  }
414 
415  while ($row = $stmt->fetchAssociative()) {
416  $row_obj = (object) $row;
417  if ($callback) {
418  $row_obj = call_user_func($callback, $row_obj);
419  }
420 
421  if ($single) {
422  $return = $row_obj;
423  break;
424  } else {
425  $return[] = $row_obj;
426  }
427  }
428 
429  // Cache result
430  $this->query_cache->set($hash, $return);
431 
432  return $return;
433  }
434 
448  protected function executeQuery($query, Connection $connection, array $params = []) {
449  if ($query == null) {
450  throw new DatabaseException("Query cannot be null");
451  }
452 
453  $sql = $query;
454  if ($query instanceof QueryBuilder) {
455  $params = $query->getParameters();
456  $sql = $query->getSQL();
457  }
458 
459  foreach ($params as $param_key => $value) {
460  if (substr($param_key, 0, 1) !== ':') {
461  continue;
462  }
463 
464  $this->getLogger()->warning('Unsupported colon detected in named param: ' . $param_key);
465 
466  unset($params[$param_key]);
467  $params[substr($param_key, 1)] = $value;
468  }
469 
470  try {
471  $result = $this->trackQuery($sql, $params, function() use ($query, $params, $connection, $sql) {
472  if ($query instanceof QueryBuilder) {
473  if ($query->getType() === QueryBuilder::SELECT) {
474  return $query->executeQuery();
475  } else {
476  return $query->executeStatement();
477  }
478  } elseif (!empty($params)) {
479  return $connection->executeQuery($sql, $params);
480  } else {
481  // faster
482  return $connection->executeQuery($sql);
483  }
484  });
485  } catch (\Exception $e) {
486  $ex = new DatabaseException($e->getMessage(), 0, $e);
487  $ex->setParameters($params);
488  $ex->setQuery($sql);
489 
490  throw $ex;
491  }
492 
493  return $result;
494  }
495 
505  public function trackQuery($query, array $params, callable $callback) {
506 
507  $sql = $query;
508  if ($query instanceof QueryBuilder) {
509  $params = $query->getParameters();
510  $sql = $query->getSQL();
511  }
512 
513  $this->query_count++;
514 
515  $timer_key = preg_replace('~\\s+~', ' ', trim($sql . '|' . serialize($params)));
516  $this->beginTimer(['SQL', $timer_key]);
517 
518  $stop_timer = function() use ($timer_key) {
519  $this->endTimer(['SQL', $timer_key]);
520  };
521 
522  try {
523  $result = $callback();
524  } catch (\Exception $e) {
525  $stop_timer();
526 
527  throw $e;
528  }
529 
530  $stop_timer();
531 
532  return $result;
533  }
534 
548  public function registerDelayedQuery($query, string $type, $callback = null, array $params = []): bool {
549  if (!$query instanceof QueryBuilder) {
550  $this->logDeprecatedMessage('The use of non ' . QueryBuilder::class . ' queries in ' . __METHOD__ . ' has been deprecated', '4.0');
551  }
552 
553  if ($type !== 'read' && $type !== 'write') {
554  return false;
555  }
556 
557  $this->delayed_queries[] = [
558  self::DELAYED_QUERY => $query,
559  self::DELAYED_TYPE => $type,
560  self::DELAYED_HANDLER => $callback,
561  self::DELAYED_PARAMS => $params,
562  ];
563 
564  return true;
565  }
566 
573  public function executeDelayedQueries(): void {
574 
575  foreach ($this->delayed_queries as $set) {
576  $query = $set[self::DELAYED_QUERY];
577  $type = $set[self::DELAYED_TYPE];
578  $handler = $set[self::DELAYED_HANDLER];
579  $params = $set[self::DELAYED_PARAMS];
580 
581  try {
582  $stmt = $this->executeQuery($query, $this->getConnection($type), $params);
583 
584  if (is_callable($handler)) {
585  call_user_func($handler, $stmt);
586  }
587  } catch (\Exception $e) {
588  // Suppress all exceptions since page already sent to requestor
589  $this->getLogger()->error($e);
590  }
591  }
592 
593  $this->delayed_queries = [];
594  }
595 
603  public function enableQueryCache(): void {
604  $this->query_cache->enable();
605  }
606 
615  public function disableQueryCache(): void {
616  $this->query_cache->disable();
617  }
618 
624  public function getQueryCount(): int {
625  return $this->query_count;
626  }
627 
635  public function getServerVersion(string $type = DbConfig::READ_WRITE): string {
636  $driver = $this->getConnection($type)->getWrappedConnection();
637  if ($driver instanceof ServerInfoAwareConnection) {
638  $version = $driver->getServerVersion();
639 
640  if ($this->isMariaDB($type)) {
641  if (strpos($version, '5.5.5-') === 0) {
642  $version = substr($version, 6);
643  }
644  }
645 
646  return $version;
647  }
648 
649  return '';
650  }
651 
659  public function isMariaDB(string $type = DbConfig::READ_WRITE): bool {
660  $driver = $this->getConnection($type)->getWrappedConnection();
661  if ($driver instanceof ServerInfoAwareConnection) {
662  $version = $driver->getServerVersion();
663 
664  return stristr($version, 'mariadb') !== false;
665  }
666 
667  return false;
668  }
669 
678  public function __get($name) {
679  if ($name === 'prefix') {
680  return $this->table_prefix;
681  }
682 
683  throw new \RuntimeException("Cannot read property '$name'");
684  }
685 
695  public function __set($name, $value): void {
696  throw new \RuntimeException("Cannot write property '$name'");
697  }
698 }
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:12
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
getQueryCount()
Get the number of queries made to the database.
Definition: Database.php:624
$params
Saves global plugin settings.
Definition: save.php:13
Database configuration service.
Definition: DbConfig.php:13
getConnection(string $type)
Gets (if required, also creates) a DB connection.
Definition: Database.php:121
__construct(DbConfig $config, QueryCache $query_cache)
Constructor.
Definition: Database.php:76
The Elgg database.
Definition: Database.php:24
$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:21
executeQuery($query, Connection $connection, array $params=[])
Execute a query.
Definition: Database.php:448
resetConnections(DbConfig $config)
Reset the connections with new credentials.
Definition: Database.php:89
$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
getData($query, $callback=null, array $params=[])
Retrieve rows from the database.
Definition: Database.php:209
__set($name, $value)
Handle magic property writes.
Definition: Database.php:695
__get($name)
Handle magic property reads.
Definition: Database.php:678
getTablePrefix()
Get the database table prefix.
Definition: DbConfig.php:67
isMariaDB(string $type=DbConfig::READ_WRITE)
Is the database MariaDB.
Definition: Database.php:659
Volatile cache for select queries.
Definition: QueryCache.php:17
A generic parent class for database exceptions.
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:343
log($level, $message, array $context=[])
Log a message.
Definition: Loggable.php:58
getLogger()
Returns logger.
Definition: Loggable.php:37
deleteData($query, array $params=[])
Delete data from the database.
Definition: Database.php:313
disableQueryCache()
Disable the query cache.
Definition: Database.php:615
if($item instanceof\ElggEntity) elseif($item instanceof\ElggRiverItem) elseif($item instanceof ElggRelationship) elseif(is_callable([$item, 'getType']))
Definition: item.php:48
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:46
$query
updateData($query, bool $get_num_rows=false, array $params=[])
Update the database.
Definition: Database.php:280
enableQueryCache()
Enable the query cache.
Definition: Database.php:603
getDataRow($query, $callback=null, array $params=[])
Retrieve a single row from the database.
Definition: Database.php:230
_elgg_services()
Get the global service provider.
Definition: elgglib.php:777
$handler
Definition: add.php:7
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:573
getServerVersion(string $type=DbConfig::READ_WRITE)
Get the server version number.
Definition: Database.php:635
closeConnections()
Close all database connections.
Definition: Database.php:106
setupConnections()
Establish database connections.
Definition: Database.php:141
insertData($query, array $params=[])
Insert a row into the database.
Definition: Database.php:248
connect(string $type= 'readwrite')
Establish a connection to the database server.
Definition: Database.php:160
trackQuery($query, array $params, callable $callback)
Tracks the query count and timers for a given query.
Definition: Database.php:505
getResults($query, $callback=null, bool $single=false, array $params=[])
Handles queries that return results, running the results through a an optional callback function...
Definition: Database.php:377
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:62
registerDelayedQuery($query, string $type, $callback=null, array $params=[])
Queue a query for execution upon shutdown.
Definition: Database.php:548
logDeprecatedMessage(string $message, string $version)
Sends a message about deprecated use of a function, view, etc.
Definition: Loggable.php:80