Files
admin_hura_8/inc/Hura8/Database/ConnectDB.php

504 lines
15 KiB
PHP
Raw Normal View History

2024-01-29 10:39:53 +07:00
<?php
namespace Hura8\Database;
use Hura8\Traits\ClassCacheTrait;
final class ConnectDB implements iConnectDB
{
use ClassCacheTrait;
private $debug = false;
private static $instance = [];
private static $cnn_props = [];
/* @var $connection \mysqli */
private $connection;
private static $traces = [];
private $db_id = '';
private function __construct($db_id = 'main', $debug = false)
{
// enable database debug
if($debug) $this->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();
}
}