PageRenderTime 52ms CodeModel.GetById 5ms RepoModel.GetById 0ms app.codeStats 0ms

/ManaPHP/Db.php

https://gitlab.com/szlongshu/manaphp
PHP | 665 lines | 276 code | 80 blank | 309 comment | 35 complexity | 1f1df13b2575b3f452ccbd7fba2d1922 MD5 | raw file
  1. <?php
  2. namespace ManaPHP {
  3. use ManaPHP\Db\ConditionParser;
  4. use ManaPHP\Db\Exception;
  5. class Db extends Component implements DbInterface
  6. {
  7. /**
  8. * Descriptor used to connect to a database
  9. *
  10. * @var array
  11. */
  12. protected $_descriptor;
  13. /**
  14. * Type of database system driver is used for
  15. *
  16. * @var string
  17. */
  18. protected $_type;
  19. /**
  20. * Active SQL Statement
  21. *
  22. * @var string
  23. */
  24. protected $_sql;
  25. /**
  26. * Active SQL bound parameter variables
  27. *
  28. * @var array
  29. */
  30. protected $_bind;
  31. /**
  32. * Current transaction level
  33. *
  34. * @var int
  35. */
  36. protected $_transactionLevel = 0;
  37. /**
  38. * @var \PDO
  39. */
  40. protected $_pdo;
  41. /**
  42. * Last affected rows
  43. *
  44. * @var int
  45. */
  46. protected $_affectedRows;
  47. /**
  48. * \ManaPHP\Db\Adapter constructor
  49. *
  50. * @param array $descriptor
  51. */
  52. public function __construct($descriptor)
  53. {
  54. if (!isset($descriptor['options'])) {
  55. $descriptor['options'] = [];
  56. }
  57. $descriptor['options'][\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION;
  58. $this->_descriptor = $descriptor;
  59. $this->_connect();
  60. }
  61. /**
  62. * This method is automatically called in ManaPHP\Db\Adapter\Pdo constructor.
  63. * Call it when you need to restore a database connection
  64. *
  65. *<code>
  66. * //Make a connection
  67. * $connection = new \ManaPHP\Db\Adapter\Pdo\Mysql(array(
  68. * 'host' => '192.168.0.11',
  69. * 'username' => 'sigma',
  70. * 'password' => 'secret',
  71. * 'dbname' => 'blog',
  72. * ));
  73. *
  74. * //Reconnect
  75. * $connection->connect();
  76. * </code>
  77. *
  78. * @return boolean
  79. */
  80. protected function _connect()
  81. {
  82. $descriptor = $this->_descriptor;
  83. $username = isset($descriptor['username']) ? $descriptor['username'] : null;
  84. $password = isset($descriptor['password']) ? $descriptor['password'] : null;
  85. $options = $descriptor['options'];
  86. unset($descriptor['username'], $descriptor['password'], $descriptor['options']);
  87. if (isset($descriptor['dsn'])) {
  88. $dsn = $descriptor['dsn'];
  89. } else {
  90. $dsn_parts = [];
  91. foreach ($descriptor as $k => $v) {
  92. $dsn_parts[] = $k . '=' . $v;
  93. }
  94. $dsn = implode(';', $dsn_parts);
  95. }
  96. $this->_pdo = new \PDO($this->_type . ':' . $dsn, $username, $password, $options);
  97. }
  98. /**
  99. * Executes a prepared statement binding. This function uses integer indexes starting from zero
  100. *
  101. *<code>
  102. * $statement = $db->prepare('SELECT * FROM robots WHERE name = :name');
  103. * $result = $connection->executePrepared($statement, array('name' => 'mana'));
  104. *</code>
  105. *
  106. * @param \PDOStatement $statement
  107. * @param array $bind
  108. *
  109. * @return \PDOStatement
  110. * @throws \ManaPHP\Db\Exception
  111. */
  112. protected function _executePrepared($statement, $bind)
  113. {
  114. foreach ($bind as $parameter => $value) {
  115. if (is_string($value)) {
  116. $type = \PDO::PARAM_STR;
  117. } elseif (is_int($value)) {
  118. $type = \PDO::PARAM_INT;
  119. } elseif (is_bool($value)) {
  120. $type = \PDO::PARAM_BOOL;
  121. } elseif ($value === null) {
  122. $type = \PDO::PARAM_NULL;
  123. } else {
  124. throw new Exception("The type of parameter of '$parameter' is not support: " . gettype($value));
  125. }
  126. if (is_int($parameter)) {
  127. $statement->bindValue($parameter + 1, $value, $type);
  128. } else {
  129. if ($parameter[0] === ':') {
  130. throw new Exception("Bind does not require started with ':' for parameter: " . $parameter);
  131. }
  132. $statement->bindValue(':' . $parameter, $value, $type);
  133. }
  134. }
  135. $statement->execute();
  136. return $statement;
  137. }
  138. /**
  139. * Sends SQL statements to the database server returning the success state.
  140. * Use this method only when the SQL statement sent to the server is returning rows
  141. *
  142. *<code>
  143. * //Querying data
  144. * $resultset = $connection->query("SELECT * FROM robots WHERE type='mechanical'");
  145. * $resultset = $connection->query("SELECT * FROM robots WHERE type=?", array("mechanical"));
  146. *</code>
  147. *
  148. * @param string $sql
  149. * @param array $bind
  150. * @param int $fetchMode
  151. *
  152. * @return \PdoStatement
  153. * @throws \ManaPHP\Db\Exception
  154. */
  155. public function query($sql, $bind = [], $fetchMode = \PDO::FETCH_ASSOC)
  156. {
  157. $this->_sql = $sql;
  158. $this->_bind = $bind;
  159. if ($this->fireEvent('db:beforeQuery') === false) {
  160. return false;
  161. }
  162. try {
  163. if (count($bind) !== 0) {
  164. $statement = $this->_pdo->prepare($sql);
  165. $statement = $this->_executePrepared($statement, $bind);
  166. } else {
  167. $statement = $this->_pdo->query($sql);
  168. }
  169. $statement->setFetchMode($fetchMode);
  170. } catch (\PDOException $e) {
  171. throw new Exception($e->getMessage());
  172. }
  173. $this->fireEvent('db:afterQuery');
  174. return $statement;
  175. }
  176. /**
  177. * Sends SQL statements to the database server returning the success state.
  178. * Use this method only when the SQL statement sent to the server does n't return any rows
  179. *
  180. *<code>
  181. * //Inserting data
  182. * $success = $connection->execute("INSERT INTO robots VALUES (1, 'Boy')");
  183. * $success = $connection->execute("INSERT INTO robots VALUES (?, ?)", array(1, 'Boy'));
  184. *</code>
  185. *
  186. * @param string $sql
  187. * @param array $bind
  188. *
  189. * @return int
  190. * @throws \ManaPHP\Db\Exception
  191. */
  192. public function execute($sql, $bind = [])
  193. {
  194. $this->_sql = $sql;
  195. $this->_bind = $bind;
  196. $this->_affectedRows = 0;
  197. $this->fireEvent('db:beforeQuery');
  198. try {
  199. if (count($bind) !== 0) {
  200. $statement = $this->_pdo->prepare($sql);
  201. $newStatement = $this->_executePrepared($statement, $bind);
  202. $this->_affectedRows = $newStatement->rowCount();
  203. } else {
  204. $this->_affectedRows = $this->_pdo->exec($sql);
  205. }
  206. } catch (\PDOException $e) {
  207. throw new Exception($e->getMessage());
  208. }
  209. if (is_int($this->_affectedRows)) {
  210. $this->fireEvent('db:afterQuery');
  211. }
  212. return $this->_affectedRows;
  213. }
  214. /**
  215. * Escapes a column/table/schema name
  216. *
  217. * <code>
  218. * echo $connection->escapeIdentifier('my_table'); // `my_table`
  219. * echo $connection->escapeIdentifier('companies.name']); // `companies`.`name`
  220. * <code>
  221. *
  222. * @param string|array $identifier
  223. *
  224. * @return string
  225. */
  226. public function escapeIdentifier($identifier)
  227. {
  228. $identifiers = explode('.', $identifier);
  229. $list = [];
  230. foreach ($identifiers as $identifier) {
  231. if ($identifier[0] === '`') {
  232. $list[] = $identifier;
  233. } else {
  234. $list[] = "`$identifier`";
  235. }
  236. }
  237. return implode('.', $list);
  238. }
  239. /**
  240. * Returns the number of affected rows by the last INSERT/UPDATE/DELETE reported by the database system
  241. *
  242. * @return int
  243. */
  244. public function affectedRows()
  245. {
  246. return $this->_affectedRows;
  247. }
  248. /**
  249. * Returns the first row in a SQL query result
  250. *
  251. *<code>
  252. * //Getting first robot
  253. * $robot = $connection->fetchOne("SELECT * FROM robots");
  254. * print_r($robot);
  255. *
  256. * //Getting first robot with associative indexes only
  257. * $robot = $connection->fetchOne("SELECT * FROM robots", \ManaPHP\Db::FETCH_ASSOC);
  258. * print_r($robot);
  259. *</code>
  260. *
  261. * @param string $sql
  262. * @param array $bind
  263. * @param int $fetchMode
  264. *
  265. * @throws \ManaPHP\Db\Exception
  266. * @return array|false
  267. */
  268. public function fetchOne($sql, $bind = [], $fetchMode = \PDO::FETCH_ASSOC)
  269. {
  270. $result = $this->query($sql, $bind, $fetchMode);
  271. return $result->fetch();
  272. }
  273. /**
  274. * Dumps the complete result of a query into an array
  275. *
  276. *<code>
  277. * //Getting all robots with associative indexes only
  278. * $robots = $connection->fetchAll("SELECT * FROM robots", \ManaPHP\Db::FETCH_ASSOC);
  279. * foreach ($robots as $robot) {
  280. * print_r($robot);
  281. * }
  282. *
  283. * //Getting all robots that contains word "robot" withing the name
  284. * $robots = $connection->fetchAll("SELECT * FROM robots WHERE name LIKE :name",
  285. * ManaPHP\Db::FETCH_ASSOC,
  286. * array('name' => '%robot%')
  287. * );
  288. * foreach($robots as $robot){
  289. * print_r($robot);
  290. * }
  291. *</code>
  292. *
  293. * @param string $sql
  294. * @param array $bind
  295. * @param int $fetchMode
  296. *
  297. * @throws \ManaPHP\Db\Exception
  298. * @return array
  299. */
  300. public function fetchAll($sql, $bind = [], $fetchMode = \PDO::FETCH_ASSOC)
  301. {
  302. $result = $this->query($sql, $bind, $fetchMode);
  303. return $result->fetchAll();
  304. }
  305. /**
  306. * Inserts data into a table using custom SQL syntax
  307. *
  308. * <code>
  309. * //Inserting a new robot
  310. * $success = $connection->insert(
  311. * "robots",
  312. * array("Boy", 1952),
  313. * array("name", "year")
  314. * );
  315. *
  316. * //Next SQL sentence is sent to the database system
  317. * INSERT INTO `robots` (`name`, `year`) VALUES ("boy", 1952);
  318. * </code>
  319. *
  320. * @param string $table
  321. * @param array $columnValues
  322. *
  323. * @return boolean
  324. * @throws \ManaPHP\Db\Exception
  325. */
  326. public function insert($table, $columnValues)
  327. {
  328. if (count($columnValues) === 0) {
  329. throw new Exception('Unable to insert into ' . $table . ' without data');
  330. }
  331. $escapedTable = $this->escapeIdentifier($table);
  332. if (array_key_exists(0, $columnValues)) {
  333. $insertedValues = rtrim(str_repeat('?,', count($columnValues)), ',');
  334. $sql = /** @lang Text */
  335. "INSERT INTO $escapedTable VALUES ($insertedValues)";
  336. } else {
  337. $columns = array_keys($columnValues);
  338. $insertedValues = ':' . implode(',:', $columns);
  339. $insertedColumns = '`' . implode('`,`', $columns) . '`';
  340. $sql = /** @lang Text */
  341. "INSERT INTO $escapedTable ($insertedColumns) VALUES ($insertedValues)";
  342. }
  343. return $this->execute($sql, $columnValues) === 1;
  344. }
  345. /**
  346. * Updates data on a table using custom SQL syntax
  347. *
  348. * <code>
  349. * //Updating existing robot
  350. * $success = $connection->update(
  351. * "robots",
  352. * array("name"),
  353. * array("New Boy"),
  354. * "id = 101"
  355. * );
  356. *
  357. * //Next SQL sentence is sent to the database system
  358. * UPDATE `robots` SET `name` = "boy" WHERE id = 101
  359. * </code>
  360. *
  361. * @param string $table
  362. * @param array $columnValues
  363. * @param string|array $conditions
  364. * @param array $bind
  365. *
  366. * @return int|false
  367. * @throws \ManaPHP\Db\Exception
  368. */
  369. public function update($table, $columnValues, $conditions, $bind = [])
  370. {
  371. $escapedTable = "`$table`";
  372. if (count($columnValues) === 0) {
  373. throw new Exception('Unable to update ' . $table . ' without data');
  374. }
  375. $where = (new ConditionParser())->parse($conditions, $conditionBind);
  376. $bind = $bind ? array_merge($conditionBind, $bind) : $conditionBind;
  377. $setColumns = [];
  378. foreach ($columnValues as $k => $v) {
  379. $setColumns[] = "`$k`=:$k";
  380. $bind[$k] = $v;
  381. }
  382. $updateColumns = implode(',', $setColumns);
  383. $updateSql = /** @lang Text */
  384. "UPDATE $escapedTable SET $updateColumns WHERE $where";
  385. return $this->execute($updateSql, $bind);
  386. }
  387. /**
  388. * Deletes data from a table using custom SQL syntax
  389. *
  390. * <code>
  391. * //Deleting existing robot
  392. * $success = $connection->delete(
  393. * "robots",
  394. * "id = 101"
  395. * );
  396. *
  397. * //Next SQL sentence is generated
  398. * DELETE FROM `robots` WHERE `id` = 101
  399. * </code>
  400. *
  401. * @param string $table
  402. * @param string|array $conditions
  403. * @param array $bind
  404. *
  405. * @return boolean
  406. * @throws \ManaPHP\Db\Exception
  407. */
  408. public function delete($table, $conditions, $bind = [])
  409. {
  410. $where = (new ConditionParser())->parse($conditions, $conditionBind);
  411. $sql = /**@lang Text */
  412. "DELETE FROM `$table` WHERE " . $where;
  413. if ($bind === null) {
  414. $bind = $conditionBind;
  415. } else {
  416. $bind = array_merge($conditionBind, $bind);
  417. }
  418. return $this->execute($sql, $bind);
  419. }
  420. /**
  421. * Appends a LIMIT clause to $sqlQuery argument
  422. *
  423. * <code>
  424. * echo $connection->limit("SELECT * FROM robots", 5);
  425. * </code>
  426. *
  427. * @param string $sql
  428. * @param int $number
  429. * @param int $offset
  430. *
  431. * @return string
  432. */
  433. public function limit($sql, $number, $offset = null)
  434. {
  435. return $sql . ' LIMIT ' . $number . ($offset === null ? '' : (' OFFSET ' . $offset));
  436. }
  437. /**
  438. * Active SQL statement in the object
  439. *
  440. * @return string
  441. */
  442. public function getSQL()
  443. {
  444. return $this->_sql;
  445. }
  446. /**
  447. * @param mixed $value
  448. * @param int $preservedStrLength
  449. *
  450. * @return int|string
  451. */
  452. protected function _parseBindValue($value, $preservedStrLength)
  453. {
  454. if (is_string($value)) {
  455. if ($preservedStrLength > 0 && strlen($value) >= $preservedStrLength) {
  456. return $this->_pdo->quote(substr($value, 0, $preservedStrLength) . '...');
  457. } else {
  458. return $this->_pdo->quote($value);
  459. }
  460. } elseif (is_int($value)) {
  461. return $value;
  462. } elseif ($value === null) {
  463. return 'NULL';
  464. } elseif (is_bool($value)) {
  465. return (int)$value;
  466. } else {
  467. return $value;
  468. }
  469. }
  470. /**
  471. * Active SQL statement in the object with replace the bind with value
  472. *
  473. * @param int $preservedStrLength
  474. *
  475. * @return string
  476. * @throws \ManaPHP\Db\Exception
  477. */
  478. public function getEmulatedSQL($preservedStrLength = -1)
  479. {
  480. if ($this->_bind === null || count($this->_bind) === 0) {
  481. return $this->_sql;
  482. }
  483. $bind = $this->_bind;
  484. if (isset($bind[0])) {
  485. $pos = 0;
  486. return preg_replace_callback('/(\?)/',
  487. function () use ($bind, &$pos, $preservedStrLength) {
  488. return $this->_parseBindValue($bind[$pos++], $preservedStrLength);
  489. }, $this->_sql);
  490. } else {
  491. $replaces = [];
  492. foreach ($bind as $key => $value) {
  493. $replaces[':' . $key] = $this->_parseBindValue($value, $preservedStrLength);
  494. }
  495. return strtr($this->_sql, $replaces);
  496. }
  497. }
  498. /**
  499. * Active SQL statement in the object
  500. *
  501. * @return array
  502. */
  503. public function getBind()
  504. {
  505. return $this->_bind;
  506. }
  507. /**
  508. * Starts a transaction in the connection
  509. *
  510. * @return boolean
  511. * @throws \ManaPHP\Db\Exception
  512. */
  513. public function begin()
  514. {
  515. if ($this->_transactionLevel !== 0) {
  516. throw new Exception('There is in a active transaction already.');
  517. }
  518. $this->fireEvent('db:beginTransaction');
  519. $this->_transactionLevel++;
  520. return $this->_pdo->beginTransaction();
  521. }
  522. /**
  523. * Checks whether the connection is under a transaction
  524. *
  525. *<code>
  526. * $connection->begin();
  527. * var_dump($connection->isUnderTransaction()); //true
  528. *</code>
  529. *
  530. * @return bool
  531. */
  532. public function isUnderTransaction()
  533. {
  534. return $this->_pdo->inTransaction();
  535. }
  536. /**
  537. * Rollbacks the active transaction in the connection
  538. *
  539. * @return boolean
  540. * @throws \ManaPHP\Db\Exception
  541. */
  542. public function rollback()
  543. {
  544. if ($this->_transactionLevel === 0) {
  545. throw new Exception('There is no active transaction');
  546. }
  547. $this->fireEvent('db:rollbackTransaction');
  548. $this->_transactionLevel--;
  549. return $this->_pdo->rollBack();
  550. }
  551. /**
  552. * Commits the active transaction in the connection
  553. *
  554. * @return boolean
  555. * @throws \ManaPHP\Db\Exception
  556. */
  557. public function commit()
  558. {
  559. if ($this->_transactionLevel === 0) {
  560. throw new Exception('There is no active transaction');
  561. }
  562. $this->fireEvent('db:commitTransaction');
  563. $this->_transactionLevel--;
  564. return $this->_pdo->commit();
  565. }
  566. /**
  567. * Returns insert id for the auto_increment column inserted in the last SQL statement
  568. *
  569. * @return int
  570. */
  571. public function lastInsertId()
  572. {
  573. return (int)$this->_pdo->lastInsertId();
  574. }
  575. /**
  576. * Return internal PDO handler
  577. *
  578. * @return \PDO
  579. */
  580. public function getInternalHandler()
  581. {
  582. return $this->_pdo;
  583. }
  584. }
  585. }