Elgg  Version 2.3
Database.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg;
3 
8 
17 class Database {
19 
20  const DELAYED_QUERY = 'q';
21  const DELAYED_TYPE = 't';
22  const DELAYED_HANDLER = 'h';
23  const DELAYED_PARAMS = 'p';
24 
28  private $table_prefix;
29 
33  private $connections = [];
34 
38  private $query_count = 0;
39 
51  private $query_cache = null;
52 
56  private $query_cache_size = 50;
57 
65  private $delayed_queries = array();
66 
70  private $installed = false;
71 
75  private $config;
76 
80  private $logger;
81 
88  public function __construct(\Elgg\Database\Config $config, \Elgg\Logger $logger = null) {
89 
90  $this->logger = $logger;
91  $this->config = $config;
92 
93  $this->table_prefix = $config->getTablePrefix();
94 
95  $this->enableQueryCache();
96  }
97 
105  public function setLogger(Logger $logger) {
106  $this->logger = $logger;
107  }
108 
117  protected function getConnection($type) {
118  if (isset($this->connections[$type])) {
119  return $this->connections[$type];
120  } else if (isset($this->connections['readwrite'])) {
121  return $this->connections['readwrite'];
122  } else {
123  $this->setupConnections();
124  return $this->getConnection($type);
125  }
126  }
127 
138  public function setupConnections() {
139  if ($this->config->isDatabaseSplit()) {
140  $this->connect('read');
141  $this->connect('write');
142  } else {
143  $this->connect('readwrite');
144  }
145  }
146 
158  public function connect($type = "readwrite") {
159  $conf = $this->config->getConnectionConfig($type);
160 
161  $params = [
162  'dbname' => $conf['database'],
163  'user' => $conf['user'],
164  'password' => $conf['password'],
165  'host' => $conf['host'],
166  'charset' => 'utf8',
167  'driver' => 'pdo_mysql',
168  ];
169 
170  try {
171  $this->connections[$type] = DriverManager::getConnection($params);
172  $this->connections[$type]->setFetchMode(\PDO::FETCH_OBJ);
173 
174  // https://github.com/Elgg/Elgg/issues/8121
175  $sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
176  $this->connections[$type]->exec("SET SESSION sql_mode=($sub_query);");
177 
178  } catch (\PDOException $e) {
179  // @todo just allow PDO exceptions
180  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
181  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
182  $msg = "Elgg couldn't select the database '{$conf['database']}'. "
183  . "Please check that the database is created and you have access to it.";
184  } else {
185  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
186  }
187  throw new \DatabaseException($msg);
188  }
189  }
190 
208  public function getData($query, $callback = null, array $params = []) {
209  return $this->getResults($query, $callback, false, $params);
210  }
211 
226  public function getDataRow($query, $callback = null, array $params = []) {
227  return $this->getResults($query, $callback, true, $params);
228  }
229 
242  public function insertData($query, array $params = []) {
243 
244  if ($this->logger) {
245  $this->logger->info("DB query $query");
246  }
247 
248  $connection = $this->getConnection('write');
249 
250  $this->invalidateQueryCache();
251 
252  $this->executeQuery($query, $connection, $params);
253  return (int)$connection->lastInsertId();
254  }
255 
270  public function updateData($query, $get_num_rows = false, array $params = []) {
271 
272  if ($this->logger) {
273  $this->logger->info("DB query $query");
274  }
275 
276  $this->invalidateQueryCache();
277 
278  $stmt = $this->executeQuery($query, $this->getConnection('write'), $params);
279  if ($get_num_rows) {
280  return $stmt->rowCount();
281  } else {
282  return true;
283  }
284  }
285 
297  public function deleteData($query, array $params = []) {
298 
299  if ($this->logger) {
300  $this->logger->info("DB query $query");
301  }
302 
303  $connection = $this->getConnection('write');
304 
305  $this->invalidateQueryCache();
306 
307  $stmt = $this->executeQuery("$query", $connection, $params);
308  return (int)$stmt->rowCount();
309  }
310 
324  public function fingerprintCallback($callback) {
325  if (is_string($callback)) {
326  return $callback;
327  }
328  if (is_object($callback)) {
329  return spl_object_hash($callback) . "::__invoke";
330  }
331  if (is_array($callback)) {
332  if (is_string($callback[0])) {
333  return "{$callback[0]}::{$callback[1]}";
334  }
335  return spl_object_hash($callback[0]) . "::{$callback[1]}";
336  }
337  // this should not happen
338  return "";
339  }
340 
354  protected function getResults($query, $callback = null, $single = false, array $params = []) {
355 
356  // Since we want to cache results of running the callback, we need to
357  // need to namespace the query with the callback and single result request.
358  // https://github.com/elgg/elgg/issues/4049
359  $query_id = (int)$single . $query . '|';
360  if ($params) {
361  $query_id .= serialize($params) . '|';
362  }
363 
364  if ($callback) {
365  if (!is_callable($callback)) {
366  $inspector = new \Elgg\Debug\Inspector();
367  throw new \RuntimeException('$callback must be a callable function. Given ' . $inspector->describeCallable($callback));
368  }
369  $query_id .= $this->fingerprintCallback($callback);
370  }
371  // MD5 yields smaller mem usage for cache and cleaner logs
372  $hash = md5($query_id);
373 
374  // Is cached?
375  if ($this->query_cache) {
376  if (isset($this->query_cache[$hash])) {
377  if ($this->logger) {
378  // TODO add params in $query here
379  $this->logger->info("DB query $query results returned from cache (hash: $hash)");
380  }
381  return $this->query_cache[$hash];
382  }
383  }
384 
385  $return = array();
386 
387  $stmt = $this->executeQuery($query, $this->getConnection('read'), $params);
388  while ($row = $stmt->fetch()) {
389  if ($callback) {
390  $row = call_user_func($callback, $row);
391  }
392 
393  if ($single) {
394  $return = $row;
395  break;
396  } else {
397  $return[] = $row;
398  }
399  }
400 
401  // Cache result
402  if ($this->query_cache) {
403  $this->query_cache[$hash] = $return;
404  if ($this->logger) {
405  // TODO add params in $query here
406  $this->logger->info("DB query $query results cached (hash: $hash)");
407  }
408  }
409 
410  return $return;
411  }
412 
426  protected function executeQuery($query, Connection $connection, array $params = []) {
427  if ($query == null) {
428  throw new \DatabaseException("Query cannot be null");
429  }
430 
431  $this->query_count++;
432 
433  if ($this->timer) {
434  $timer_key = preg_replace('~\\s+~', ' ', trim($query . '|' . serialize($params)));
435  $this->timer->begin(['SQL', $timer_key]);
436  }
437 
438  try {
439  if ($params) {
440  $value = $connection->executeQuery($query, $params);
441  } else {
442  // faster
443  $value = $connection->query($query);
444  }
445  } catch (\Exception $e) {
446  throw new \DatabaseException($e->getMessage() . "\n\n"
447  . "QUERY: $query \n\n"
448  . "PARAMS: " . print_r($params, true),
449  null,
450  $e);
451  }
452 
453  if ($this->timer) {
454  $this->timer->end(['SQL', $timer_key]);
455  }
456 
457  return $value;
458  }
459 
485  public function runSqlScript($scriptlocation) {
486  $script = file_get_contents($scriptlocation);
487  if ($script) {
488 
489  $errors = array();
490 
491  // Remove MySQL '-- ' and '# ' style comments
492  $script = preg_replace('/^(?:--|#) .*$/m', '', $script);
493 
494  // Statements must end with ; and a newline
495  $sql_statements = preg_split('/;[\n\r]+/', "$script\n");
496 
497  foreach ($sql_statements as $statement) {
498  $statement = trim($statement);
499  $statement = str_replace("prefix_", $this->table_prefix, $statement);
500  if (!empty($statement)) {
501  try {
502  $this->updateData($statement);
503  } catch (\DatabaseException $e) {
504  $errors[] = $e->getMessage();
505  }
506  }
507  }
508  if (!empty($errors)) {
509  $errortxt = "";
510  foreach ($errors as $error) {
511  $errortxt .= " {$error};";
512  }
513 
514  $msg = "There were a number of issues: " . $errortxt;
515  throw new \DatabaseException($msg);
516  }
517  } else {
518  $msg = "Elgg couldn't find the requested database script at " . $scriptlocation . ".";
519  throw new \DatabaseException($msg);
520  }
521  }
522 
537  public function registerDelayedQuery($query, $type, $callback = null, array $params = []) {
538  if ($type != 'read' && $type != 'write') {
539  return false;
540  }
541 
542  $this->delayed_queries[] = [
543  self::DELAYED_QUERY => $query,
544  self::DELAYED_TYPE => $type,
545  self::DELAYED_HANDLER => $callback,
546  self::DELAYED_PARAMS => $params,
547  ];
548 
549  return true;
550  }
551 
560  public function executeDelayedQueries() {
561 
562  foreach ($this->delayed_queries as $set) {
563  $query = $set[self::DELAYED_QUERY];
564  $type = $set[self::DELAYED_TYPE];
565  $handler = $set[self::DELAYED_HANDLER];
566  $params = $set[self::DELAYED_PARAMS];
567 
568  try {
569 
570  $stmt = $this->executeQuery($query, $this->getConnection($type), $params);
571 
572  if (is_callable($handler)) {
573  call_user_func($handler, $stmt);
574  }
575  } catch (\Exception $e) {
576  if ($this->logger) {
577  // Suppress all exceptions since page already sent to requestor
578  $this->logger->error($e);
579  }
580  }
581  }
582  }
583 
592  public function enableQueryCache() {
593  if ($this->config->isQueryCacheEnabled() && $this->query_cache === null) {
594  // @todo if we keep this cache, expose the size as a config parameter
595  $this->query_cache = new \Elgg\Cache\LRUCache($this->query_cache_size);
596  }
597  }
598 
608  public function disableQueryCache() {
609  $this->query_cache = null;
610  }
611 
617  protected function invalidateQueryCache() {
618  if ($this->query_cache) {
619  $this->query_cache->clear();
620  if ($this->logger) {
621  $this->logger->info("Query cache invalidated");
622  }
623  }
624  }
625 
633  public function assertInstalled() {
634 
635  if ($this->installed) {
636  return;
637  }
638 
639  try {
640  $sql = "SELECT value FROM {$this->table_prefix}datalists WHERE name = 'installed'";
641  $this->getConnection('read')->query($sql);
642  } catch (\DatabaseException $e) {
643  throw new \InstallationException("Unable to handle this request. This site is not configured or the database is down.");
644  }
645 
646  $this->installed = true;
647  }
648 
655  public function getQueryCount() {
656  return $this->query_count;
657  }
658 
667  public function getTablePrefix() {
668  if (function_exists('elgg_deprecated_notice')) {
669  elgg_deprecated_notice(__METHOD__ . ' is deprecated. Read the "prefix" property', '2.3');
670  }
671  return $this->table_prefix;
672  }
673 
682  public function sanitizeInt($value, $signed = true) {
683  $value = (int) $value;
684 
685  if ($signed === false) {
686  if ($value < 0) {
687  $value = 0;
688  }
689  }
690 
691  return $value;
692  }
693 
702  public function sanitizeString($value) {
703  if (is_array($value)) {
704  throw new \DatabaseException(__METHOD__ . '() and serialize_string() cannot accept arrays.');
705  }
706  $quoted = $this->getConnection('read')->quote($value);
707  if ($quoted[0] !== "'" || substr($quoted, -1) !== "'") {
708  throw new \DatabaseException("PDO::quote did not return surrounding single quotes.");
709  }
710  return substr($quoted, 1, -1);
711  }
712 
721  public function getServerVersion($type) {
722  $driver = $this->getConnection($type)->getWrappedConnection();
723  if ($driver instanceof ServerInfoAwareConnection) {
724  return $driver->getServerVersion();
725  }
726 
727  return null;
728  }
729 
736  public function __get($name) {
737  if ($name === 'prefix') {
738  return $this->table_prefix;
739  }
740 
741  throw new \RuntimeException("Cannot read property '$name'");
742  }
743 
751  public function __set($name, $value) {
752  throw new \RuntimeException("Cannot write property '$name'");
753  }
754 }
updateData($query, $get_num_rows=false, array $params=[])
Update the database.
Definition: Database.php:270
if(!$owner||!($owner instanceof ElggUser)||!$owner->canEdit()) $error
Definition: upload.php:14
setLogger(Logger $logger)
Set the logger object.
Definition: Database.php:105
getQueryCount()
Get the number of queries made to the database.
Definition: Database.php:655
__construct(\Elgg\Database\Config $config,\Elgg\Logger $logger=null)
Constructor.
Definition: Database.php:88
if($guid==elgg_get_logged_in_user_guid()) $name
Definition: delete.php:21
invalidateQueryCache()
Invalidate the query cache.
Definition: Database.php:617
$e
Definition: metadata.php:12
The Elgg database.
Definition: Database.php:17
$CONFIG installed
Is the site fully installed.
Definition: config.php:58
$value
Definition: longtext.php:42
Access to configuration values.
Definition: Config.php:11
$return
Definition: opendd.php:15
executeQuery($query, Connection $connection, array $params=[])
Execute a query.
Definition: Database.php:426
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:9
$params
Definition: login.php:72
sanitizeInt($value, $signed=true)
Sanitizes an integer value for use in a query.
Definition: Database.php:682
getData($query, $callback=null, array $params=[])
Retrieve rows from the database.
Definition: Database.php:208
Save menu items.
__set($name, $value)
Handle magic property writes.
Definition: Database.php:751
sanitizeString($value)
Sanitizes a string for use in a query.
Definition: Database.php:702
__get($name)
Handle magic property reads.
Definition: Database.php:736
assertInstalled()
Test that the Elgg database is installed.
Definition: Database.php:633
elgg_deprecated_notice($msg, $dep_version, $backtrace_level=1)
Log a notice about deprecated use of a function, view, etc.
Definition: elgglib.php:1098
runSqlScript($scriptlocation)
Runs a full database script from disk.
Definition: Database.php:485
registerDelayedQuery($query, $type, $callback=null, array $params=[])
Queue a query for execution upon shutdown.
Definition: Database.php:537
fingerprintCallback($callback)
Get a string that uniquely identifies a callback during the current request.
Definition: Database.php:324
getResults($query, $callback=null, $single=false, array $params=[])
Handles queries that return results, running the results through a an optional callback function...
Definition: Database.php:354
connect($type="readwrite")
Establish a connection to the database server.
Definition: Database.php:158
deleteData($query, array $params=[])
Delete data from the database.
Definition: Database.php:297
getConnection($type)
Gets (if required, also creates) a DB connection.
Definition: Database.php:117
disableQueryCache()
Disable the query cache.
Definition: Database.php:608
enableQueryCache()
Enable the query cache.
Definition: Database.php:592
getDataRow($query, $callback=null, array $params=[])
Retrieve a single row from the database.
Definition: Database.php:226
$row
getServerVersion($type)
Get the server version number.
Definition: Database.php:721
$handler
Definition: add.php:10
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:560
setupConnections()
Establish database connections.
Definition: Database.php:138
insertData($query, array $params=[])
Insert a row into the database.
Definition: Database.php:242
http free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:5
getTablePrefix()
Get the value of the "prefix" property.
Definition: Database.php:667
if(!$display_name) $type
Definition: delete.php:27