504 lines
15 KiB
PHP
504 lines
15 KiB
PHP
|
|
<?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();
|
||
|
|
}
|
||
|
|
}
|