<?php
// last edit: 12.12.2018
// old edit: 22.10.2018
/*
	вариант работы с БД MySQL
*/

/*
краткое содержание возможностей:
	DB::$connection
	DB::$result
	DB::$debug

	DB::connect(($hostname, $database, $username, $password [, $port])
	DB::debug_add($group, $str)
	DB::debug_add_error()
	DB::get_debug([$delimiter])
	
	### прямой функционал ###
	DB::query($sql)
	DB::rows_count()
	DB::affect_rows()
	DB::count_fields()
	DB::result_all()
	DB::esc($value)
	
	DB::page($limit [,$page])
	DB::limit($limit [,$offset])
	DB::group($ar_fields)
	DB::sort($ar_fields)

	### вспомогательные функции ###
	DB::where_to_s( $array_where_params [,$logic_operator])

	### Упрощенные методы работы с БД ###
	DB::getFields()
	DB::table($sql [,$is_one])
	DB::select( $table_name [, $fields, $where, $other_sql, $is_one])
	DB::select_one( $table_name [, $fields, $where, $other_sql])
	DB::_insert($table_name, $fields, $values [,$other_sql])
	DB::_insertGetId($table_name, $fields, $values [,$other])
	DB::insert($table_name, $fields_values [,$other_sql])
	DB::insertGetId($table_name, $fields_values [,$other_sql])
	DB::delete($table_name [, $where, $other_sql])
	DB::update($table_name, $values [, $where, $other_sql])
	DB::count($table_name [, $where, $other_sql])
	DB::count_page($table_name, $limit [, $where, $other_sql])

*/

class DisconectorDB{ // на всякий случай, что бы при завершении скрипта было точное отсоединения от БД
	function __destruct(){
        DB::$connection->close();
    }
}

class DB { // класс для работы с БД MySQL.
	static public 
		$connection,    // хранит ресурсы соединения
		$result,        // хранит ресурсы последнего результата запроса
		$debug = false, // если true, то будет вестись логирование всех запросов, 
						//    и в любой момент можно получить этот список вызвав DB::get_debug()
		$last_count_row = false; // хранит в себе кол-во строк последнего запроса, обнуляется после использования count_page

	static private $disconector, $debugList = [];

	// в данной константе храняться операторы, которые допустимы для сравнения в поле WHERE
	const OPERATORS_WHERE = ['=','>','<','>=','<=>','<=','!=','IN','<>', 'IS', 'LIKE', 'RLIKE', 'NOT LIKE', 'NOT RLIKE', 'REGEXP','NOT REGEXP'];

	// в данной константе храняться значения, которые не нужно приводить в безопасную форму
	const VALUE_NO_STRING = [
		'NULL', 'NOT NULL', 'TRUE', 'NOT TRUE', 'FALSE', 'NOT FALSE', 
		'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'
	];

	const VALUE_CMP_FOR_IS = [
		'NULL', 'NOT NULL'
	];
	

	 /**
     * Установка соединения с СУБД
     * 
     * @param string $host     Хост размещения СУБД
     * @param string $db       Название БД
     * @param string $user     Имя пользователя
     * @param string $password Пароль пользователя
     */
	static function connect($hostname, $database, $username, $password, $port = '3306') {
		self::$connection = new \mysqli($hostname, $username, $password, $database, $port);

		if (self::$connection->connect_error) {
			self::debug_add_error();
			die("<b>Приносим наши извинения!</b> <br/>В настоящее время на сайте ведутся технические работы!<br/>");
		}else self::$disconector = new DisconectorDB;

		self::$connection->set_charset("utf8");
		self::$connection->query("SET SQL_MODE = ''");
	}

    /**
	* Записть в отладочной информации в накопитель
	*
	* @param string $group категория записи, может быть любой, для понятия где произошла запись
	* @param string $str   сообщение которое отразит ситуацию
	*/
    static function debug_add($group, $str){
    	self::$debugList[] = date_format(date_create(),'d/m/Y G:i:s.u # ') . '['. $group . ']: ' . $str;
    }

