debug = $debug; // (defined('ENABLE_DB_DEBUG') && ENABLE_DB_DEBUG); if($db_id) $this->db_id = $db_id; if(!sizeof(self::$cnn_props)) { self::$cnn_props = self::setConnectionSettings(); } } private static function setConnectionSettings() { return static::getCache("getConnectionSettings", function (){ $db_file = CONFIG_DIR.'/db.php'; if(file_exists($db_file)) { return include $db_file; } return []; }); } /** * @param string $table * @return array */ public function getTableDefaultItemInfo($table) { $column_info = $this->getTableInfo($table); $default_info = []; foreach ($column_info as $field => $info) { $default_info[$field] = $info['COLUMN_DEFAULT']; } return $default_info; } //28-07-2015 get all columns of a table public function getTableInfo($table) { return self::getCache('getTableInfo-'.$table, function () use ($table) { $db_props = $this->getConnectionProps(); if(!$db_props) { return false; } $database = $db_props['db'] ?? null; if(!$database) { return false; } $query = $this->runQuery( "SELECT `COLUMN_NAME` , COLUMN_DEFAULT, ORDINAL_POSITION, COLUMN_DEFAULT, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, COLUMN_TYPE, COLUMN_KEY, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE `TABLE_SCHEMA` = ? AND`TABLE_NAME` = ? ", ['s', 's'], [$database, $table] ); $output = []; foreach( $this->fetchAll($query) as $row){ $output[$row['COLUMN_NAME']] = $row; } return $output; }); } protected function setTrace($query, $start_time, $msg = ''){ static::$traces[] = [ "msg" => $msg, "query" => $query, "start_time" => $start_time, "total_time" => round($this->getCurrentTime() - $start_time, 5), ]; } public static function getTraces() { $query_count = sizeof(static::$traces); $query_time = array_sum(array_map(function ($item){ return $item['total_time'];}, static::$traces)) ; return [ 'query_count' => $query_count, 'query_time' => $query_time, 'list' => static::$traces, ]; } private function __clone(){ // } /** * @param string $db_id * @param bool $debug * @return iConnectDB */ public static function getInstance(string $db_id = '', bool $debug = false) { if( ! $db_id ) $db_id = 'main'; if(!isset(ConnectDB::$instance[$db_id])) { ConnectDB::$instance[$db_id] = new self($db_id, $debug); } return ConnectDB::$instance[$db_id]; } //close all connections public static function close() { foreach (ConnectDB::$instance as $db_id => $cnn) { $cnn->disconnect(); } // reset ConnectDB::$instance = []; } public function isConnected(): bool { return ($this->connection instanceof \mysqli); } private function getConnectionProps() { return self::$cnn_props[$this->db_id] ?? null; } private function connect() { // already connect if($this->connection instanceof \mysqli) { return true; } // connection props not set $db_props = $this->getConnectionProps(); if(!$db_props) { return false; } $host = $db_props['host'] ?? null; $user = $db_props['user'] ?? null; $pass = $db_props['pass'] ?? null; $database = $db_props['db'] ?? null; $db_charset = $db_props['charset'] ?? 'latin1'; if(!$host || !$user || !$pass || !$database) { return false; } try { //create the object $cnn = \mysqli_init(); $cnn->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5); //specify the read timeout if (!defined('MYSQLI_OPT_READ_TIMEOUT')) { define ('MYSQLI_OPT_READ_TIMEOUT', 11); } $cnn->options(MYSQLI_OPT_READ_TIMEOUT, 10); if($cnn->real_connect($host, $user, $pass)){ $cnn->select_db($database); $cnn->query("SET NAMES ".$db_charset); // UTF8|latin1 $cnn->query("SET sql_mode=''"); //for old version to work with mysql 5.7 //mysqli_select_db($this->connection, $database); //mysqli_query($this->connection, "SET NAMES UTF8") ;//set utf8 if database using utf-8 encode //mysqli_query($this->connection, "SET NAMES latin1") ;//set back to latin1 //mysqli_query($this->connection, "SET sql_mode=''") ;//for old version to work with mysql 5.7 $this->connection = $cnn; return true; } //throw new \Exception('Unable to connect'); return false; }catch (\Exception $e) { //die($e->getMessage()); echo $e->getMessage(); return false; } } public function disconnect() { if($this->connection instanceof \mysqli) { $this->connection->close(); $this->connection = null; } } public function ping() { if($this->connection) { $this->connection->ping(); } } /** * @param string $query * @param array $bind_types * @param array $bind_values * @param bool $get_affected_row_or_id get the affected row or newly insert-id from the query, default return the mysqli_result * @return \mysqli_result | false | int */ public function runQuery($query, array $bind_types=[], array $bind_values=[], $get_affected_row_or_id = false) { $start_time = $this->getCurrentTime(); // connect on demand if(!$this->connect()) { if($this->debug) $this->setTrace($query, $start_time, 'runQuery: Connection fails'); return false; } $stmt = $this->connection->prepare( $query ); if(!$stmt) { //throw new \Exception($stmt->error); //throw new \Exception($this->connection->error); if($this->debug) { $this->setTrace($query, $start_time, 'runQuery: '.$this->connection->error); die("runQuery error: ".$this->connection->error.". Query: ".$query); } return false; } if(sizeof($bind_types) && sizeof($bind_types) == sizeof($bind_values)) { if(!$stmt->bind_param(join('', $bind_types), ...$bind_values)) { if($this->debug) { $this->setTrace($query, $start_time, 'runQuery: bind_param '.$this->connection->error); die('runQuery: bind_param '.$this->connection->error); } return false; } } if(!$stmt->execute()) { if($this->debug) { $this->setTrace($query, $start_time, 'runQuery: execute '.$this->connection->error); die("runQuery error: ".$this->connection->error.". Query: ".$query); } return false; } if($this->debug) $this->setTrace($query, $start_time, ''); if($get_affected_row_or_id) { return $stmt->insert_id ?: $stmt->affected_rows; } // default return $stmt->get_result(); } private function getCurrentTime(){ return microtime(true); } /** * @deprecated Unsafe, make sure all variables are properly escaped */ public function multi_query(array $array_query){ if(!sizeof($array_query)) return false; $start_time = $this->getCurrentTime(); // late connect if(!$this->connect()) { if($this->debug) $this->setTrace(join("; ", $array_query), $start_time, 'multi_query: Connection fails'); return false; } $multi_query = join(";", $array_query); $multi_query = str_replace("\n", " ", $multi_query); //remove double ; if exist $multi_query = preg_replace("/;(\s+)?;/i", ";", $multi_query); $set = 0; $list = []; if(mysqli_multi_query($this->connection, $multi_query)){ // flush multi_queries, so any query after can run do { $set ++; /* store first result set */ if ($result = mysqli_store_result($this->connection)) { /*while ($row = mysqli_fetch_assoc($result)) { $list[$set][] = $row; }*/ $list[$set] = $this->fetchAll($result); mysqli_free_result($result); } } while (mysqli_more_results($this->connection) && mysqli_next_result($this->connection)); } if($this->debug) $this->setTrace(join(";", $array_query), $start_time); return $list; } // a simple utitily method which we use very frequently public function getItemInfo($table_name, $id_value, $id_field = 'id'){ return $this->select( $table_name, [], [ $id_field => ["=", $id_value], ], '', 1 ); } //$table_name = 'table_abc' /*$fields = array( "field_name_1", );*/ /*$where_condition = array( "field_name_1" => ["=", "value_1"], "field_name_2" => ["LIKE", "value_1"], );*/ public function select($table_name, $fields =[], $where_condition=[], $order_by = '', $limit='1'){ // connect on demand if(!$this->connect()) { if($this->debug) $this->setTrace($table_name, 0, 'select: Connection fails'); return false; } $start_time = $this->getCurrentTime(); $check_fields = []; if(sizeof($fields) && $fields[0] != '*') $check_fields = $fields; $check_fields = array_merge($check_fields, array_keys($where_condition)); $cols_in_tables = $this->filterColumnInTable($table_name, $check_fields); $bind_types = []; $bind_values = []; $condition_list = []; $permitted_operator = [">", ">=", "<", "<=", "!=", "=", "BETWEEN", "LIKE", "IS"]; // https://dev.mysql.com/doc/refman/8.0/en/non-typed-operators.html foreach ($where_condition as $field_name => $operator_value) { // if any field invalid, stop the query if(!in_array($field_name, $cols_in_tables)) { echo "Invalid field_name ".$field_name; return false; }; // invalidate if(!is_array($operator_value) || sizeof($operator_value) != 2) continue; $operator = $operator_value[0]; if(!in_array($operator, $permitted_operator)) continue; $value = $operator_value[1]; $condition_list[] = " `".$field_name."` ".$operator." ? "; $bind_types[] = (is_int($value)) ? 'd' : 's'; $bind_values[] = $value; } $select_fields = (sizeof($fields) > 0) ? array_map(function ($field){ return "`".$field."`";}, $fields) : ["*"]; $order_by_query = ($order_by) ? "ORDER BY ".preg_replace("/[^a-z0-9\s_]/i", "", $order_by) : ""; $safe_limit = ($limit) ? preg_replace("/[^0-9\,\s]/i", "", $limit) : "1"; $where_condition = (sizeof($condition_list)) ? " AND " . join(" AND ", $condition_list) : ""; $query = "SELECT ".join(',', $select_fields)." FROM `".$table_name."` WHERE 1 ".$where_condition." ".$order_by_query." LIMIT ".$safe_limit; $stmt = $this->connection->prepare( $query ); if(!$stmt) return false; if(sizeof($bind_types)) { $stmt->bind_param(join('', $bind_types), ...$bind_values); } $stmt->execute(); $result = $stmt->get_result(); if($this->debug) $this->setTrace($query, $start_time); return ($safe_limit == '1') ? $result->fetch_assoc() : $result->fetch_all(); } protected function filterColumnInTable($table_name, array $list_check_fields) { $table_columns = $this->getTableInfo($table_name); $safe_fields = []; foreach ($list_check_fields as $field_name) { // make sure the field exists if(!array_key_exists($field_name, $table_columns)) continue; $safe_fields[] = $field_name; } return $safe_fields; } //fetch array /** * @deprecated */ public function fetchArray(\mysqli_result $resource){ return $resource->fetch_array(); } /** * @param \mysqli_result | false $resource * @return array|null */ public function fetchAssoc( $resource){ if(!$resource) { //die("Resource failed: code fetchAssoc 122"); return null; } return $resource->fetch_assoc(); } /** * @param \mysqli_result | false $resource * @return array|mixed */ public function fetchAll($resource){ if(!$resource) return []; //some hosting does not enable this function, so this is the work-around if( ! function_exists("mysqli_fetch_all")) { $item_list = array(); while ($item = $this->fetchAssoc($resource)) { $item_list[] = $item; } return $item_list; } return ($resource) ? $resource->fetch_all(MYSQLI_ASSOC) : []; } //affected rows from last query public function get_affected_rows(){ return $this->connection->affected_rows; } public function getErrorNo(){ return mysqli_errno($this->connection); } public function __destruct() { $this->disconnect(); } }