PageRenderTime 33ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/Ip/Db.php

https://gitlab.com/x33n/ImpressPages
PHP | 642 lines | 387 code | 79 blank | 176 comment | 54 complexity | 742b76d5e540aa34aef57972cc595054 MD5 | raw file
  1. <?php
  2. /**
  3. *
  4. * @package ImpressPages
  5. *
  6. */
  7. namespace Ip;
  8. /**
  9. * Database connector
  10. *
  11. */
  12. class Db
  13. {
  14. /**
  15. * @var \PDO
  16. */
  17. protected $pdoConnection;
  18. protected $tablePrefix;
  19. public function __construct($pdo = null)
  20. {
  21. if ($pdo) {
  22. $this->pdoConnection = $pdo;
  23. } else {
  24. $this->getConnection();
  25. }
  26. }
  27. /**
  28. * Get database connection object
  29. *
  30. * @throws \Ip\Exception\Db
  31. * @return \PDO
  32. */
  33. public function getConnection()
  34. {
  35. if ($this->pdoConnection) {
  36. return $this->pdoConnection;
  37. }
  38. $dbConfig = ipConfig()->get('db');
  39. ipConfig()->set('db', null);
  40. if (empty($dbConfig)) {
  41. throw new \Ip\Exception\Db("Can't connect to database. No connection config found or \\Ip\\Db::disconnect() has been used.");
  42. }
  43. try {
  44. if (array_key_exists('driver', $dbConfig) && $dbConfig['driver'] == 'sqlite') {
  45. $dsn = 'sqlite:' . $dbConfig['database'];
  46. $this->pdoConnection = new \PDO($dsn);
  47. $this->pdoConnection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
  48. } else {
  49. $dsn = 'mysql:host=' . str_replace(':', ';port=', $dbConfig['hostname']);
  50. if (!empty($dbConfig['database'])) {
  51. $dsn .= ';dbname=' . $dbConfig['database'];
  52. }
  53. $this->pdoConnection = new \PDO($dsn, $dbConfig['username'], $dbConfig['password']);
  54. $this->pdoConnection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
  55. $dt = new \DateTime();
  56. $offset = $dt->format("P");
  57. $this->pdoConnection->exec("SET time_zone='$offset';");
  58. $this->pdoConnection->exec("SET CHARACTER SET " . $dbConfig['charset']);
  59. }
  60. } catch (\PDOException $e) {
  61. throw new \Ip\Exception\Db("Can't connect to database. Stack trace hidden for security reasons");
  62. //PHP traces all details of error including DB password. This could be a disaster on live server. So we hide that data.
  63. }
  64. $this->tablePrefix = $dbConfig['tablePrefix'];
  65. return $this->pdoConnection;
  66. }
  67. public function setConnection($connection)
  68. {
  69. $this->pdoConnection = $connection;
  70. }
  71. /**
  72. * Disconnect from the database
  73. */
  74. public function disconnect()
  75. {
  76. $this->pdoConnection = null;
  77. }
  78. /**
  79. * Execute SQL query and fetch a value from the result set.
  80. *
  81. * @param string $sql
  82. * @param array $params
  83. * @return string|null
  84. * @throws \Ip\Exception\Db
  85. */
  86. public function fetchValue($sql, $params = array())
  87. {
  88. try {
  89. $query = $this->getConnection()->prepare($sql . " LIMIT 1");
  90. foreach ($params as $key => $value) {
  91. if (is_bool($value)) {
  92. $value = $value ? 1 : 0;
  93. }
  94. $query->bindValue(is_numeric($key) ? $key + 1 : $key, $value);
  95. }
  96. $query->execute();
  97. $result = $query->fetchColumn(0);
  98. return $result === false ? null : $result;
  99. } catch (\PDOException $e) {
  100. throw new \Ip\Exception\Db($e->getMessage(), $e->getCode(), $e);
  101. }
  102. }
  103. /**
  104. * Execute SQL query and fetch a single row from the result set
  105. *
  106. * @param $sql
  107. * @param array $params
  108. * @return array|null
  109. * @throws \Ip\Exception\Db
  110. */
  111. public function fetchRow($sql, $params = array())
  112. {
  113. try {
  114. $query = $this->getConnection()->prepare($sql . " LIMIT 1");
  115. foreach ($params as $key => $value) {
  116. if (is_bool($value)) {
  117. $value = $value ? 1 : 0;
  118. }
  119. $query->bindValue(is_numeric($key) ? $key + 1 : $key, $value);
  120. }
  121. $query->execute();
  122. $result = $query->fetchAll(\PDO::FETCH_ASSOC);
  123. return $result ? $result[0] : null;
  124. } catch (\PDOException $e) {
  125. throw new \Ip\Exception\Db($e->getMessage(), $e->getCode(), $e);
  126. }
  127. }
  128. /**
  129. * Execute SQL query and fetch all query results
  130. *
  131. * @param $sql
  132. * @param array $params
  133. * @return array
  134. * @throws \Ip\Exception\Db
  135. */
  136. public function fetchAll($sql, $params = array())
  137. {
  138. try {
  139. $query = $this->getConnection()->prepare($sql);
  140. foreach ($params as $key => $value) {
  141. if (is_bool($value)) {
  142. $value = $value ? 1 : 0;
  143. }
  144. $query->bindValue(is_numeric($key) ? $key + 1 : $key, $value);
  145. }
  146. $query->execute();
  147. $result = $query->fetchAll(\PDO::FETCH_ASSOC);
  148. return $result ? $result : array();
  149. } catch (\PDOException $e) {
  150. throw new \Ip\Exception\Db($e->getMessage(), $e->getCode(), $e);
  151. }
  152. }
  153. /**
  154. * Execute SELECT query on specified table and return array with results
  155. *
  156. * @param string $table Table name without prefix
  157. * @param array|string $columns list of columns or string. For example array('id', 'name') or '*'.
  158. * @param array $where Conditional array. For example array('id' => 20)
  159. * @param string $sqlEnd SQL string appended at the end of the query. For example 'ORDER BY `createdAt` DESC'
  160. * @return array
  161. */
  162. public function selectAll($table, $columns, $where = array(), $sqlEnd = '')
  163. {
  164. if (is_array($columns)) {
  165. $columns = '`' . implode('`,`', $columns) . '`';
  166. }
  167. $sql = 'SELECT ' . $columns . ' FROM ' . ipTable($table);
  168. $params = array();
  169. $sql .= ' WHERE ';
  170. if ($where) {
  171. foreach ($where as $column => $value) {
  172. if ($value === null) {
  173. $sql .= "`{$column}` IS NULL AND ";
  174. } else {
  175. if (is_bool($value)) {
  176. $value = $value ? 1 : 0;
  177. }
  178. $sql .= "`{$column}` = ? AND ";
  179. $params[] = $value;
  180. }
  181. }
  182. $sql = substr($sql, 0, -4);
  183. } else {
  184. $sql .= ' 1 ';
  185. }
  186. if ($sqlEnd) {
  187. $sql .= $sqlEnd;
  188. }
  189. return $this->fetchAll($sql, $params);
  190. }
  191. /**
  192. * Execute SELECT query and return a single row
  193. * @see self::selectAll()
  194. *
  195. * @param string $table Table name without prefix
  196. * @param array|string $columns List of columns as array or string. For example array('id', 'name') or '*'.
  197. * @param array $where Conditional array. For example array('id' => 20)
  198. * @param string $sqlEnd SQL string appended at the end of the query. For example 'ORDER BY `createdAt` DESC'
  199. * @return array|null
  200. */
  201. public function selectRow($table, $columns, $where, $sqlEnd = '')
  202. {
  203. $result = $this->selectAll($table, $columns, $where, $sqlEnd . ' LIMIT 1');
  204. return $result ? $result[0] : null;
  205. }
  206. /**
  207. * Execute SELECT query and return a single value
  208. *
  209. * @see self::selectAll()
  210. *
  211. * @param string $table Table name without prefix
  212. * @param string $column Column name. For example 'id'.
  213. * @param array $where Conditional array. For example array('id' => 20)
  214. * @param string $sqlEnd SQL string appended at the end of the query. For example 'ORDER BY `createdAt` DESC'
  215. * @return mixed|null
  216. */
  217. public function selectValue($table, $column, $where, $sqlEnd = '')
  218. {
  219. $result = $this->selectAll($table, $column, $where, $sqlEnd . ' LIMIT 1');
  220. return $result ? array_shift($result[0]) : null;
  221. }
  222. public function selectColumn($table, $column, $where, $sqlEnd = '')
  223. {
  224. $sql = 'SELECT ' . $column . ' FROM ' . ipTable($table);
  225. $params = array();
  226. $sql .= ' WHERE ';
  227. if ($where) {
  228. foreach ($where as $column => $value) {
  229. if ($value === null) {
  230. $sql .= "`{$column}` IS NULL AND ";
  231. } else {
  232. $sql .= "`{$column}` = ? AND ";
  233. if (is_bool($value)) {
  234. $value = $value ? 1 : 0;
  235. }
  236. $params[] = $value;
  237. }
  238. }
  239. $sql = substr($sql, 0, -4);
  240. } else {
  241. $sql .= ' 1 ';
  242. }
  243. if ($sqlEnd) {
  244. $sql .= $sqlEnd;
  245. }
  246. try {
  247. $query = $this->getConnection()->prepare($sql);
  248. foreach ($params as $key => $value) {
  249. if (is_bool($value)) {
  250. $value = $value ? 1 : 0;
  251. }
  252. $query->bindValue($key + 1, $value);
  253. }
  254. $query->execute();
  255. return $query->fetchAll(\PDO::FETCH_COLUMN);
  256. } catch (\PDOException $e) {
  257. throw new \Ip\Exception\Db($e->getMessage(), $e->getCode(), $e);
  258. }
  259. }
  260. /**
  261. * Execute SQL query
  262. *
  263. * @param string $sql
  264. * @param array $params
  265. * @return int The number of rows affected by the last SQL statement
  266. * @throws \Ip\Exception\Db
  267. */
  268. public function execute($sql, $params = array())
  269. {
  270. try {
  271. $query = $this->getConnection()->prepare($sql);
  272. foreach ($params as $key => $value) {
  273. if (is_bool($value)) {
  274. $value = $value ? 1 : 0;
  275. }
  276. $query->bindValue(is_numeric($key) ? $key + 1 : $key, $value);
  277. }
  278. $query->execute();
  279. return $query->rowCount();
  280. } catch (\PDOException $e) {
  281. throw new \Ip\Exception\Db($e->getMessage(), $e->getCode(), $e);
  282. }
  283. }
  284. /**
  285. * Execute SQL query and return a result set
  286. *
  287. * @param string $sql query
  288. * @param array $params The array represents each row as either an array of column values.
  289. * @return array
  290. * @throws \Ip\Exception\Db
  291. */
  292. public function fetchColumn($sql, $params = array())
  293. {
  294. try {
  295. $query = $this->getConnection()->prepare($sql);
  296. foreach ($params as $key => $value) {
  297. if (is_bool($value)) {
  298. $value = $value ? 1 : 0;
  299. }
  300. $query->bindValue(is_numeric($key) ? $key + 1 : $key, $value);
  301. }
  302. $query->execute();
  303. return $query->fetchAll(\PDO::FETCH_COLUMN);
  304. } catch (\PDOException $e) {
  305. throw new \Ip\Exception\Db($e->getMessage(), $e->getCode(), $e);
  306. }
  307. }
  308. /**
  309. * Execute query, insert values from associative array
  310. * @param $table
  311. * @param $row
  312. * @param bool $ignore
  313. * @return bool|string
  314. */
  315. public function insert($table, $row, $ignore = false)
  316. {
  317. $params = array();
  318. $values = '';
  319. $_ignore = $ignore ? ($this->isSqlite()?"OR IGNORE":"IGNORE") : "";
  320. $sql = "INSERT {$_ignore} INTO " . ipTable($table) . " (";
  321. foreach ($row as $column => $value) {
  322. $sql .= "`{$column}`, ";
  323. if (is_bool($value)) {
  324. $value = $value ? 1 : 0;
  325. }
  326. $params[] = $value;
  327. $values.='?, ';
  328. }
  329. $sql = substr($sql, 0, -2);
  330. $values = substr($values, 0, -2);
  331. $sql .= ") VALUES (${values})";
  332. if (empty($params)) {
  333. $sql = "INSERT {$_ignore} INTO " . ipTable($table) . " () VALUES()";
  334. }
  335. if ($this->execute($sql, $params)) {
  336. $lastInsertId = $this->getConnection()->lastInsertId();
  337. if ($lastInsertId === '0') { // for tables that do not have auto increment id
  338. return true;
  339. }
  340. return $lastInsertId;
  341. } else {
  342. return false;
  343. }
  344. }
  345. /**
  346. * Delete rows from a table
  347. *
  348. * @param string $table
  349. * @param array $condition A condition, for example, array("userId" => 5, "card_id" => 8)
  350. * @return int count of rows affected
  351. */
  352. public function delete($table, $condition)
  353. {
  354. $sql = "DELETE FROM " . ipTable($table, false) . " WHERE ";
  355. $params = array();
  356. $sql .= $this->buildConditions($condition, $params);
  357. return $this->execute($sql, $params);
  358. }
  359. /**
  360. * Update table records
  361. *
  362. * Execute query, updates values from associative array
  363. * @param string $table
  364. * @param array $update
  365. * @param array|int $condition
  366. * @return int count of rows updated
  367. */
  368. public function update($table, $update, $condition)
  369. {
  370. if (empty($update)) {
  371. return false;
  372. }
  373. $sql = 'UPDATE ' . ipTable($table) . ' SET ';
  374. $params = array();
  375. foreach ($update as $column => $value) {
  376. $sql .= "`{$column}` = ? , ";
  377. if (is_bool($value)) {
  378. $value = $value ? 1 : 0;
  379. }
  380. $params[] = $value;
  381. }
  382. $sql = substr($sql, 0, -2);
  383. $sql .= " WHERE ";
  384. $sql .= $this->buildConditions($condition, $params);
  385. return $this->execute($sql, $params);
  386. }
  387. /**
  388. * insert or update row of $table identified by $keys with $values
  389. *
  390. * @param string $table
  391. * @param array $keys
  392. * @param array $values
  393. */
  394. public function upsert($table, $keys, $values) {
  395. if ($this->insert($table, array_merge($keys, $values), true) == false) {
  396. $this->update($table, $values, $keys);
  397. }
  398. }
  399. /**
  400. * Return table prefix
  401. * @return mixed
  402. */
  403. public function tablePrefix()
  404. {
  405. return $this->tablePrefix;
  406. }
  407. /**
  408. * Get connection status
  409. *
  410. * @return bool
  411. */
  412. public function isConnected()
  413. {
  414. return $this->pdoConnection ? true : false;
  415. }
  416. /**
  417. * Return name of current driver
  418. */
  419. public function getDriverName()
  420. {
  421. return $this->pdoConnection->getAttribute(\PDO::ATTR_DRIVER_NAME);
  422. }
  423. /**
  424. * Return true if database is sqlite
  425. *
  426. * @return bool
  427. */
  428. public function isSQLite()
  429. {
  430. return $this->getDriverName() == 'sqlite';
  431. }
  432. /**
  433. * Return true if database is mysql
  434. *
  435. * @return bool
  436. */
  437. public function isMySQL()
  438. {
  439. return $this->getDriverName() == 'mysql';
  440. }
  441. /**
  442. * Return SQL condition to select rows with minimum age (database-dependent)
  443. *
  444. * @param string $fieldName field to compare
  445. * @param int $minAge minimum age
  446. * @param string $unit unit for age (HOUR or MINUTE)
  447. * @return string sql condition
  448. */
  449. public function sqlMinAge($fieldName, $minAge, $unit='HOUR') {
  450. if (!in_array($unit, array('MINUTE', 'HOUR'))) {
  451. throw \Ip\Exception("Only 'MINUTE' or 'HOUR' are available as unit options.");
  452. }
  453. if (ipDb()->isMySQL()) {
  454. $sql = "`".$fieldName."` < NOW() - INTERVAL " . ((int)$minAge) . " ".$unit;
  455. } else {
  456. $divider = 1;
  457. switch($unit) {
  458. case 'HOUR':
  459. $divider = 60*60;
  460. break;
  461. case 'MINUTE':
  462. $divider = 60;
  463. break;
  464. }
  465. $sql = "((STRFTIME('%s', 'now', 'localtime') - STRFTIME('%s', `".$fieldName.
  466. "`)/".$divider.")>". ((int)$minAge). ") ";
  467. }
  468. return $sql;
  469. }
  470. /**
  471. * Return SQL condition to select rows with minimum age (database-dependent)
  472. *
  473. * @param string $fieldName field to compare
  474. * @param int $maxAge minimum age
  475. * @param string $unit unit for age (HOUR or MINUTE)
  476. * @return string sql condition
  477. */
  478. public function sqlMaxAge($fieldName, $maxAge, $unit='HOUR') {
  479. if (!in_array($unit, array('MINUTE', 'HOUR'))) {
  480. throw \Ip\Exception("Only 'MINUTE' or 'HOUR' are available as unit options.");
  481. }
  482. // SELECT DATE(NOW()-INTERVAL 15 DAY)
  483. if (ipDb()->isMySQL()) {
  484. $sql = "`".$fieldName."` > NOW() - INTERVAL " . ((int)$maxAge) . " ".$unit;
  485. } else {
  486. switch($unit) {
  487. case 'HOUR':
  488. $divider = 60*60;
  489. break;
  490. case 'MINUTE':
  491. $divider = 60;
  492. break;
  493. }
  494. $sql = "((STRFTIME('%s', 'now', 'localtime') - STRFTIME('%s', `".$fieldName.
  495. "`)/".$divider.")>". ((int)$maxAge). ") ";
  496. }
  497. return $sql;
  498. }
  499. /**
  500. * Build WHERE statement from conditions.
  501. *
  502. * @param array $conditions
  503. * @param array $params
  504. *
  505. * @return string
  506. */
  507. protected function buildConditions($conditions = array(), &$params = array())
  508. {
  509. if (empty($conditions)) {
  510. return '1';
  511. }
  512. $sql = '';
  513. if (is_array($conditions)) {
  514. foreach ($conditions as $column => $value) {
  515. $realCol = $column;
  516. $pair = $this->containsOperator($column);
  517. if ($pair) {
  518. $realCol = $pair[0];
  519. }
  520. if ($value === null) {
  521. $isNull = 'IS NULL AND';
  522. if ($pair && preg_match("/(<>|!=)/", $pair[1])) {
  523. $isNull = 'IS NOT NULL AND';
  524. }
  525. $sql .= "`{$realCol}` {$isNull} ";
  526. } else {
  527. if ($pair) {
  528. $sql .= "`{$realCol}` {$pair[1]} ? AND ";
  529. } else {
  530. $sql .= "`{$realCol}` = ? AND ";
  531. }
  532. if (is_bool($value)) {
  533. $value = $value ? 1 : 0;
  534. }
  535. $params[] = $value;
  536. }
  537. }
  538. $sql = substr($sql, 0, -4);
  539. } else {
  540. $sql .= " `id` = ? ";
  541. $params[] = $conditions;
  542. }
  543. return $sql;
  544. }
  545. /**
  546. * Check whether a value contains an operator.
  547. *
  548. * @param $value string
  549. *
  550. * @return array|bool
  551. */
  552. protected function containsOperator($value)
  553. {
  554. $idents = preg_split("/(<=>|>=|<=|<>|>|<|!=|=|LIKE)/", $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
  555. if (count($idents) <= 1) {
  556. return false;
  557. } else {
  558. return array_map('trim', $idents);
  559. }
  560. }
  561. }