    /**
	* Записть в отладочной информации об ошипке, в накопитель
	*/
    static function debug_add_error(){
    	self::debug_add('ERROR', self::$connection->error.' (#'.self::$connection->errno.')');
    }

    /**
	* Получение всей отладочной информации из накопителя
	*
	* @param string $delemiter  позволяет указать свой разделитель, который будет вставлен между строками информации
	* @return string
	*/
    static function get_debug($delimiter = "<br>"){
    	return implode($delimiter, self::$debugList);
    }

    /*****************************************/
    /**         прямой функционал           **/
    /*****************************************/


    /**
	* Выполнение SQL-скрипта
	*
	* @param string $sql  скрипт который должен выполнить СУБД
	* @return boolean
	*/
    static function query( $sql ){
    	if(self::$debug){
    		self::debug_add('query', $sql);
    		$timer = new Timer;
    	}

    	self::$result = self::$connection->query($sql);
    	if(self::$debug)
    		self::debug_add('query', "worked time: $timer");
    	return !!self::$result;
    }

    /**
	* Количество строк результата запроса (выполнения SQL-скрипта)
	* @return integer
	*/
    static function rows_count(){
    	if(!self::$result) return -1;
    	return self::$result->num_rows;
    }

    /**
	* Количество затронутых строк в результате запроса (выполнения SQL-скрипта)
	* @return integer
	*/
    static function affect_rows(){
    	if(!self::$result) return 0;
    	return self::$connection->affected_rows;
    }

    /**
	* Количество полей/колонок результата запроса (выполнения SQL-скрипта)
	* @return integer
	*/
    static function count_fields(){
    	if(!self::$result) return -1;
    	return self::$result->fields_count;
    }

    /**
	* Возвращат весь результат в виде массива ассоциативных массивов
	* @return array
	*/
    static function result_all(){
    	if(!self::$result) return [];
    	return self::$result->fetch_all(MYSQLI_ASSOC);
    }

    /**
	* Обработка строки для использования в запросе, включая добавление кавычек 
	*
	* @param mixed $value  значение которое надо привести в безопасную для БД форму
	* @return string
	*/
	static function esc($value) {
		
		if( is_null($value)) $value = 'NULL';
		if( $value === false) $value = 'FALSE';
		if( $value === true) $value = 'TRUE';
		if( is_numeric($value) && is_string($value) && strlen($value)>1 && strpos($value,'.')===false && $value{0}=='0')
			$value = "'$value'";
		// Если переменная - число, то экранировать её не нужно
		// если нет - то окружаем её кавычками, и экранируем
		elseif (!is_numeric($value)){
			// если magic_quotes_gpc включена - используем stripslashes
			if (get_magic_quotes_gpc()) 
				$value = stripslashes($value);

			$temp = strlen($value) < 16 ? strtoupper(trim($value)) : $value;

			if(!in_array($temp, self::VALUE_NO_STRING))
				$value = "'" . self::$connection->escape_string($value) . "'";
		}
		return $value;
	}


	static function page($limit, $page = 0){
		return " LIMIT ". ($page ? ($page * $limit).', ' : '' ) . $limit;
	}

	static function limit($limit, $offset = 0){
		return " LIMIT " . ($offset ? "$offset, " : '') . $limit;
	}

	static function group($groups){
		return ' GROUP BY ' . (is_array($groups) ? implode(', ', $groups) : $groups);
	}

	static function sort($sorts){
		if(is_array($sorts)){
			$temp = [];
			foreach($sorts as $field => $asc){
				if(is_numeric($field))
					$temp[] = $asc;
				else{
					$asc = strtoupper($asc);
					if($asc != 'DESC' && $asc != 'ASC')
						$temp[] = $field;
					else $temp[] = $field . ' ' . $asc;
				} 
			}
			$sorts = $temp;
		}
		return ' ORDER BY ' . (is_array($sorts) ? implode(', ', $sorts) : $sorts);
		
	}

	/*****************************************/
	/**       вспомогательные функции       **/
	/*****************************************/

