battles/classes/Database/Mysql.php
Igor Barkov (iwork) 3502904656 Рефакторинг, очистка, работа над ошибками, связанными с базой, отказ от глобальной переменной $user во многих файлах.
Singleton в некоторых местах вместо решения #42.
Новые шаги для решения #16 и #52.
Closes #42.
Closes #32.
Closes #31.
2022-01-27 01:15:33 +02:00

1045 lines
45 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @author Vasiliy Makogon, makogon-vs@yandex.ru
* @link https://github.com/Vasiliy-Makogon/Database/
*
* ---------------------------------------------------------------------------------------------------------------------
* Библиотека для удобной и безопасной работы с СУБД MySql на базе расширения PHP mysqli.
* ---------------------------------------------------------------------------------------------------------------------
*
* Библиотека имулирует технологию Prepared statement (или placeholders) - для формирования корректных SQL-запросов
* (т.е. запросов, исключающих SQL-уязвимости), в строке запроса вместо значений пишутся специальные типизированные
* маркеры - т.н. "заполнители", а сами данные передаются "позже", в качестве последующих аргументов основного метода,
* выполняющего SQL-запрос - Mysql::query($sql [, $arg, $...]):
*
* $getInstance->query('SELECT * FROM `table` WHERE `name` = "?s" AND `age` = ?i', $_POST['name'], $_POST['age']);
*
* Аргументы SQL-запроса, прошедшие через систему placeholders данного класса, экранируются специальными функциями
* экранирования, в зависимости от типа заполнителей. Т.е. вам теперь нет необходимости заключать переменные в функции
* экранирования типа mysqli_real_escape_string($value) или приводить их к числовому типу через (int)$value.
*
* Кроме того, данный класс позволяет:
* - получать "подготовленный" SQL-запрос для отладки, т.е. запрос с реальными значениями, что невозможно сделать
* используя "сырые" драйверы PHP типа PDO.
* - получать список всех запросов выполненных в рамках одного подключения к Mysql-серверу.
*
*
* ---------------------------------------------------------------------------------------------------------------------
* Режимы работы.
* ---------------------------------------------------------------------------------------------------------------------
*
* Существует два режима работы класса:
* Mysql::MODE_STRICT - строгий режим соответствия типа заполнителя и типа аргумента.
* Mysql::MODE_TRANSFORM - режим преобразования аргумента к типу заполнителя при несовпадении
* типа заполнителя и типа аргумента.
*
* Режим Mysql::MODE_TRANSFORM установлен по умолчанию и является основным для большинства приложений.
* Если же вам нужна максимальная прозрачность операций над типами данных, производимых библиотекой Datavase,
* установите режим Mysql::MODE_STRICT.
*
*
* MODE_STRICT
*
* В "строгом" режиме MODE_STRICT аргументы, передаваемые в основной метод Mysql::query(),
* должны соответствовать типу заполнителя.
* Например, попытка передать в качестве аргумента значение 55.5 или '55.5' для заполнителя целочисленного типа ?i
* приведет к выбросу исключения:
*
* $getInstance->setTypeMode(Mysql::MODE_STRICT); // устанавливаем строгий режим работы
* $getInstance->query('SELECT ?i', 55.5); // Попытка указать для заполнителя типа int значение типа double в шаблоне запроса SELECT ?i
*
* Это утверждение не относится к числам (целым и с плавающей точкой), заключенным в строки.
* С точки зрения библиотеки, строка '123' и значение 123 являются типом int.
*
*
* MODE_TRANSFORM
*
* Режим MODE_TRANSFORM является "щадящим" режимом и при несоответствии типа заполнителя и типа аргумента не генерирует
* исключение, а пытается преобразовать аргумент к нужному типу заполнителя посредством самого языка PHP.
*
* Допускаются следующие преобразования:
*
* К типу int приводятся (заполнитель ?i):
* - числа с плавающей точкой, представленные как строка или тип double
* - bool
* - null
*
* К типу double приводятся (заполнитель ?d):
* - целые числа, представленные как строка или тип int
* - bool
* - null
*
* К типу string приводятся (заполнитель ?s):
* - значение boolean TRUE преобразуется в строку "1", а значение FALSE преобразуется в "" (пустую строку).
* - значение типа numeric преобразуется в строку согласно правилам преобразования, определенным языком.
* - NULL преобразуется в пустую строку.
*
* К типу null приводятся (заполнитель ?n):
* - любые аргументы
*
* Для массивов, объектов и ресурсов преобразования не допускаются.
*
*
* ---------------------------------------------------------------------------------------------------------------------
* Типы маркеров-заполнителей
* ---------------------------------------------------------------------------------------------------------------------
*
* ?f - заполнитель имени таблицы или поля (первая буква слова field).
* Данный заполнитель предназначен для случаев, когда имя таблицы или поля передается в запроос через аргумент.
*
* ?i - заполнитель целого числа (первая буква слова integer).
* В режиме MODE_TRANSFORM любые скалярные аргументы принудительно приводятся к типу integer
* согласно правилам преобразования к типу integer в PHP.
*
* ?d - заполнитель числа с плавающей точкой (первая буква слова double).
* В режиме MODE_TRANSFORM любые скалярные аргументы принудительно приводятся к типу float
* согласно правилам преобразования к типу float в PHP.
*
* ?s - заполнитель строкового типа (первая буква слова string).
* В режиме MODE_TRANSFORM любые скалярные аргументы принудительно приводятся к типу string
* согласно правилам преобразования к типу string в PHP
* и экранируются с помощью функции PHP mysqli_real_escape_string().
*
* ?S - заполнитель строкового типа для подстановки в SQL-оператор LIKE (первая буква слова string).
* В режиме MODE_TRANSFORM Любые скалярные аргументы принудительно приводятся к типу string
* согласно правилам преобразования к типу string в PHP
* и экранируются с помощью функции PHP mysqli_real_escape_string() + экранирование спецсимволов,
* используемых в операторе LIKE (%_).
*
* ?n - заполнитель NULL типа (первая буква слова null).
* В режиме MODE_TRANSFORM любые аргументы игнорируются, заполнители заменяются на строку `NULL` в SQL запросе.
*
* ?A* - заполнитель ассоциативного множества для ассоциативного массива-аргумента, генерирующий последовательность
* пар ключ => значение.
* Пример: "key_1" = "val_1", "key_2" = "val_2", ...
*
* ?a* - заполнитель множества из простого (или также ассоциативного) массива-аргумента, генерирующий последовательность
* значений.
* Пример: "val_1", "val_2", ...
*
* где * после маркера заполнителя - один из типов:
* - i (int)
* - p (float)
* - s (string)
* правила преобразования и экранирования такие же, как и для одиночных скалярных аргументов (см. выше).
*
* ?A[?n, ?s, ?i, ?d] - заполнитель ассоциативного множества с явным указанием типа и количества аргументов,
* генерирующий последовательность пар ключ => значение.
* Пример: "key_1" = "val_1", "key_2" => "val_2", ...
*
* ?a[?n, ?s, ?i, ?d] - заполнитель множества с явным указанием типа и количества аргументов, генерирующий
* последовательность значений.
* Пример: "val_1", "val_2", ...
*
*
* ---------------------------------------------------------------------------------------------------------------------
* Ограничивающие кавчки
* ---------------------------------------------------------------------------------------------------------------------
*
* Данный класс при формировании SQL-запроса НЕ занимается проставлением ограничивающих кавычек для одиночных
* заполнителей скалярного типа, таких как ?i, ?d и ?s. Это сделано по идеологическим соображениям,
* автоподстановка кавычек может стать ограничением для возможностей SQL.
* Например, выражение
* $getInstance->query('SELECT "Total: ?s"', '200');
* вернёт строку
* 'Total: 200'
* Если бы кавычки, ограничивающие строковой литерал, ставились бы автоматически,
* то вышеприведённое условие вернуло бы строку
* 'Total: "200"'
* что было бы не ожидаемым поведением.
*
* Тем не менее, для перечислений ?as, ?ai, ?ap, ?As, ?Ai и ?Ap ограничивающие кавычки ставятся принудительно, т.к.
* перечисления всегда используются в запросах, где наличие кавчек обязательно или не играет роли (а так ли это?):
*
* $getInstance->query('INSERT INTO `test` SET ?As', array('name' => 'Маша', 'age' => '23', 'adress' => 'Москва'));
* -> INSERT INTO test SET `name` = "Маша", `age` = "23", `adress` = "Москва"
*
* $getInstance->query('SELECT * FROM table WHERE field IN (?as)', array('55', '12', '132'));
* -> SELECT * FROM table WHERE field IN ("55", "12", "132")
*
* Также исключения составляют заполнители типа ?f, предназначенные для передачи в запрос имен таблиц и полей.
* Аргумент заполнителя ?f всегда обрамляется обратными кавычками (`):
*
* $getInstance->query('SELECT ?f FROM ?f', 'my_field', 'my_table');
* -> SELECT `my_field` FROM `my_table`
*/
namespace Krugozor\Database\Mysql;
use mysqli;
use mysqli_result;
class Mysql
{
/**
* Строгий режим типизации.
* Если тип заполнителя не совпадает с типом аргумента, то будет выброшено исключение.
* Пример такой ситуации:
*
* $getInstance->query('SELECT * FROM `table` WHERE `id` = ?i', '2+мусор');
*
* - в данной ситуации тип заполнителя ?i - число или числовая строка,
* а в качестве аргумента передаётся строка '2+мусор' не являющаяся ни числом, ни числовой строкой.
*
* @var int
*/
const MODE_STRICT = 1;
/**
* Режим преобразования.
* Если тип заполнителя не совпадает с типом аргумента, аргумент принудительно будет приведён
* к нужному типу - к типу заполнителя.
* Пример такой ситуации:
*
* $getInstance->query('SELECT * FROM `table` WHERE `id` = ?i', '2+мусор');
*
* - в данной ситуации тип заполнителя ?i - число или числовая строка,
* а в качестве аргумента передаётся строка '2+мусор' не являющаяся ни числом, ни числовой строкой.
* Строка '2+мусор' будет принудительно приведена к типу int согласно правилам преобразования типов в PHP.
*
* @var int
*/
const MODE_TRANSFORM = 2;
/**
* Режим работы инстанцированного объекта.
* См. описание констант self::MODE_STRICT и self::MODE_TRANSFORM.
*
* @var int
*/
protected $type_mode = self::MODE_TRANSFORM;
protected $server;
protected $user;
protected $password;
protected $port;
protected $socket;
/**
* Имя текущей БД.
*
* @var string
*/
protected $database_name;
/**
* Стандартный объект соединения сервером MySQL.
*
* @var mysqli
*/
protected $mysqli;
/**
* Строка последнего SQL-запроса до преобразования.
*
* @var string
*/
private $original_query;
/**
* Строка последнего SQL-запроса после преобразования.
*
* @var string
*/
private $query;
/**
* Массив со всеми запросами, которые были выполнены объектом.
* Ключи - SQL после преобразования, значения - SQL до преобразования.
*
* @var array
*/
private $queries = array();
/**
* Накапливать ли в хранилище $this->queries исполненные запросы.
*
* @var bool
*/
private $store_queries = true;
/**
* Создает инстанс данного класса.
*
* @param string $server имя сервера
* @param string $username имя пользователя
* @param string $password пароль
* @param string $port порт
* @param string $socket сокет
*/
public static function create($server, $username, $password, $port=null, $socket=null)
{
return new self($server, $username, $password, $port, $socket);
}
/**
* Задает набор символов по умолчанию.
* Вызов данного метода эквивалентен следующей установки конфигурации MySql-сервера:
* SET character_set_client = charset_name;
* SET character_set_results = charset_name;
* SET character_set_connection = charset_name;
*
* @param string $charset
* @return Mysql
*/
public function setCharset($charset)
{
if (!$this->mysqli->set_charset($charset)) {
throw new Exception(__METHOD__ . ': ' . $this->mysqli->error);
}
return $this;
}
/**
* Возвращает кодировку по умолчанию, установленную для соединения с БД.
*
* @param void
* @return string
*/
public function getCharset()
{
return $this->mysqli->character_set_name();
}
/**
* Устанавливает имя используемой СУБД.
*
* @param string имя базы данных
* @return Mysql
*/
public function setDatabaseName($database_name)
{
if (!$database_name) {
throw new Exception(__METHOD__ . ': Не указано имя базы данных');
}
$this->database_name = $database_name;
if (!$this->mysqli->select_db($this->database_name)) {
throw new Exception(__METHOD__ . ': ' . $this->mysqli->error);
}
return $this;
}
/**
* Возвращает имя текущей БД.
*
* @param void
* @return string
*/
public function getDatabaseName()
{
return $this->database_name;
}
/**
* Устанавливает режим поведения при несовпадении типа заполнителя и типа аргумента.
*
* @param $value int
* @return Mysql
*/
public function setTypeMode($value)
{
if (!in_array($value, array(self::MODE_STRICT, self::MODE_TRANSFORM))) {
throw new Exception(__METHOD__ . ': Указан неизвестный тип режима');
}
$this->type_mode = $value;
return $this;
}
/**
* Устанавливает свойство $this->store_queries, отвечающее за накопление исполненных запросов в
* хранилище $this->queries.
*
* @param bool $value
* @return Mysql
*/
public function setStoreQueries($value)
{
$this->store_queries = (bool) $value;
return $this;
}
/**
* Выполняет SQL-запрос.
* Принимает обязательный параметр - SQL-запрос и, в случае наличия,
* любое количество аргументов - значения заполнителей.
*
* @param string строка SQL-запроса
* @param mixed аргументы для заполнителей
* @return bool|Statement false в случае ошибки, в обратном случае объект результата
* @throws Exception
*/
public function query()
{
if (!func_num_args()) {
return false;
}
$args = func_get_args();
$query = $this->original_query = array_shift($args);
$this->query = $this->parse($query, $args);
$result = $this->mysqli->query($this->query);
if ($this->store_queries) {
$this->queries[$this->query] = $this->original_query;
}
if ($result === false) {
throw new Exception(__METHOD__ . ': ' . $this->mysqli->error . '; SQL: ' . $this->query);
}
if (is_object($result) && $result instanceof mysqli_result) {
return new Statement($result);
}
return $result;
}
/**
* Поведение аналогично методу self::query(), только метод принимает только два параметра -
* SQL запрос $query и массив аргументов $arguments, которые и будут заменены на заменители в той
* последовательности, в которой они представленны в массиве $arguments.
*
* @param string
* @param array
* @return bool|Mysql_Statement
*/
public function queryArguments($query, array $arguments=array())
{
array_unshift($arguments, $query);
return call_user_func_array(array($this, 'query'), $arguments);
}
/**
* Обёртка над методом $this->parse().
* Применяется для случаев, когда SQL-запрос формируется частями.
*
* Пример:
* $getInstance->prepare('WHERE `name` = "?s" OR `id` IN(?ai)', 'Василий', array(1, 2));
* Результат:
* WHERE `name` = "Василий" OR `id` IN(1, 2)
*
* @param string SQL-запрос или его часть
* @param mixed аргументы заполнителей
* @return boolean|string
*/
public function prepare()
{
if (!func_num_args()) {
return false;
}
$args = func_get_args();
$query = array_shift($args);
return $this->parse($query, $args);
}
/**
* Получает количество рядов, задействованных в предыдущей MySQL-операции.
* Возвращает количество рядов, задействованных в последнем запросе INSERT, UPDATE или DELETE.
* Если последним запросом был DELETE без оператора WHERE,
* все записи таблицы будут удалены, но функция возвратит ноль.
*
* @see mysqli_affected_rows
* @param void
* @return int
*/
public function getAffectedRows()
{
return $this->mysqli->affected_rows;
}
/**
* Возвращает последний оригинальный SQL-запрос до преобразования.
*
* @param void
* @return string
*/
public function getOriginalQueryString()
{
return $this->original_query;
}
/**
* Возвращает последний выполненный MySQL-запрос (после преобразования).
*
* @param void
* @return string
*/
public function getQueryString()
{
return $this->query;
}
/**
* Возвращает массив со всеми исполненными SQL-запросами в рамках текущего объекта.
*
* @param void
* @return array
*/
public function getQueries()
{
return $this->queries;
}
/**
* Возвращает id, сгенерированный предыдущей операцией INSERT.
*
* @param void
* @return int
*/
public function getLastInsertId()
{
return $this->mysqli->insert_id;
}
/**
* Возвращает оригинальный объект mysqli.
*
* @param void
* @return mysqli
*/
public function getMysqli()
{
return $this->mysqli;
}
public function __destruct()
{
$this->close();
}
/**
* @param string $server
* @param string $username
* @param string $password
* @param string $port
* @param string $socket
* @return void
*/
private function __construct($server, $user, $password, $port, $socket)
{
$this->server = $server;
$this->user = $user;
$this->password = $password;
$this->port = $port;
$this->socket = $socket;
$this->connect();
}
/**
* Устанавливает соеденение с базой данных.
*
* @param void
* @return void
*/
private function connect()
{
if (!is_object($this->mysqli) || !$this->mysqli instanceof mysqli) {
$this->mysqli = @new mysqli($this->server, $this->user, $this->password, null, $this->port, $this->socket);
if ($this->mysqli->connect_error) {
throw new Exception(__METHOD__ . ': ' . $this->mysqli->connect_error);
}
}
}
/**
* Закрывает MySQL-соединение.
*
* @param void
* @return Mysql
*/
private function close()
{
if (is_object($this->mysqli) && $this->mysqli instanceof mysqli) {
@$this->mysqli->close();
}
return $this;
}
/**
* Возвращает экранированную строку для placeholder-а поиска LIKE (?S).
*
* @param string $var строка в которой необходимо экранировать спец. символы
* @param string $chars набор символов, которые так же необходимо экранировать.
* По умолчанию экранируются следующие символы: `'"%_`.
* @return string
*/
private function escapeLike($var, $chars = "%_")
{
$var = str_replace('\\', '\\\\', $var);
$var = $this->mysqlRealEscapeString($var);
if ($chars) {
$var = addCslashes($var, $chars);
}
return $var;
}
/**
* Экранирует специальные символы в строке для использования в SQL выражении,
* используя текущий набор символов соединения.
*
* @see mysqli_real_escape_string
* @param string
* @return string
*/
private function mysqlRealEscapeString($value)
{
return $this->mysqli->real_escape_string($value);
}
/**
* Возвращает строку описания ошибки при несовпадении типов заполнителей и аргументов.
*
* @param string $type тип заполнителя
* @param mixed $value значение аргумента
* @param string $original_query оригинальный SQL-запрос
* @return string
*/
private function createErrorMessage($type, $value, $original_query)
{
return "Попытка указать для заполнителя типа $type значение типа " . gettype($value) . " в шаблоне запроса $original_query";
}
/**
* Парсит запрос $query и подставляет в него аргументы из $args.
*
* @param string $query SQL запрос или его часть (в случае парсинга условия в скобках [])
* @param array $args аргументы заполнителей
* @param string $original_query "оригинальный", полный SQL-запрос
* @return string SQL запрос для исполнения
*/
private function parse($query, array $args, $original_query=null)
{
$original_query = $original_query ? $original_query : $query;
$offset = 0;
while (($posQM = mb_strpos($query, '?', $offset)) !== false) {
$offset = $posQM;
$placeholder_type = mb_substr($query, $posQM + 1, 1);
// Любые ситуации с нахождением знака вопроса, который не явялется заполнителем.
if ($placeholder_type == '' || !in_array($placeholder_type, array('i', 'd', 's', 'S', 'n', 'A', 'a', 'f'))) {
$offset += 1;
continue;
}
if (!$args) {
throw new Exception(
__METHOD__ . ': количество заполнителей в запросе ' . $original_query .
' не соответствует переданному количеству аргументов'
);
}
$value = array_shift($args);
$is_associative_array = false;
switch ($placeholder_type) {
// `LIKE` search escaping
case 'S':
$is_like_escaping = true;
// Simple string escaping
// В случае установки MODE_TRANSFORM режима, преобразование происходит согласно правилам php типизации
// http://php.net/manual/ru/language.types.string.php#language.types.string.casting
// для bool, null и numeric типа.
case 's':
$value = $this->getValueStringType($value, $original_query);
$value = !empty($is_like_escaping) ? $this->escapeLike($value) : $this->mysqlRealEscapeString($value);
$query = mb_substr_replace($query, $value, $posQM, 2);
$offset += mb_strlen($value);
break;
// Integer
// В случае установки MODE_TRANSFORM режима, преобразование происходит согласно правилам php типизации
// http://php.net/manual/ru/language.types.integer.php#language.types.integer.casting
// для bool, null и string типа.
case 'i':
$value = $this->getValueIntType($value, $original_query);
$query = mb_substr_replace($query, $value, $posQM, 2);
$offset += mb_strlen($value);
break;
// double
case 'd':
$value = $this->getValueFloatType($value, $original_query);
$query = mb_substr_replace($query, $value, $posQM, 2);
$offset += mb_strlen($value);
break;
// NULL insert
case 'n':
$value = $this->getValueNullType($value, $original_query);
$query = mb_substr_replace($query, $value, $posQM, 2);
$offset += mb_strlen($value);
break;
// field or table name
case 'f':
$value = $this->escapeFieldName($value, $original_query);
$query = mb_substr_replace($query, $value, $posQM, 2);
$offset += mb_strlen($value);
break;
// Парсинг массивов.
// Associative array
case 'A':
$is_associative_array = true;
// Simple array
case 'a':
$value = $this->getValueArrayType($value, $original_query);
$next_char = mb_substr($query, $posQM + 2, 1);
if ($next_char != '' && preg_match('#[sid\[]#u', $next_char, $matches)) {
// Парсим выражение вида ?a[?i, "?s", "?s"]
if ($next_char == '[' and ($close = mb_strpos($query, ']', $posQM+3)) !== false) {
// Выражение между скобками [ и ]
$array_parse = mb_substr($query, $posQM+3, $close - ($posQM+3));
$array_parse = trim($array_parse);
$placeholders = array_map('trim', explode(',', $array_parse));
if (count($value) != count($placeholders)) {
throw new Exception('Несовпадение количества аргументов и заполнителей в массиве, запрос ' . $original_query);
}
reset($value);
reset($placeholders);
$replacements = array();
foreach ($placeholders as $placeholder) {
list($key, $val) = each($value);
$replacements[$key] = $this->parse($placeholder, array($val), $original_query);
}
if (!empty($is_associative_array)) {
foreach ($replacements as $key => $val) {
$values[] = $this->escapeFieldName($key, $original_query) . ' = ' . $val;
}
$value = implode(',', $values);
} else {
$value = implode(', ', $replacements);
}
$query = mb_substr_replace($query, $value, $posQM, 4 + mb_strlen($array_parse));
$offset += mb_strlen($value);
}
// Выражение вида ?ai, ?as, ?ap
else if (preg_match('#[sid]#u', $next_char, $matches)) {
$sql = '';
$parts = array();
foreach ($value as $key => $val) {
switch ($matches[0]) {
case 's':
$val = $this->getValueStringType($val, $original_query);
$val = $this->mysqlRealEscapeString($val);
break;
case 'i':
$val = $this->getValueIntType($val, $original_query);
break;
case 'd':
$val = $this->getValueFloatType($val, $original_query);
break;
}
if (!empty($is_associative_array)) {
$parts[] = $this->escapeFieldName($key, $original_query) . ' = "' . $val . '"';
} else {
$parts[] = '"' . $val . '"';
}
}
$value = implode(', ', $parts);
$value = $value !== '' ? $value : 'NULL';
$query = mb_substr_replace($query, $value, $posQM, 3);
$offset += mb_strlen($value);
}
} else {
throw new Exception('Попытка воспользоваться заполнителем массива без указания типа данных его элементов');
}
break;
}
}
return $query;
}
/**
* В зависимости от типа режима возвращает либо строковое значение $value,
* либо кидает исключение.
*
* @param mixed $value
* @param string $original_query оригинальный SQL запрос
* @throws Exception
* @return string
*/
private function getValueStringType($value, $original_query)
{
if (!is_string($value) && $this->type_mode == self::MODE_STRICT) {
// Если это числовой string, меняем его тип для вывода в тексте исключения его типа.
if ($this->isInteger($value) || $this->isFloat($value)) {
$value += 0;
}
throw new Exception($this->createErrorMessage('string', $value, $original_query));
}
// меняем поведение PHP в отношении приведения bool к string
if (is_bool($value)) {
return (string) (int) $value;
}
if (!is_string($value) && !(is_numeric($value) || is_null($value))) {
throw new Exception($this->createErrorMessage('string', $value, $original_query));
}
return (string) $value;
}
/**
* В зависимости от типа режима возвращает либо строковое значение числа $value,
* приведенного к типу int, либо кидает исключение.
*
* @param mixed $value
* @param string $original_query оригинальный SQL запрос
* @throws Exception
* @return string
*/
private function getValueIntType($value, $original_query)
{
if ($this->isInteger($value)) {
return $value;
}
switch ($this->type_mode) {
case self::MODE_TRANSFORM:
if ($this->isFloat($value) || is_null($value) || is_bool($value)) {
return (int) $value;
}
case self::MODE_STRICT:
// Если это числовой string, меняем его тип для вывода в тексте исключения его типа.
if ($this->isFloat($value)) {
$value += 0;
}
throw new Exception($this->createErrorMessage('integer', $value, $original_query));
}
}
/**
* В зависимости от типа режима возвращает либо строковое значение числа $value,
* приведенного к типу float, либо кидает исключение.
*
* Внимание! Разделитель целой и дробной части, возвращаемый float, может не совпадать с разделителем СУБД.
* Для установки необходимого разделителя дробной части используйте setlocale().
*
* @param mixed $value
* @param string $original_query оригинальный SQL запрос
* @throws Exception
* @return string
*/
private function getValueFloatType($value, $original_query)
{
if ($this->isFloat($value)) {
return $value;
}
switch ($this->type_mode) {
case self::MODE_TRANSFORM:
if ($this->isInteger($value) || is_null($value) || is_bool($value)) {
return (float) $value;
}
case self::MODE_STRICT:
// Если это числовой string, меняем его тип на int для вывода в тексте исключения.
if ($this->isInteger($value)) {
$value += 0;
}
throw new Exception($this->createErrorMessage('double', $value, $original_query));
}
}
/**
* В зависимости от типа режима возвращает либо строковое значение 'NULL',
* либо кидает исключение.
*
* @param mixed $value
* @param string $original_query оригинальный SQL запрос
* @throws Exception
* @return string
*/
private function getValueNullType($value, $original_query)
{
if ($value !== null && $this->type_mode == self::MODE_STRICT) {
// Если это числовой string, меняем его тип для вывода в тексте исключения его типа.
if ($this->isInteger($value) || $this->isFloat($value)) {
$value += 0;
}
throw new Exception($this->createErrorMessage('NULL', $value, $original_query));
}
return 'NULL';
}
/**
* Всегда генерирует исключение, если $value не является массивом.
* Первоначально была идея в режиме self::MODE_TRANSFORM приводить к типу array
* скалярные данные, но на данный момент я считаю это излишним послаблением для клиентов,
* которые будут использовать данный класс.
*
* @param mixed $value
* @param string $original_query
* @throws Exception
* @return array
*/
private function getValueArrayType($value, $original_query)
{
if (!is_array($value)) {
throw new Exception($this->createErrorMessage('array', $value, $original_query));
}
return $value;
}
/**
* Экранирует имя поля таблицы или столбца.
*
* @param string $value
* @return string $value
*/
private function escapeFieldName($value, $original_query)
{
if (!is_string($value)) {
throw new Exception($this->createErrorMessage('field', $value, $original_query));
}
$new_value = '';
$replace = function($value){
return '`' . str_replace("`", "``", $value) . '`';
};
// Признак обнаружения символа текущей базы данных
$dot = false;
if ($values = explode('.', $value)) {
foreach ($values as $value) {
if ($value === '') {
if (!$dot) {
$dot = true;
$new_value .= '.';
} else {
throw new Exception('Два символа `.` идущие подряд в имени столбца или таблицы');
}
} else {
$new_value .= $replace($value) . '.';
}
}
return rtrim($new_value, '.');
} else {
return $replace($value);
}
}
/**
* Проверяет, является ли значение целым числом, умещающимся в диапазон PHP_INT_MAX.
*
* @param mixed $input
* @return boolean
*/
private function isInteger($val)
{
if (!is_scalar($val) || is_bool($val)) {
return false;
}
return $this->isFloat($val) ? false : preg_match('~^((?:\+|-)?[0-9]+)$~', $val) === 1;
}
/**
* Проверяет, является ли значение числом с плавающей точкой.
*
* @param mixed $input
* @return boolean
*/
private function isFloat($val)
{
if (!is_scalar($val) || is_bool($val)) {
return false;
}
$type = gettype($val);
if ($type === "double") {
return true;
} else {
return preg_match("/^([+-]*\\d+)*\\.(\\d+)*$/", $val) === 1;
}
}
}
/**
* Заменяет часть строки string, начинающуюся с символа с порядковым номером start
* и (необязательной) длиной length, строкой replacement и возвращает результат.
*
* @param string $string
* @param string $replacement
* @param string $start
* @param string $length
* @param string $encoding
* @return string
*/
if (!function_exists("mb_substr_replace"))
{
function mb_substr_replace($string, $replacement, $start, $length=null, $encoding=null)
{
if ($encoding == null) {
$encoding = mb_internal_encoding();
}
if ($length == null) {
return mb_substr($string, 0, $start, $encoding) . $replacement;
} else {
if ($length < 0) {
$length = mb_strlen($string, $encoding) - $start + $length;
}
return
mb_substr($string, 0, $start, $encoding) .
$replacement .
mb_substr($string, $start + $length, mb_strlen($string, $encoding), $encoding);
}
}
}