PageRenderTime 59ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Lampcms/DB.php

https://github.com/snytkine/LampCMS
PHP | 630 lines | 256 code | 102 blank | 272 comment | 15 complexity | ade22e5d1d58271e002fec54a4d67edd MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /**
  3. *
  4. * License, TERMS and CONDITIONS
  5. *
  6. * This software is licensed under the GNU LESSER GENERAL PUBLIC LICENSE (LGPL) version 3
  7. * Please read the license here : http://www.gnu.org/licenses/lgpl-3.0.txt
  8. *
  9. * Redistribution and use in source and binary forms, with or without
  10. * modification, are permitted provided that the following conditions are met:
  11. * 1. Redistributions of source code must retain the above copyright
  12. * notice, this list of conditions and the following disclaimer.
  13. * 2. Redistributions in binary form must reproduce the above copyright
  14. * notice, this list of conditions and the following disclaimer in the
  15. * documentation and/or other materials provided with the distribution.
  16. * 3. The name of the author may not be used to endorse or promote products
  17. * derived from this software without specific prior written permission.
  18. *
  19. * ATTRIBUTION REQUIRED
  20. * 4. All web pages generated by the use of this software, or at least
  21. * the page that lists the recent questions (usually home page) must include
  22. * a link to the http://www.lampcms.com and text of the link must indicate that
  23. * the website's Questions/Answers functionality is powered by lampcms.com
  24. * An example of acceptable link would be "Powered by <a href="http://www.lampcms.com">LampCMS</a>"
  25. * The location of the link is not important, it can be in the footer of the page
  26. * but it must not be hidden by style attributes
  27. *
  28. * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
  29. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  30. * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  31. * IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
  32. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  33. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  34. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  35. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  36. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  37. * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  38. *
  39. * This product includes GeoLite data created by MaxMind,
  40. * available from http://www.maxmind.com/
  41. *
  42. *
  43. * @author Dmitri Snytkine <cms@lampcms.com>
  44. * @copyright 2005-2012 (or current year) Dmitri Snytkine
  45. * @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE (LGPL) version 3
  46. * @link http://www.lampcms.com Lampcms.com project
  47. * @version Release: @package_version@
  48. *
  49. *
  50. */
  51. namespace Lampcms;
  52. use \PDO;
  53. /**
  54. * Wrapped for PDO class
  55. * to perform common tasks
  56. *
  57. * @author Dmitri Snytkine
  58. *
  59. */
  60. class DB
  61. {
  62. protected $Ini;
  63. /**
  64. * Instance of this object
  65. * @var object
  66. */
  67. protected static $oDb = null;
  68. /**
  69. * PDO object
  70. *
  71. * @var object PDO object
  72. */
  73. protected $dbh;
  74. /**
  75. * Array of DB section
  76. * in !config.inc
  77. *
  78. * @var mixed null|array
  79. */
  80. protected $aDB = null;
  81. /**
  82. * Array to log
  83. * queries during the life
  84. * of the object
  85. * @var array
  86. */
  87. protected $aLog = array();
  88. /**
  89. * Timestamp in microseconds
  90. * this var initiated just
  91. * before the query starts
  92. * @var float
  93. */
  94. protected $ts = null;
  95. /**
  96. * Constructor
  97. *
  98. * @return object
  99. */
  100. public function __construct(\Lampcms\Config\Ini $Ini)
  101. {
  102. $this->Ini = $Ini;
  103. }
  104. protected function connect()
  105. {
  106. $sDsn = $this->makeDsn();
  107. d('$this->aDB: ' . print_r($this->aDB, 1) . ' DSN: ' . $sDsn);
  108. $aOptions = array(PDO::ATTR_PERSISTENT => false);
  109. if (isset($this->aDB['Persistent']) && (true === (bool)$this->aDB['Persistent'])) {
  110. d('Instantiating persistent connection');
  111. $aOptions[PDO::ATTR_PERSISTENT] = true;
  112. }
  113. if ('mysql' === $this->aDB['Database_type']) {
  114. $aOptions[PDO::MYSQL_ATTR_USE_BUFFERED_QUERY] = true;
  115. }
  116. d('trying to connect to database with options: ' . print_r($aOptions, true));
  117. try {
  118. $this->dbh = new PDO($sDsn, $this->aDB['Database_username'], $this->aDB['Database_password'], $aOptions);
  119. $this->dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
  120. $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  121. $this->dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
  122. /**
  123. * In order to have all queries be done in utf8 charset, uncomment
  124. * the line below
  125. * and MAKE sure the default charset in mysql tables is set to utf8
  126. * which should be done anyway, regardless...
  127. *
  128. * The query below will only set the connection (php's client lib)
  129. * to use utf8, so mysql server will know that all data coming from
  130. * php is already in utf8 and will not try to convert it into utf8
  131. *
  132. */
  133. $this->dbh->exec('SET NAMES utf8'); // now using this as connection option!
  134. } catch (\PDOException $e) {
  135. throw new DBException('Cannot connect to database: ' . $e->getMessage());
  136. }
  137. }
  138. /**
  139. * Getter of PDO dbh object
  140. * @return object of type PDO
  141. */
  142. public function getDbh()
  143. {
  144. if (!isset($this->dbh)) {
  145. $this->connect();
  146. }
  147. return $this->dbh;
  148. }
  149. public function __clone()
  150. {
  151. throw new DevException('cloning DB object not allowed');
  152. }
  153. /**
  154. * Sets the object var $this->aDB
  155. * and returns the dsn string
  156. *
  157. * @return string dsn
  158. *
  159. * @throws LampcmsIniException if
  160. * some required elements are missing
  161. * in the !config.ini file
  162. */
  163. protected function makeDsn()
  164. {
  165. $this->aDB = $this->Ini->getSection('DB');
  166. if (!isset($this->aDB['Database_username']) || !isset($this->aDB['Database_password'])) {
  167. throw new IniException('Database_username OR Database_password not set');
  168. }
  169. return $this->getDSN();
  170. }
  171. /**
  172. * Creates a dsn string from
  173. * values in DB section of ini file
  174. *
  175. * @return string DSN string
  176. * @throws LampcmsIniException if some
  177. * required values in DB section are not set
  178. */
  179. protected function getDSN()
  180. {
  181. if (empty($this->aDB['Database_name']) || empty($this->aDB['Database_host']) ||
  182. empty ($this->aDB['Database_type'])
  183. ) {
  184. throw new IniException('Cannot create dsn because some required dns params are missing: ' . print_r($this->aDB, true));
  185. }
  186. /**
  187. * LAMPCMS_TEST is the name we use in Unit Tests
  188. * If the actual name is also LAMPCMS_TEST then
  189. * Unit tests will destroy actual database during
  190. * tests. This should not be allowed!
  191. */
  192. if ('LAMPCMS_TEST' === \trim($this->aDB['Database_name'])) {
  193. throw new DevException('Reserved name! You cannot name your database ' . $this->aDB['Database_name'] . ' Please set different value of Database_name is !config.ini');
  194. }
  195. $dbhost = \strtolower($this->aDB['Database_host']);
  196. /**
  197. * Always try to use defined LAMPCMS_MYSQL_DB
  198. * This is useful in Unit testing so we can
  199. * define value for test database and not
  200. * use live database!
  201. *
  202. * @var string
  203. */
  204. $dbname = (defined('LAMPCMS_MYSQL_DB')) ? LAMPCMS_MYSQL_DB : $this->aDB['Database_name'];
  205. $ret = \strtolower($this->aDB['Database_type']) . ':host=' . $dbhost;
  206. if ('localhost' !== $dbhost) {
  207. if (empty ($this->aDB['TCP_Port_number'])) {
  208. throw new IniException('If Database_host is not "localhost" then "TCP_Port_number" MUST be defined');
  209. }
  210. $ret .= ';port=' . $this->aDB['TCP_Port_number'];
  211. }
  212. $ret .= ';dbname=' . $dbname;
  213. return $ret;
  214. }
  215. /**
  216. * This function is executing sql statement
  217. * with database
  218. * and return an resultset if it executed
  219. * successfully or log message on error
  220. *
  221. * @param string $strSql Sql Statement
  222. * @param string $strErr2 optional string
  223. * for additional logging.
  224. * usually used to pass the information
  225. * with class name, line from
  226. * where this function was called.
  227. *
  228. * @param string $fetchmode the MDB2
  229. * class-specific fetchmode
  230. * @param boolean $rekey turn the result
  231. * array into associative array
  232. * where the first value
  233. * (result of first column in select)
  234. * becomes an array key.
  235. *
  236. * @param boolean $force_array
  237. * @param boolean $group
  238. *
  239. * @return array with one mysql
  240. * row per element,
  241. * each row is an associative
  242. * array or empty array
  243. */
  244. public function getQueryResult($strSql, $strErr2 = '', $types = null, $m = PDO::FETCH_ASSOC,
  245. $rekey = false, $force_array = false, $group = false)
  246. {
  247. $aRes = array();
  248. if (true === $force_array) {
  249. return $this->getKeyVal($strSql, $strErr2);
  250. } elseif ($rekey) {
  251. return $this->getRekeyed($strSql, $strErr2);
  252. }
  253. try {
  254. $aRes = $this->initTimer()->getDbh()->query($strSql, $m)->fetchAll();
  255. $this->logQuery($strSql);
  256. } catch (\PDOException $e) {
  257. d('Line: ' . $e->getLine() . ' PDO Error: ' . $e->getMessage() . ' ERROR: ' . print_r($e->errorInfo, true) .
  258. "\nSQL Error code: " . $e->getCode() . ' called from ' . $strErr2);
  259. $aRes = array();
  260. }
  261. return $aRes;
  262. }
  263. /**
  264. * Return result of
  265. * sql query where only 2
  266. * columns are returns in a form
  267. * of an associative array where the
  268. * value of the first column is key
  269. * and value of second column is value
  270. * in the result array
  271. *
  272. * @return mixed array associative array
  273. * or false if did not work. Most likely
  274. * reason would be is when the result of
  275. * sql query contains other than 2 columns
  276. *
  277. * @param object $sql
  278. * @param string $sql the sql to execute
  279. */
  280. public function getKeyVal($sql, $err = '')
  281. {
  282. $aRes = array();
  283. try {
  284. $aRes = $this->initTimer()->getDbh()->query($sql)->fetchAll(PDO::FETCH_KEY_PAIR);
  285. $this->logQuery($sql);
  286. } catch (\PDOException $e) {
  287. d('Line: ' . $e->getLine() . ' PDO Error: ' . $e->getMessage() . ' ERROR: ' . print_r($e->errorInfo, 1) .
  288. "\nSQL Error code: " . $e->getCode() . ' called from ' . $err);
  289. throw new DevException('failed to fetch result using PDO::FETCH_KEY_PAIR. Error: ' . $e->getMessage() . ' Called from ' . $err);
  290. }
  291. return $aRes;
  292. }
  293. /**
  294. * Get the result of fetch All
  295. * where the array key is the value of
  296. * the first column
  297. * and array value is array with key [0]
  298. * under which the result is the array
  299. * of the rest of the values.
  300. *
  301. * @return
  302. * @param string $sql
  303. * @param string $err extra string
  304. * for logging/debugging
  305. */
  306. public function getRekeyed($sql, $strErr2 = '')
  307. {
  308. $aRes = array();
  309. try {
  310. $aRes = $this->initTimer()->getDbh()->query($sql)->fetchAll(PDO::FETCH_GROUP);
  311. $this->logQuery($sql);
  312. } catch (\PDOException $e) {
  313. d('Line: ' . $e->getLine() . ' PDO Error: ' . $e->getMessage() . ' ERROR: ' . print_r($e->errorInfo, true) .
  314. "\nSQL Error code: " . $e->getCode() . ' called from ' . $strErr2);
  315. }
  316. return $aRes;
  317. }
  318. /**
  319. * Fetch the first column from
  320. * the first row in the result set
  321. *
  322. * @param string $strSql
  323. *
  324. * @param string $strErr2 optional string for additional logging.
  325. * usually used to pass the information with class name, line from
  326. * where this function was called.
  327. *
  328. * @return mixed. On success a single record from a single row.
  329. * For example 'select userid from USER where id=121
  330. * will return a nickname of the user with id 121
  331. * On failure it returns
  332. * false
  333. *
  334. */
  335. public function fetchOne($strSql, $strErr2 = '')
  336. {
  337. $ret = false;
  338. d('sql: ' . $strSql);
  339. try {
  340. $sth = $this->initTimer()->getDbh()->prepare($strSql);
  341. $sth->execute();
  342. $ret = $sth->fetchColumn();
  343. $sth = null;
  344. $this->logQuery($strSql);
  345. d('$ret: ' . $ret);
  346. } catch (\PDOException $e) {
  347. d('Line: ' . $e->getLine() . ' PDO Error: ' . $e->getMessage() . ' ERROR: ' . print_r($e->errorInfo,
  348. true) . "\nSQL Error code: " . $e->getCode() . ' called from ' . $strErr2);
  349. }
  350. return $ret;
  351. }
  352. /**
  353. * Set the value of $this->ts
  354. * to the current time in milliseconds
  355. *
  356. * @return object $this
  357. */
  358. protected function initTimer()
  359. {
  360. $this->ts = microtime(true);
  361. return $this;
  362. }
  363. public function execPrepared(\PDOStatement $sth)
  364. {
  365. $this->initTimer();
  366. try {
  367. $ret = $sth->execute();
  368. } catch (\PDOException $e) {
  369. $arr = $sth->errorInfo();
  370. $err = 'Error executing sth: ' . $e->getMessage() . ' PDOException errorInfo: ' . print_r($e->errorInfo, true) . ' sth errorInfo: ' . print_r($arr, 1);
  371. e($err);
  372. throw new DevException($err);
  373. }
  374. $endTs = microtime(true);
  375. $sql = 'Executed prepared statement ';
  376. if (true === LAMPCMS_DEBUG) {
  377. ob_start();
  378. $sth->debugDumpParams();
  379. $sql .= "\r\n" . ob_get_clean();
  380. }
  381. /**
  382. * New on Dec 31, 09
  383. */
  384. $sth = null;
  385. unset($sth);
  386. $this->logQuery($sql, $endTs);
  387. return $ret;
  388. }
  389. /**
  390. * Add query to $this->aLog array
  391. *
  392. * @return object $this
  393. * @param string $sql
  394. */
  395. protected function logQuery($sql, $endTs = null)
  396. {
  397. if (null === $this->ts) {
  398. throw new DevException('valus of $this->ts was not set. Unable to log query');
  399. }
  400. $endTs = (null === $endTs) ? microtime(true) : $endTs;
  401. $this->aLog[] = array('sql' => $sql, 'ts' => ($endTs - $this->ts));
  402. $this->ts = null;
  403. return $this;
  404. }
  405. /**
  406. * Getter for $this->aLog
  407. * @return array $this->aLog
  408. */
  409. public function getDebugLog()
  410. {
  411. return $this->aLog;
  412. }
  413. /**
  414. *
  415. * @return string a debug output
  416. * with info about queries
  417. *
  418. * @param bool $asHTML[optional]
  419. * if true, then converts line feeds
  420. * to <br>
  421. */
  422. public function dumpLog($asHTML = false)
  423. {
  424. $intTotalTime = 0;
  425. $numQueries = count($this->aLog);
  426. $ret = "\r\n" . 'SQL data: ' . "\r\n";
  427. $ret .= "\r\n" . "\r\n" . 'Total queries in this page: ';
  428. $ret .= $numQueries . "\r\n";
  429. arsort($this->aLog);
  430. foreach ($this->aLog as $aVal) {
  431. $strQuery = $aVal['sql'];
  432. $timeExec = (float)$aVal['ts'];
  433. $ret .= '<pre>Query ' . $intQuery . ': ' . wordwrap($strQuery, 60) . "\r\n" . '</pre>SQL Execution time: ' . $timeExec . "\r\n";
  434. $intTotalTime += $timeExec;
  435. }
  436. $ret .= '<strong>Total of ' . $numQueries . ' queries executed in: ' . $intTotalTime . ' seconds</strong>' . "\r\n";
  437. return ($asHTML) ? nl2br($ret) : $ret;
  438. }
  439. /**
  440. * Creates a PDOStatement object
  441. * if it has not been already created
  442. *
  443. * @return object of type PDOStatement
  444. * @param object $key
  445. * @param object $sql
  446. * @param object $strErr2[optional]
  447. */
  448. public function makePrepared($sql, $strErr2 = '')
  449. {
  450. try {
  451. $sth = $this->initTimer()->getDbh()->prepare($sql);
  452. $this->logQuery($sql, microtime(true));
  453. } catch (\PDOException $e) {
  454. e('Line: ' . $e->getLine() . ' PDO Error: ' . $e->getMessage() . ' ERROR: ' . print_r($e->errorInfo,
  455. true) . "\nSQL Error code: " . $e->getCode() . ' called from ' . $strErr2);
  456. }
  457. return $sth;
  458. }
  459. /**
  460. * This function will fetch last inserted id
  461. *
  462. * @return int $intId last inserted id
  463. */
  464. public function getLastInsertId()
  465. {
  466. $ret = false;
  467. $ret = $this->getDbh()->lastInsertId();
  468. return $ret;
  469. } // end getLastInsertId
  470. /**
  471. * Just directly execute a query
  472. * this is usefull for update, insert and delete queries
  473. *
  474. * @param string $strSql sql to execute
  475. *
  476. * @param string $strErr2 an extra string to add to log
  477. *
  478. * @return int count of affected rows
  479. */
  480. public function exec($strSql, $strErr2 = '')
  481. {
  482. $count = 0;
  483. try {
  484. $count = $this->getDbh()->exec($strSql);
  485. } catch (\PDOException $e) {
  486. $err = ('Line: ' . $e->getLine() .
  487. ' PDO Error: ' . $e->getMessage() .
  488. ' ERROR: ' . print_r($e->errorInfo, true) .
  489. "\nSQL Error code: " . $e->getCode() . ' called from ' . $strErr2);
  490. throw new \Lampcms\DevException($err);
  491. }
  492. return $count;
  493. }
  494. /**
  495. * Returns associative array where
  496. * keys are column names and
  497. * values are default values
  498. *
  499. * @param string $strTableName
  500. *
  501. * @return array assosiative array
  502. */
  503. public function getTableColumns($strTableName, $strErr2 = '')
  504. {
  505. $strTableName = filter_var($strTableName, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
  506. $strTableName = str_replace(';', '', $strTableName);
  507. $strTableName = addslashes($strTableName);
  508. $strSql = "SHOW FULL COLUMNS FROM $strTableName";
  509. try {
  510. $stmt = $this->initTimer()->getDbh()->prepare($strSql);
  511. $stmt->execute();
  512. $stmt->bindColumn(1, $name);
  513. $stmt->bindColumn('default', $val);
  514. $aRes = array();
  515. while ($row = $stmt->fetch(PDO::FETCH_BOUND)) {
  516. $aRes[$name] = $val;
  517. }
  518. $this->logQuery($strSql);
  519. d('$aRes: ' . print_r($aRes, true));
  520. } catch (\PDOException $e) {
  521. $message = 'Line: ' . $e->getLine() . ' PDO Error: ' . $e->getMessage() . ' ERROR: ' . print_r($e->errorInfo, true) .
  522. "\nSQL Error code: " . $e->getCode() . ' called from ' . $strErr2;
  523. e($message);
  524. return false;
  525. }
  526. return $aRes;
  527. }
  528. }