	/**
	* Преобразовывает массив значений в строку перечисления безопасных значений для БД
	*
	* @param array $arr массив значений, которые надо преобразовать в строку
	* @return string
	*/
	static function value_arr_to_s( $arr ){
		return '('.implode(',',array_map(function($val){ return self::esc($val); }, $arr)).')';
	}

	/**
	*
	*/

	static function for_where_get_operator($value, $def_op = '='){
		if(is_array($value))
			return 'IN';

		if(is_null($value) || in_array(strtoupper($value), self::VALUE_CMP_FOR_IS))
			return 'IS';
		

		if($def_op === '') return '';

		$def_op = strtoupper($def_op);
		if(in_array($def_op, self::OPERATORS_WHERE))
			return $def_op;

		return '=';
	}

	static function for_where_cmp($key, $value){
		
		$op = strrpos($key,' ') > 0 ? '' : '=';
		if(func_num_args() == 3){
			$op = $value;
			$value = func_get_arg(2);
		}
		$op = self::for_where_get_operator($value, $op);

		return $key . ' ' . ($op ? $op.' ' : '') . (is_array($value) ? ($op == 'IN' ? self::value_arr_to_s($value) : self::esc($value[0])) : self::esc($value) );
	}

	/**
	* Преобразовывает набор условий в строку для слова WHERE в запросе
	*
	* @param array  $arr  массив, ассоциативный массив, или массив в массиве, который необходимо преобразовать в строку условия
	* @param string $op  соединительный оператор условий, может быть AND или OR
	* @return string
	*/
	static function where_to_s( $arr, $op = 'AND'){
		$res = [];
		if(isset($arr[0]) && is_string($arr[0])){
			$temp = strtolower($arr[0]);
			if($temp == 'or' || $temp == 'and')
				$op = array_shift($arr);
		}

		foreach($arr as $field => $value){
			if(is_numeric($field)){
				if(is_string($value))
					$res[] = $value;
				elseif(is_array($value)){
					if(!isset($value[0]))
						$res[] = '(' . self::where_to_s($value) . ')';
					else {
						$temp = strtolower($value[0]);
						if($temp == 'or' || $temp == 'and'){
							$temp = array_shift($value);
							$res[] = '(' . self::where_to_s($value, $temp) . ')';
						}else switch(count($value)){
							case 1: $res[] = $value[0]; break;
							case 2: $res[] = self::for_where_cmp($value[0], $value[1]); break;
							case 3: $res[] = self::for_where_cmp($value[0], $value[1], $value[2]); break;
						}
					}
				}
			} else $res[] = self::for_where_cmp($field, $value);
		}
		return implode(" $op ", $res);
	}


	/*****************************************/
	/**    Упрощенные методы работы с БД    **/
	/*****************************************/

	/**
	* Возвращаем все поля пришедшие из результата запроса.
	* @return array
	*/
	static function getFields(){ 
		if(!self::$result) return [];
		$res = [];
		$count = self::count_fields();
		if($count > 0)
			for($i = 0; $i < $count; $i++)
				$res[] = self::$result->fetch_field_direct($i)['name'];
		return $res;
	}


	/**
	* Выполняет SQL-запрос, предпологается, что результат вернет таблицу.
	*
	* @param string  $sql  скрипт запроса, который необходимо выполнить
	* @param boolean $one  если TRUE, то будет возвращена только первая строка результата, в противном случае, будет возвращен массив всех записей результата.
	* @return array
	*/
	static function table($sql, $one = false){
		if(self::query($sql)){
			$num_rows = self::rows_count();
			if(self::$debug)
				self::debug_add('SELECT', "count rows $num_rows");
			if($num_rows <= 0)
				$result = [];
			elseif($one)
				$result = self::$result->fetch_assoc();
			else 
				$result = self::result_all();
			self::$result->close();
			return $result;
		} else {
			if(self::$debug)
				self::debug_add_error();
			return [];
		}		
	}


