PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/DatabaseHandle.php

https://gitlab.com/tomgidden/php-DatabaseObject
PHP | 375 lines | 222 code | 67 blank | 86 comment | 44 complexity | a394a933590af45442b3bee733cd7aae MD5 | raw file
  1. <?php
  2. /* DatabaseHandle
  3. * by Tom Gidden <tom@gidden.net>
  4. * Copyright (C) 2009, Tom Gidden
  5. */
  6. require_once('DatabaseObject/common/vars.php');
  7. class ConnectionException extends Exception {};
  8. class QueryErrorException extends Exception {};
  9. class ResultsErrorException extends Exception {};
  10. class DatabaseHandle {
  11. /*
  12. * This class wrappers the PDO DB code, offering a singleton (for any
  13. * given DSN) and basic prepared query caching. It also takes the
  14. * opportunity to profile the queries using the TIMINGS constant to
  15. * activate.
  16. *
  17. * The prepared query caching may not be necessary, as I think DB.php
  18. * does this itself.
  19. *
  20. * To obtain a database handle, call:
  21. *
  22. * $dbh = DatabaseHandle::get_instance()
  23. */
  24. private static $instances = array(); // One DatabaseHandle per DSN
  25. protected $cache = array(); // The prepared query cache
  26. protected $sqllog = array(); // Log of queries run through this handle
  27. protected $dbh; // The DB handle itself
  28. protected $lastRowCount;
  29. private function __construct($dsn, $username, $password) {
  30. // This is effectively a singleton (per DSN), so the constructor is
  31. // private.
  32. // May throw an exception.
  33. $this->dbh = new PDO($dsn, $username, $password);
  34. }
  35. private function __clone() {}
  36. public static function get_instance($dsn=null) {
  37. // Get the DatabaseHandle for a DSN, creating one if necessary.
  38. //
  39. // Call: $dbh = DatabaseHandle::get_instance($dsn);
  40. // If there is no DSN, and there's a global one defined, use it.
  41. if(is_null($dsn))
  42. if(defined('DSN'))
  43. $dsn = DSN;
  44. else
  45. return null;
  46. if(defined('DSN_USERNAME')) {
  47. $dsn .= ':user='.DSN_USERNAME;
  48. $username = DSN_USERNAME;
  49. }
  50. else
  51. $username = null;
  52. if(defined('DSN_PASSWORD')) {
  53. $dsn .= ':password='.DSN_PASSWORD;
  54. $password = DSN_PASSWORD;
  55. }
  56. else
  57. $password = null;
  58. // If the DatabaseHandle already exists, return it.
  59. if(isset(self::$instances[$dsn]))
  60. return self::$instances[$dsn];
  61. // Otherwise create it and return it.
  62. return self::$instances[$dsn] = new DatabaseHandle(DSN, $username, $password);
  63. }
  64. protected function prepare($sql) {
  65. // Prepares a 'prepared statement', using the PDO prepare() call. In
  66. // addition, it will also cache that query. I can't tell if PDO has
  67. // this functionality, but it doesn't seem to be documented.
  68. // Anyway, the caching is relatively low-cost, so might as well do it
  69. // here.
  70. if(isset($this->cache[$sql]))
  71. return $this->cache[$sql];
  72. return $this->cache[$sql] = $this->dbh->prepare($sql);
  73. }
  74. public function query($sql, $params=null, $with_exceptions=false) {
  75. // Executes a query, using prepared statements. It will return the
  76. // DB_result or DB_Error object. Parameters should be passed in an
  77. // array, as a single scalar if there's only one, or null if there are
  78. // no placeholders.
  79. // Log the start time of the query
  80. if(defined('TIMINGS') and TIMINGS) {
  81. list($usec, $sec) = explode(" ",microtime());
  82. $start = ((float)$usec + (float)$sec);
  83. }
  84. // $r: the result object
  85. $r = null;
  86. // $msg: the error message, if any
  87. $msg = null;
  88. try {
  89. // Prepare the query
  90. $q = $this->prepare($sql);
  91. // The query should be repeated if a deadlock or failed connect
  92. // occurs, so $c counts the number of attempts.
  93. $c=0;
  94. do {
  95. // If this is not the first time around (ie. a deadlock), then sleep
  96. if($c++) sleep($c*DATABASE_RETRY_WAIT);
  97. // Execute the query, binding parameters if necessary.
  98. try {
  99. if(isset($params))
  100. if(is_array($params))
  101. $r = $q->execute($params);
  102. else
  103. $r = $q->execute(array($params));
  104. else
  105. $r = $q->execute();
  106. }
  107. catch (Exception $e) {
  108. $this->sqllog[] = array(false, 0, 'Exception: '.$msg->getMessage(), $sql);
  109. throw $e;
  110. }
  111. // $retry will be set to true if the query needs to be retried.
  112. $retry = false;
  113. // XXX: This bit needs fixin'
  114. //
  115. if(!$r) {
  116. if($q->errorCode() == '00000') {
  117. $retry = false; // All okay, but just no results.
  118. }
  119. else {
  120. $msg = $this->errorMessage($q);
  121. $retry = false;
  122. }
  123. }
  124. /*
  125. // If there's an error in the result set (NOT the query prepare),
  126. // then if it's a deadlock or a failed connect, then do a retry.
  127. if(!$r) {
  128. // switch($q->errorCode()) {
  129. // case DB_ERROR_NOT_LOCKED:
  130. // case DB_ERROR_CONNECT_FAILED: // XXX?
  131. $retry = true;
  132. break;
  133. }
  134. }
  135. */
  136. // This will only loop if the query failed softly
  137. } while($retry and $c<=DATABASE_RETRY_LIMIT);
  138. }
  139. catch (PDOException $e) {
  140. $msg = $e->getMessage();
  141. }
  142. // Remove the stupid SQL dump at the start of the message
  143. if(!is_null($msg))
  144. if(preg_match('/^.*?\[nativecode=([^\]]+)\]$/', $msg, $parts))
  145. $msg = $parts[1];
  146. // Log the end time of the query, and the error if necessary.
  147. if(defined('TIMINGS') and TIMINGS) {
  148. list($usec, $sec) = explode(" ",microtime());
  149. $end = ((float)$usec + (float)$sec);
  150. // If $msg is still null, then everything's fine
  151. if(is_null($msg)) {
  152. $this->sqllog[] = array(true, $end-$start, $sql, $params, null);
  153. return $q;
  154. } else
  155. $this->sqllog[] = array(false, $end-$start, $sql, $params, $msg);
  156. }
  157. if(!is_null($msg)) {
  158. if(!$q)
  159. if($with_exceptions)
  160. throw new QueryErrorException($msg);
  161. else
  162. return $q;
  163. else if(!$r)
  164. if($with_exceptions)
  165. throw new ResultsErrorException($msg);
  166. else
  167. return $q;
  168. else
  169. return $q;
  170. }
  171. else {
  172. $this->lastRowCount = $q->rowCount();
  173. return $q;
  174. }
  175. }
  176. public function add_to_log($text, $time, $success=true, $msg=null) {
  177. $this->sqllog[] = array($success, $time, $text, null, $msg);
  178. }
  179. public function query_with_exceptions($sql, &$params=null) {
  180. // Executes a query as with ::query(), but with an exception being
  181. // thrown if there's an SQL error.
  182. return $this->query($sql, $params, true);
  183. }
  184. public function html($sep="\n") {
  185. // Serialise to HTML. Almost widgety.
  186. return "<!--".$sep.$this->text($sep).$sep."-->".$sep;
  187. }
  188. public function affectedRows() {
  189. return $this->lastRowCount;
  190. }
  191. public function autoCommit($bool) {
  192. // Log the start time of the query
  193. if(defined('TIMINGS') and TIMINGS) {
  194. list($usec, $sec) = explode(" ",microtime());
  195. $start = ((float)$usec + (float)$sec);
  196. }
  197. if($bool) {
  198. if(!($this->dbh->getAttribute(constant('PDO::ATTR_AUTOCOMMIT')))) {
  199. $this->sqllog[] = array(false, 0, 'Weird rollback', '');
  200. $this->dbh->rollBack();
  201. }
  202. // XXX: Erk. This whole thing is really thanks to PEAR DB, and it
  203. // doesn't map well to PDO.
  204. }
  205. else {
  206. $x = $this->dbh->beginTransaction();
  207. }
  208. // Log the end time of the query, and the error if necessary.
  209. if(defined('TIMINGS') and TIMINGS) {
  210. list($usec, $sec) = explode(" ",microtime());
  211. $end = ((float)$usec + (float)$sec);
  212. // If $x, then everything's fine
  213. if(!$x) {
  214. $msg = $this->errorMessage();
  215. $this->sqllog[] = array(false, $end-$start, 'Autocommit '.($bool?'true':'false'), $msg);
  216. } else
  217. $this->sqllog[] = array(true, $end-$start, 'Autocommit '.($bool?'true':'false'), null, null);
  218. }
  219. return $x;
  220. }
  221. public function rollback() {
  222. // Log the start time of the query
  223. if(defined('TIMINGS') and TIMINGS) {
  224. list($usec, $sec) = explode(" ",microtime());
  225. $start = ((float)$usec + (float)$sec);
  226. }
  227. $x = $this->dbh->rollBack();
  228. // Log the end time of the query, and the error if necessary.
  229. if(defined('TIMINGS') and TIMINGS) {
  230. list($usec, $sec) = explode(" ",microtime());
  231. $end = ((float)$usec + (float)$sec);
  232. // If $x, then everything's fine
  233. if(!$x) {
  234. $msg = $this->errorMessage();
  235. $this->sqllog[] = array(false, $end-$start, 'Rollback', $msg);
  236. } else
  237. $this->sqllog[] = array(true, $end-$start, 'Rollback', null, null);
  238. }
  239. return $x;
  240. }
  241. public function commit() {
  242. // Log the start time of the query
  243. if(defined('TIMINGS') and TIMINGS) {
  244. list($usec, $sec) = explode(" ",microtime());
  245. $start = ((float)$usec + (float)$sec);
  246. }
  247. $x = $this->dbh->commit();
  248. // Log the end time of the query, and the error if necessary.
  249. if(defined('TIMINGS') and TIMINGS) {
  250. list($usec, $sec) = explode(" ",microtime());
  251. $end = ((float)$usec + (float)$sec);
  252. // If $x, then everything's fine
  253. if(!$x) {
  254. $msg = $this->errorMessage();
  255. $this->sqllog[] = array(false, $end-$start, 'Commit', $msg);
  256. } else
  257. $this->sqllog[] = array(true, $end-$start, 'Commit', null, null);
  258. }
  259. return $x;
  260. }
  261. public function nextId($seq) {
  262. throw new Exception("nextId is not currently implemented");
  263. return $this->dbh->lastInsertId($seq)+1;
  264. }
  265. public function lastInsertId() {
  266. return $this->dbh->lastInsertId();
  267. }
  268. public function errorMessage($q=null) {
  269. if($q) {
  270. return join(' ', $q->errorInfo());
  271. }
  272. else {
  273. return join(' ', $this->dbh->errorInfo());
  274. }
  275. }
  276. public function text($sep="\n") {
  277. // Serialise to plain (but formatted) text.
  278. $buf = array();
  279. $total_duration = 0;
  280. // For each executed query...
  281. foreach ($this->sqllog as $arr) {
  282. $str = '';
  283. // $arr[0] is success/failure
  284. if($arr[0])
  285. $str = 'Y ';
  286. else
  287. $str = 'N ';
  288. // $arr[1] is the duration of the query,
  289. // $arr[2] is the query SQL itself
  290. $str .= $arr[1].': '.$arr[2];
  291. // If there are parameters for the query (prepared statement), then
  292. // they're in $arr[3].
  293. if(is_array($arr[3])) // Array supplied
  294. $str .= ' ('.join(',',$arr[3]).')';
  295. else if(isset($arr[3])) // Scalar supplied
  296. $str .= ' ('.$arr[3].')';
  297. // $arr[4] contains any message from the result
  298. if(isset($arr[4]))
  299. $str .= ': '.$arr[4];
  300. // Add to output buffer
  301. $buf[] = $str;
  302. // Tot up the total duration time from the query duration
  303. $total_duration += $arr[1];
  304. }
  305. $buf[] = "Total: $total_duration";
  306. return join($sep, $buf);
  307. }
  308. };