Elgg  Version 2.2
 All Classes Namespaces Files Functions Variables Pages
Database.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg;
3 
4 use Doctrine\DBAL\DriverManager;
5 use Doctrine\DBAL\Connection;
6 use Doctrine\DBAL\Driver\Statement;
7 use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
8 
19 class Database {
20  use Profilable;
21 
22  const DELAYED_QUERY = 'q';
23  const DELAYED_TYPE = 't';
24  const DELAYED_HANDLER = 'h';
25  const DELAYED_PARAMS = 'p';
26 
30  private $tablePrefix;
31 
35  private $connections = [];
36 
40  private $queryCount = 0;
41 
53  private $queryCache = null;
54 
58  private $queryCacheSize = 50;
59 
67  private $delayedQueries = array();
68 
72  private $installed = false;
73 
77  private $config;
78 
82  private $logger;
83 
90  public function __construct(\Elgg\Database\Config $config, \Elgg\Logger $logger = null) {
91 
92  $this->logger = $logger;
93  $this->config = $config;
94 
95  $this->tablePrefix = $config->getTablePrefix();
96 
97  $this->enableQueryCache();
98  }
99 
107  public function setLogger(Logger $logger) {
108  $this->logger = $logger;
109  }
110 
119  protected function getConnection($type) {
120  if (isset($this->connections[$type])) {
121  return $this->connections[$type];
122  } else if (isset($this->connections['readwrite'])) {
123  return $this->connections['readwrite'];
124  } else {
125  $this->setupConnections();
126  return $this->getConnection($type);
127  }
128  }
129 
140  public function setupConnections() {
141  if ($this->config->isDatabaseSplit()) {
142  $this->connect('read');
143  $this->connect('write');
144  } else {
145  $this->connect('readwrite');
146  }
147  }
148 
160  public function connect($type = "readwrite") {
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  'charset' => 'utf8',
169  'driver' => 'pdo_mysql',
170  ];
171 
172  try {
173  $this->connections[$type] = DriverManager::getConnection($params);
174  $this->connections[$type]->setFetchMode(\PDO::FETCH_OBJ);
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]->exec("SET SESSION sql_mode=($sub_query);");
179 
180  } catch (\PDOException $e) {
181  // @todo just allow PDO exceptions
182  // http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
183  if ($e->getCode() == 1102 || $e->getCode() == 1049) {
184  $msg = "Elgg couldn't select the database '{$conf['database']}'. Please check that the database is created and you have access to it.";
185  } else {
186  $msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
187  }
188  throw new \DatabaseException($msg);
189  }
190  }
191 
209  public function getData($query, $callback = null, array $params = []) {
210  return $this->getResults($query, $callback, false, $params);
211  }
212 
227  public function getDataRow($query, $callback = null, array $params = []) {
228  return $this->getResults($query, $callback, true, $params);
229  }
230 
243  public function insertData($query, array $params = []) {
244 
245  if ($this->logger) {
246  $this->logger->info("DB query $query");
247  }
248 
249  $connection = $this->getConnection('write');
250 
251  $this->invalidateQueryCache();
252 
253  $this->executeQuery($query, $connection, $params);
254  return (int)$connection->lastInsertId();
255  }
256 
271  public function updateData($query, $get_num_rows = false, array $params = []) {
272 
273  if ($this->logger) {
274  $this->logger->info("DB query $query");
275  }
276 
277  $this->invalidateQueryCache();
278 
279  $stmt = $this->executeQuery($query, $this->getConnection('write'), $params);
280  if ($get_num_rows) {
281  return $stmt->rowCount();
282  } else {
283  return true;
284  }
285  }
286 
298  public function deleteData($query, array $params = []) {
299 
300  if ($this->logger) {
301  $this->logger->info("DB query $query");
302  }
303 
304  $connection = $this->getConnection('write');
305 
306  $this->invalidateQueryCache();
307 
308  $stmt = $this->executeQuery("$query", $connection, $params);
309  return (int)$stmt->rowCount();
310  }
311 
325  public function fingerprintCallback($callback) {
326  if (is_string($callback)) {
327  return $callback;
328  }
329  if (is_object($callback)) {
330  return spl_object_hash($callback) . "::__invoke";
331  }
332  if (is_array($callback)) {
333  if (is_string($callback[0])) {
334  return "{$callback[0]}::{$callback[1]}";
335  }
336  return spl_object_hash($callback[0]) . "::{$callback[1]}";
337  }
338  // this should not happen
339  return "";
340  }
341 
355  protected function getResults($query, $callback = null, $single = false, array $params = []) {
356 
357  // Since we want to cache results of running the callback, we need to
358  // need to namespace the query with the callback and single result request.
359  // https://github.com/elgg/elgg/issues/4049
360  $query_id = (int)$single . $query . '|';
361  if ($params) {
362  $query_id .= serialize($params) . '|';
363  }
364 
365  if ($callback) {
366  if (!is_callable($callback)) {
367  $inspector = new \Elgg\Debug\Inspector();
368  throw new \RuntimeException('$callback must be a callable function. Given ' . $inspector->describeCallable($callback));
369  }
370  $query_id .= $this->fingerprintCallback($callback);
371  }
372  // MD5 yields smaller mem usage for cache and cleaner logs
373  $hash = md5($query_id);
374 
375  // Is cached?
376  if ($this->queryCache) {
377  if (isset($this->queryCache[$hash])) {
378  if ($this->logger) {
379  // TODO add params in $query here
380  $this->logger->info("DB query $query results returned from cache (hash: $hash)");
381  }
382  return $this->queryCache[$hash];
383  }
384  }
385 
386  $return = array();
387 
388  $stmt = $this->executeQuery($query, $this->getConnection('read'), $params);
389  while ($row = $stmt->fetch()) {
390  if ($callback) {
391  $row = call_user_func($callback, $row);
392  }
393 
394  if ($single) {
395  $return = $row;
396  break;
397  } else {
398  $return[] = $row;
399  }
400  }
401 
402  // Cache result
403  if ($this->queryCache) {
404  $this->queryCache[$hash] = $return;
405  if ($this->logger) {
406  // TODO add params in $query here
407  $this->logger->info("DB query $query results cached (hash: $hash)");
408  }
409  }
410 
411  return $return;
412  }
413 
427  protected function executeQuery($query, Connection $connection, array $params = []) {
428  if ($query == null) {
429  throw new \DatabaseException("Query cannot be null");
430  }
431 
432  $this->queryCount++;
433 
434  if ($this->timer) {
435  $timer_key = preg_replace('~\\s+~', ' ', trim($query . '|' . serialize($params)));
436  $this->timer->begin(['SQL', $timer_key]);
437  }
438 
439  try {
440  if ($params) {
441  $value = $connection->executeQuery($query, $params);
442  } else {
443  // faster
444  $value = $connection->query($query);
445  }
446  } catch (\Exception $e) {
447  throw new \DatabaseException($e->getMessage() . "\n\n QUERY: $query");
448  }
449 
450  if ($this->timer) {
451  $this->timer->end(['SQL', $timer_key]);
452  }
453 
454  return $value;
455  }
456 
482  public function runSqlScript($scriptlocation) {
483  $script = file_get_contents($scriptlocation);
484  if ($script) {
485 
486  $errors = array();
487 
488  // Remove MySQL '-- ' and '# ' style comments
489  $script = preg_replace('/^(?:--|#) .*$/m', '', $script);
490 
491  // Statements must end with ; and a newline
492  $sql_statements = preg_split('/;[\n\r]+/', "$script\n");
493 
494  foreach ($sql_statements as $statement) {
495  $statement = trim($statement);
496  $statement = str_replace("prefix_", $this->tablePrefix, $statement);
497  if (!empty($statement)) {
498  try {
499  $this->updateData($statement);
500  } catch (\DatabaseException $e) {
501  $errors[] = $e->getMessage();
502  }
503  }
504  }
505  if (!empty($errors)) {
506  $errortxt = "";
507  foreach ($errors as $error) {
508  $errortxt .= " {$error};";
509  }
510 
511  $msg = "There were a number of issues: " . $errortxt;
512  throw new \DatabaseException($msg);
513  }
514  } else {
515  $msg = "Elgg couldn't find the requested database script at " . $scriptlocation . ".";
516  throw new \DatabaseException($msg);
517  }
518  }
519 
534  public function registerDelayedQuery($query, $type, $callback = null, array $params = []) {
535  if ($type != 'read' && $type != 'write') {
536  return false;
537  }
538 
539  $this->delayedQueries[] = [
540  self::DELAYED_QUERY => $query,
541  self::DELAYED_TYPE => $type,
542  self::DELAYED_HANDLER => $callback,
543  self::DELAYED_PARAMS => $params,
544  ];
545 
546  return true;
547  }
548 
557  public function executeDelayedQueries() {
558 
559  foreach ($this->delayedQueries as $set) {
560  $query = $set[self::DELAYED_QUERY];
561  $type = $set[self::DELAYED_TYPE];
562  $handler = $set[self::DELAYED_HANDLER];
563  $params = $set[self::DELAYED_PARAMS];
564 
565  try {
566 
567  $stmt = $this->executeQuery($query, $this->getConnection($type), $params);
568 
569  if (is_callable($handler)) {
570  call_user_func($handler, $stmt);
571  }
572  } catch (\Exception $e) {
573  if ($this->logger) {
574  // Suppress all exceptions since page already sent to requestor
575  $this->logger->error($e);
576  }
577  }
578  }
579  }
580 
589  public function enableQueryCache() {
590  if ($this->config->isQueryCacheEnabled() && $this->queryCache === null) {
591  // @todo if we keep this cache, expose the size as a config parameter
592  $this->queryCache = new \Elgg\Cache\LRUCache($this->queryCacheSize);
593  }
594  }
595 
605  public function disableQueryCache() {
606  $this->queryCache = null;
607  }
608 
614  protected function invalidateQueryCache() {
615  if ($this->queryCache) {
616  $this->queryCache->clear();
617  if ($this->logger) {
618  $this->logger->info("Query cache invalidated");
619  }
620  }
621  }
622 
630  public function assertInstalled() {
631 
632  if ($this->installed) {
633  return;
634  }
635 
636  try {
637  $sql = "SELECT value FROM {$this->tablePrefix}datalists WHERE name = 'installed'";
638  $this->getConnection('read')->query($sql);
639  } catch (\DatabaseException $e) {
640  throw new \InstallationException("Unable to handle this request. This site is not configured or the database is down.");
641  }
642 
643  $this->installed = true;
644  }
645 
652  public function getQueryCount() {
653  return $this->queryCount;
654  }
655 
661  public function getTablePrefix() {
662  return $this->tablePrefix;
663  }
664 
673  public function sanitizeInt($value, $signed = true) {
674  $value = (int) $value;
675 
676  if ($signed === false) {
677  if ($value < 0) {
678  $value = 0;
679  }
680  }
681 
682  return $value;
683  }
684 
693  public function sanitizeString($value) {
694  $quoted = $this->getConnection('read')->quote($value);
695  if ($quoted[0] !== "'" || substr($quoted, -1) !== "'") {
696  throw new \DatabaseException("PDO::quote did not return surrounding single quotes.");
697  }
698  return substr($quoted, 1, -1);
699  }
700 
709  public function getServerVersion($type) {
710  $driver = $this->getConnection($type)->getWrappedConnection();
711  if ($driver instanceof ServerInfoAwareConnection) {
712  return $driver->getServerVersion();
713  }
714 
715  return null;
716  }
717 }
updateData($query, $get_num_rows=false, array $params=[])
Update the database.
Definition: Database.php:271
if(!$owner||!($owner instanceof ElggUser)||!$owner->canEdit()) $error
Definition: upload.php:14
setLogger(Logger $logger)
Set the logger object.
Definition: Database.php:107
getQueryCount()
Get the number of queries made to the database.
Definition: Database.php:652
__construct(\Elgg\Database\Config $config,\Elgg\Logger $logger=null)
Constructor.
Definition: Database.php:90
invalidateQueryCache()
Invalidate the query cache.
Definition: Database.php:614
$e
Definition: metadata.php:12
$CONFIG installed
Is the site fully installed.
Definition: config.php:58
$value
Definition: longtext.php:26
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:427
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:673
getData($query, $callback=null, array $params=[])
Retrieve rows from the database.
Definition: Database.php:209
sanitizeString($value)
Sanitizes a string for use in a query.
Definition: Database.php:693
assertInstalled()
Test that the Elgg database is installed.
Definition: Database.php:630
runSqlScript($scriptlocation)
Runs a full database script from disk.
Definition: Database.php:482
registerDelayedQuery($query, $type, $callback=null, array $params=[])
Queue a query for execution upon shutdown.
Definition: Database.php:534
fingerprintCallback($callback)
Get a string that uniquely identifies a callback during the current request.
Definition: Database.php:325
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:355
connect($type="readwrite")
Establish a connection to the database server.
Definition: Database.php:160
deleteData($query, array $params=[])
Delete data from the database.
Definition: Database.php:298
getConnection($type)
Gets (if required, also creates) a DB connection.
Definition: Database.php:119
disableQueryCache()
Disable the query cache.
Definition: Database.php:605
enableQueryCache()
Enable the query cache.
Definition: Database.php:589
getDataRow($query, $callback=null, array $params=[])
Retrieve a single row from the database.
Definition: Database.php:227
$row
getServerVersion($type)
Get the server version number.
Definition: Database.php:709
$handler
Definition: add.php:10
executeDelayedQueries()
Trigger all queries that were registered as "delayed" queries.
Definition: Database.php:557
setupConnections()
Establish database connections.
Definition: Database.php:140
insertData($query, array $params=[])
Insert a row into the database.
Definition: Database.php:243
getTablePrefix()
Get the prefix for Elgg's tables.
Definition: Database.php:661
if(!$display_name) $type
Definition: delete.php:27