	/**
	* Выполняет запрос выборки данных из таблицы
	*
	* @param string       $table_name  имя таблице из которой необходимо сделать выборку
	* @param string|array $fields      строка или массив полей, которые необходимо получить в результате запроса выборки
	* @param string|array $where       строка или массив, условий выборки запроса
	* @param string       $other       дополнительная строка манимуляций с запросом (например сортировка, группировка или установка лимита)
	* @param boolean      $one         если TRUE, то будет возвращена только первая строка результата, в противном случае, будет возвращен массив всех записей результата.
	* @return array 
	*/
	static function select( $table_name, $fields = '*', $where = '', $other = '', $one = false){
		if(is_array($fields))
			$fields = implode(', ', $fields);

		if(is_array($where))
			$where = self::where_to_s($where);

		$sql = "SELECT $fields FROM $table_name " . ($where ? "WHERE $where " : '') . $other . ';';

		return self::table($sql, $one);
	}

	/**
	* Аналогична методу "select", но всегда возвращает только одну запись из результата запроса
	*
	* @param string       $table_name  имя таблице из которой необходимо сделать выборку
	* @param string|array $fields      строка или массив полей, которые необходимо получить в результате запроса выборки
	* @param string|array $where       строка или массив, условий выборки запроса
	* @param string       $other       дополнительная строка манимуляций с запросом (например сортировка, группировка или установка лимита)
	* @return array 
	*/
	static function select_one($table_name, $fields = '*', $where = '', $other = ''){
		return self::select( $table_name, $fields, $where, $other, true);
	}

	/**
	* Делает втавку новой записи или новых записей в таблицу.
	*
	* @param string       $table_name  имя таблице из которой необходимо сделать выборку
	* @param string|array $fields      строка или массив, полей, по которым будут вставляться данные
	* @param string|array $values      строка или массив или массив массивов, значений, которые необходимо вставить
	* @param string       $other       дополнительная строка манимуляций с запросом
	* @return boolean
	*/
	static function _insert($table_name, $fields, $values, $other = ''){
		if(is_array($fields))
			$fields = implode(', ', $fields);
		if(is_array($values)){
			if(is_array($values[0])){
				$temp = [];
				foreach($values as $arr)
					$temp[] = self::value_arr_to_s($arr);
				$values = implode(', ', $temp);
			} else
				$values = self::value_arr_to_s($values);
		}

		$sql = "INSERT INTO $table_name($fields) VALUES $values $other;";

		if(self::query($sql)){
			if(self::$result instanceof \mysqli_result)
				self::$result->close();
			if(self::$debug)
				self::debug_add('INSERT','insert ok');
			return true;	
		} else {
			if(self::$debug)
				self::debug_add_error();
			return false;
		}
	}

	/**
	* Делает втавку новой записи или новых записей в таблицу, и возвращает их ID.
	*
	* @param string       $table_name     имя таблице из которой необходимо сделать выборку
	* @param string|array $fields         строка или массив, полей, по которым будут вставляться данные
	* @param string|array $values         строка или массив или массив массивов, значений, которые необходимо вставить
	* @param string|array $fields_result  строка или массив полей, значения которых необходимо вернуть после вставки записи, как результат
	* @return array|integer
	*/
	static function _insertGetId($table_name, $fields, $values, $other = ''){
		if(self::_insert($table_name, $fields, $values)){
			$id = self::$connection->insert_id;
			if(self::$result instanceof \mysqli_result)
				self::$result->close();
			if(self::$debug)
				self::debug_add('INSERT', "last id: $id");
			return $id;
		}
	}

	/**
	* Аналог функции "_insert", но вставляет всегда только 1 запись, с массивом (поле=>значение)
	*
	* @param string    $table_name  имя таблице из которой необходимо сделать выборку
	* @param array     $values      массив значений полей, в котором каждый ключ является именем поля.
	* @param string    $other       дополнительная строка манимуляций с запросом
	* @return boolean
	*/
	static function insert($table_name, $values, $other = ''){
		return self::_insert($table_name, array_keys($values), array_values($values), $other);
	}

