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