MySQLi.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <?php
  2. /**
  3. * MySQLi database connection.
  4. *
  5. * @package Kohana/Database
  6. * @category Drivers
  7. * @author Kohana Team
  8. * @copyright (c) Kohana Team
  9. * @license https://koseven.ga/LICENSE.md
  10. */
  11. class Kohana_Database_MySQLi extends Database {
  12. // Database in use by each connection
  13. protected static $_current_databases = [];
  14. // Use SET NAMES to set the character set
  15. protected static $_set_names;
  16. // Identifier for this connection within the PHP driver
  17. protected $_connection_id;
  18. // MySQL uses a backtick for identifiers
  19. protected $_identifier = '`';
  20. public function connect()
  21. {
  22. if ($this->_connection)
  23. return;
  24. if (Database_MySQLi::$_set_names === NULL)
  25. {
  26. // Determine if we can use mysqli_set_charset(), which is only
  27. // available on PHP 5.2.3+ when compiled against MySQL 5.0+
  28. Database_MySQLi::$_set_names = ! function_exists('mysqli_set_charset');
  29. }
  30. // Extract the connection parameters, adding required variabels
  31. extract($this->_config['connection'] + [
  32. 'database' => '',
  33. 'hostname' => '',
  34. 'username' => '',
  35. 'password' => '',
  36. 'socket' => '',
  37. 'port' => 3306,
  38. 'ssl' => NULL,
  39. ]);
  40. // Prevent this information from showing up in traces
  41. unset($this->_config['connection']['username'], $this->_config['connection']['password']);
  42. try
  43. {
  44. if(is_array($ssl))
  45. {
  46. $this->_connection = mysqli_init();
  47. $this->_connection->ssl_set(
  48. Arr::get($ssl, 'client_key_path'),
  49. Arr::get($ssl, 'client_cert_path'),
  50. Arr::get($ssl, 'ca_cert_path'),
  51. Arr::get($ssl, 'ca_dir_path'),
  52. Arr::get($ssl, 'cipher')
  53. );
  54. $this->_connection->real_connect($hostname, $username, $password, $database, $port, $socket, MYSQLI_CLIENT_SSL);
  55. }
  56. else
  57. {
  58. $this->_connection = new mysqli($hostname, $username, $password, $database, $port, $socket);
  59. }
  60. }
  61. catch (Exception $e)
  62. {
  63. // No connection exists
  64. $this->_connection = NULL;
  65. throw new Database_Exception(':error', [':error' => $e->getMessage()], $e->getCode());
  66. }
  67. // \xFF is a better delimiter, but the PHP driver uses underscore
  68. $this->_connection_id = sha1($hostname.'_'.$username.'_'.$password);
  69. if ( ! empty($this->_config['charset']))
  70. {
  71. // Set the character set
  72. $this->set_charset($this->_config['charset']);
  73. }
  74. if ( ! empty($this->_config['connection']['variables']))
  75. {
  76. // Set session variables
  77. $variables = [];
  78. foreach ($this->_config['connection']['variables'] as $var => $val)
  79. {
  80. $variables[] = 'SESSION '.$var.' = '.$this->quote($val);
  81. }
  82. $this->_connection->query('SET '.implode(', ', $variables));
  83. }
  84. }
  85. public function disconnect()
  86. {
  87. try
  88. {
  89. // Database is assumed disconnected
  90. $status = TRUE;
  91. if (is_resource($this->_connection))
  92. {
  93. if ($status = $this->_connection->close())
  94. {
  95. // Clear the connection
  96. $this->_connection = NULL;
  97. // Clear the instance
  98. parent::disconnect();
  99. }
  100. }
  101. }
  102. catch (Exception $e)
  103. {
  104. // Database is probably not disconnected
  105. $status = ! is_resource($this->_connection);
  106. }
  107. return $status;
  108. }
  109. public function set_charset($charset)
  110. {
  111. // Make sure the database is connected
  112. $this->_connection or $this->connect();
  113. if (Database_MySQLi::$_set_names === TRUE)
  114. {
  115. // PHP is compiled against MySQL 4.x
  116. $status = (bool) $this->_connection->query('SET NAMES '.$this->quote($charset));
  117. }
  118. else
  119. {
  120. // PHP is compiled against MySQL 5.x
  121. $status = $this->_connection->set_charset($charset);
  122. }
  123. if ($status === FALSE)
  124. {
  125. throw new Database_Exception(':error', [':error' => $this->_connection->error], $this->_connection->errno);
  126. }
  127. }
  128. public function query($type, $sql, $as_object = FALSE, array $params = NULL)
  129. {
  130. // Make sure the database is connected
  131. $this->_connection or $this->connect();
  132. if (Kohana::$profiling)
  133. {
  134. // Benchmark this query for the current instance
  135. $benchmark = Profiler::start("Database ({$this->_instance})", $sql);
  136. }
  137. // Execute the query
  138. if (($result = $this->_connection->query($sql)) === FALSE)
  139. {
  140. if (isset($benchmark))
  141. {
  142. // This benchmark is worthless
  143. Profiler::delete($benchmark);
  144. }
  145. throw new Database_Exception(':error [ :query ]', [
  146. ':error' => $this->_connection->error,
  147. ':query' => $sql
  148. ], $this->_connection->errno);
  149. }
  150. if (isset($benchmark))
  151. {
  152. Profiler::stop($benchmark);
  153. }
  154. // Set the last query
  155. $this->last_query = $sql;
  156. if ($type === Database::SELECT)
  157. {
  158. // Return an iterator of results
  159. return new Database_MySQLi_Result($result, $sql, $as_object, $params);
  160. }
  161. elseif ($type === Database::INSERT)
  162. {
  163. // Return a list of insert id and rows created
  164. return [
  165. $this->_connection->insert_id,
  166. $this->_connection->affected_rows,
  167. ];
  168. }
  169. else
  170. {
  171. // Return the number of rows affected
  172. return $this->_connection->affected_rows;
  173. }
  174. }
  175. public function datatype($type)
  176. {
  177. static $types = [
  178. 'blob' => ['type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '65535'],
  179. 'bool' => ['type' => 'bool'],
  180. 'bigint unsigned' => ['type' => 'int', 'min' => '0', 'max' => '18446744073709551615'],
  181. 'datetime' => ['type' => 'string'],
  182. 'decimal unsigned' => ['type' => 'float', 'exact' => TRUE, 'min' => '0'],
  183. 'double' => ['type' => 'float'],
  184. 'double precision unsigned' => ['type' => 'float', 'min' => '0'],
  185. 'double unsigned' => ['type' => 'float', 'min' => '0'],
  186. 'enum' => ['type' => 'string'],
  187. 'fixed' => ['type' => 'float', 'exact' => TRUE],
  188. 'fixed unsigned' => ['type' => 'float', 'exact' => TRUE, 'min' => '0'],
  189. 'float unsigned' => ['type' => 'float', 'min' => '0'],
  190. 'geometry' => ['type' => 'string', 'binary' => TRUE],
  191. 'int unsigned' => ['type' => 'int', 'min' => '0', 'max' => '4294967295'],
  192. 'integer unsigned' => ['type' => 'int', 'min' => '0', 'max' => '4294967295'],
  193. 'longblob' => ['type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '4294967295'],
  194. 'longtext' => ['type' => 'string', 'character_maximum_length' => '4294967295'],
  195. 'mediumblob' => ['type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '16777215'],
  196. 'mediumint' => ['type' => 'int', 'min' => '-8388608', 'max' => '8388607'],
  197. 'mediumint unsigned' => ['type' => 'int', 'min' => '0', 'max' => '16777215'],
  198. 'mediumtext' => ['type' => 'string', 'character_maximum_length' => '16777215'],
  199. 'national varchar' => ['type' => 'string'],
  200. 'numeric unsigned' => ['type' => 'float', 'exact' => TRUE, 'min' => '0'],
  201. 'nvarchar' => ['type' => 'string'],
  202. 'point' => ['type' => 'string', 'binary' => TRUE],
  203. 'real unsigned' => ['type' => 'float', 'min' => '0'],
  204. 'set' => ['type' => 'string'],
  205. 'smallint unsigned' => ['type' => 'int', 'min' => '0', 'max' => '65535'],
  206. 'text' => ['type' => 'string', 'character_maximum_length' => '65535'],
  207. 'tinyblob' => ['type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '255'],
  208. 'tinyint' => ['type' => 'int', 'min' => '-128', 'max' => '127'],
  209. 'tinyint unsigned' => ['type' => 'int', 'min' => '0', 'max' => '255'],
  210. 'tinytext' => ['type' => 'string', 'character_maximum_length' => '255'],
  211. 'year' => ['type' => 'string'],
  212. ];
  213. $type = str_replace(' zerofill', '', $type);
  214. if (isset($types[$type]))
  215. return $types[$type];
  216. return parent::datatype($type);
  217. }
  218. /**
  219. * Start a SQL transaction
  220. *
  221. * @link http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
  222. *
  223. * @param string $mode Isolation level
  224. * @return boolean
  225. */
  226. public function begin($mode = NULL)
  227. {
  228. // Make sure the database is connected
  229. $this->_connection or $this->connect();
  230. if ($mode AND ! $this->_connection->query("SET TRANSACTION ISOLATION LEVEL $mode"))
  231. {
  232. throw new Database_Exception(':error', [
  233. ':error' => $this->_connection->error
  234. ], $this->_connection->errno);
  235. }
  236. return (bool) $this->_connection->query('START TRANSACTION');
  237. }
  238. /**
  239. * Commit a SQL transaction
  240. *
  241. * @return boolean
  242. */
  243. public function commit()
  244. {
  245. // Make sure the database is connected
  246. $this->_connection or $this->connect();
  247. return (bool) $this->_connection->query('COMMIT');
  248. }
  249. /**
  250. * Rollback a SQL transaction
  251. *
  252. * @return boolean
  253. */
  254. public function rollback()
  255. {
  256. // Make sure the database is connected
  257. $this->_connection or $this->connect();
  258. return (bool) $this->_connection->query('ROLLBACK');
  259. }
  260. public function list_tables($like = NULL)
  261. {
  262. if (is_string($like))
  263. {
  264. // Search for table names
  265. $result = $this->query(Database::SELECT, 'SHOW TABLES LIKE '.$this->quote($like), FALSE);
  266. }
  267. else
  268. {
  269. // Find all table names
  270. $result = $this->query(Database::SELECT, 'SHOW TABLES', FALSE);
  271. }
  272. $tables = [];
  273. foreach ($result as $row)
  274. {
  275. $tables[] = reset($row);
  276. }
  277. return $tables;
  278. }
  279. public function list_columns($table, $like = NULL, $add_prefix = TRUE)
  280. {
  281. // Quote the table name
  282. $table = ($add_prefix === TRUE) ? $this->quote_table($table) : $table;
  283. if (is_string($like))
  284. {
  285. // Search for column names
  286. $result = $this->query(Database::SELECT, 'SHOW FULL COLUMNS FROM '.$table.' LIKE '.$this->quote($like), FALSE);
  287. }
  288. else
  289. {
  290. // Find all column names
  291. $result = $this->query(Database::SELECT, 'SHOW FULL COLUMNS FROM '.$table, FALSE);
  292. }
  293. $count = 0;
  294. $columns = [];
  295. foreach ($result as $row)
  296. {
  297. list($type, $length) = $this->_parse_type($row['Type']);
  298. $column = $this->datatype($type);
  299. $column['column_name'] = $row['Field'];
  300. $column['column_default'] = $row['Default'];
  301. $column['data_type'] = $type;
  302. $column['is_nullable'] = ($row['Null'] == 'YES');
  303. $column['ordinal_position'] = ++$count;
  304. switch ($column['type'])
  305. {
  306. case 'float':
  307. if (isset($length))
  308. {
  309. list($column['numeric_precision'], $column['numeric_scale']) = explode(',', $length);
  310. }
  311. break;
  312. case 'int':
  313. if (isset($length))
  314. {
  315. // MySQL attribute
  316. $column['display'] = $length;
  317. }
  318. break;
  319. case 'string':
  320. switch ($column['data_type'])
  321. {
  322. case 'binary':
  323. case 'varbinary':
  324. $column['character_maximum_length'] = $length;
  325. break;
  326. case 'char':
  327. case 'varchar':
  328. $column['character_maximum_length'] = $length;
  329. case 'text':
  330. case 'tinytext':
  331. case 'mediumtext':
  332. case 'longtext':
  333. $column['collation_name'] = $row['Collation'];
  334. break;
  335. case 'enum':
  336. case 'set':
  337. $column['collation_name'] = $row['Collation'];
  338. $column['options'] = explode('\',\'', substr($length, 1, -1));
  339. break;
  340. }
  341. break;
  342. }
  343. // MySQL attributes
  344. $column['comment'] = $row['Comment'];
  345. $column['extra'] = $row['Extra'];
  346. $column['key'] = $row['Key'];
  347. $column['privileges'] = $row['Privileges'];
  348. $columns[$row['Field']] = $column;
  349. }
  350. return $columns;
  351. }
  352. public function escape($value)
  353. {
  354. // Make sure the database is connected
  355. $this->_connection or $this->connect();
  356. if (($value = $this->_connection->real_escape_string( (string) $value)) === FALSE)
  357. {
  358. throw new Database_Exception(':error', [
  359. ':error' => $this->_connection->error,
  360. ], $this->_connection->errno);
  361. }
  362. // SQL standard is to use single-quotes for all values
  363. return "'$value'";
  364. }
  365. } // End Database_MySQLi