	/**
	* Аналог функции "_insertGetId", но вставляет всегда только 1 запись, с массивом (поле=>значение)
	*
	* @param string       $table_name     имя таблице из которой необходимо сделать выборку
	* @param array        $values         массив значений полей, в котором каждый ключ является именем поля.
	* @param string|array $fields_result  строка или массив полей, значения которых необходимо вернуть после вставки записи, как результат
	* @return integer
	*/
	static function insertGetId($table_name, $values, $other = ''){
		return self::_insertGetId($table_name, array_keys($values), array_values($values));
	}

	/**
	* Делает запрос на удаление данных из таблицы
	*
	* @param string       $table_name  имя таблице из которой необходимо сделать выборку
	* @param string|array $where       строка или массив, условий выборки запроса
	* @param string       $other       дополнительная строка манимуляций с запросом 
	* @return integer
	*/
	static function delete($table_name, $where = '', $other = ''){
		if(is_array($where))
			$where = self::where_to_s($where);

		$sql = "DELETE FROM $table_name " . ($where ? "WHERE $where " : '') . $other . ';';

		if(self::query($sql)){
			$temp = self::affect_rows();
			if(self::$result instanceof \mysqli_result)
				self::$result->close();
			if(self::$debug)
				self::debug_add('DELETE', "affect rows $temp");
			return $temp;
		} else {
			if(self::$debug)
				self::debug_add_error();
			return 0;
		}
	}

	/**
	* Делает запрос на изменение данных в таблице
	*
	* @param string       $table_name  имя таблице из которой необходимо сделать выборку
	* @param array        $values      массив значений полей, в котором каждый ключ является именем поля.
	* @param string|array $where       строка или массив, условий выборки запроса
	* @param string       $other       дополнительная строка манимуляций с запросом 
	* @return integer
	*/
	static function update($table_name, $values, $where = '', $other = ''){
		if(is_array($where))
			$where = self::where_to_s($where);

		if(is_array($values)){
			$temp = [];
			foreach($values as $field => $value){
				if(is_numeric($field))
					$temp[] = $value;
				else 
					$temp[] = $field .'='. self::esc($value);
			}
			$values = implode(', ', $temp);
		}

		$sql = "UPDATE $table_name SET $values " . ($where ? "WHERE $where " : '') . $other . ';';
		
		if(self::query($sql)){
			$temp = self::affect_rows();
			if(self::$result instanceof \mysqli_result)
				self::$result->close();
			if(self::$debug)
				self::debug_add('UPDATE', "affect rows $temp");
			return $temp;
		} else {
			if(self::$debug)
				self::debug_add_error();
			return 0;	
		}
	}

	/**
	* Делает запрос на получение кол-ва строк в таблице, по указанным условиям
	*
	* @param string       $table_name  имя таблице из которой необходимо сделать выборку
	* @param string|array $where       строка или массив, условий выборки запроса
	* @param string       $other       дополнительная строка манимуляций с запросом 
	* @return int
	*/
	static function count($table_name, $where = '', $other = ''){
		$result = self::select_one($table_name, 'count(*) as count_rows_this_table', $where, $other);
		static::$last_count_row = 0;
		if($result)
			static::$last_count_row = $result['count_rows_this_table'];
		return static::$last_count_row;
	}

	/**
	* Делает запрос на получение кол-ва строк в таблице, по указанным условиям, 
	* а затем делает рассчет кол-ва страниц, по указанному лимиту
	*
	* @param string       $table_name  имя таблице из которой необходимо сделать выборку
	* @param int          $limit       кол-во строк тфблицы на одну страницу
	* @param string|array $where       строка или массив, условий выборки запроса
	* @param string       $other       дополнительная строка манимуляций с запросом 
	* @return int
	*/
	static function count_page($table_name, $limit, $where = '', $other = ''){
		if(static::$last_count_row === false)
			$result = self::count($table_name, $where, $other);
		else $result = static::$last_count_row;
		if($result){
			$temp = $result / $limit;
			$res = intval($temp);
			if($temp - $res > 0)
				$res++;
			static::$last_count_row = false;
			return $res;
		}
		static::$last_count_row = false;
		return 0;
	}
}