battles/classes/Database/Mysql.php

1045 lines
45 KiB
PHP
Raw Permalink Normal View History

2018-01-28 16:40:49 +00:00
<?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']);
2018-01-28 16:40:49 +00:00
*
* Аргументы 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
2018-01-28 16:40:49 +00:00
*
* Это утверждение не относится к числам (целым и с плавающей точкой), заключенным в строки.
* С точки зрения библиотеки, строка '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');
2018-01-28 16:40:49 +00:00
* вернёт строку
* 'Total: 200'
* Если бы кавычки, ограничивающие строковой литерал, ставились бы автоматически,
* то вышеприведённое условие вернуло бы строку
* 'Total: "200"'
* что было бы не ожидаемым поведением.
*
* Тем не менее, для перечислений ?as, ?ai, ?ap, ?As, ?Ai и ?Ap ограничивающие кавычки ставятся принудительно, т.к.
* перечисления всегда используются в запросах, где наличие кавчек обязательно или не играет роли (а так ли это?):
*
* $getInstance->query('INSERT INTO `test` SET ?As', array('name' => 'Маша', 'age' => '23', 'adress' => 'Москва'));
2018-01-28 16:40:49 +00:00
* -> INSERT INTO test SET `name` = "Маша", `age` = "23", `adress` = "Москва"
*
* $getInstance->query('SELECT * FROM table WHERE field IN (?as)', array('55', '12', '132'));
2018-01-28 16:40:49 +00:00
* -> SELECT * FROM table WHERE field IN ("55", "12", "132")
*
* Также исключения составляют заполнители типа ?f, предназначенные для передачи в запрос имен таблиц и полей.
* Аргумент заполнителя ?f всегда обрамляется обратными кавычками (`):
*
* $getInstance->query('SELECT ?f FROM ?f', 'my_field', 'my_table');
2018-01-28 16:40:49 +00:00
* -> SELECT `my_field` FROM `my_table`
*/
namespace Krugozor\Database\Mysql;
use mysqli;
use mysqli_result;
2018-01-28 16:40:49 +00:00
class Mysql
{
/**
* Строгий режим типизации.
* Если тип заполнителя не совпадает с типом аргумента, то будет выброшено исключение.
* Пример такой ситуации:
*
* $getInstance->query('SELECT * FROM `table` WHERE `id` = ?i', '2+мусор');
2018-01-28 16:40:49 +00:00
*
* - в данной ситуации тип заполнителя ?i - число или числовая строка,
* а в качестве аргумента передаётся строка '2+мусор' не являющаяся ни числом, ни числовой строкой.
*
* @var int
*/
const MODE_STRICT = 1;
/**
* Режим преобразования.
* Если тип заполнителя не совпадает с типом аргумента, аргумент принудительно будет приведён
* к нужному типу - к типу заполнителя.
* Пример такой ситуации:
*
* $getInstance->query('SELECT * FROM `table` WHERE `id` = ?i', '2+мусор');
2018-01-28 16:40:49 +00:00
*
* - в данной ситуации тип заполнителя ?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 в случае ошибки, в обратном случае объект результата
2018-10-31 19:53:21 +00:00
* @throws Exception
2018-01-28 16:40:49 +00:00
*/
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) {
2018-01-28 16:40:49 +00:00
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));
2018-01-28 16:40:49 +00:00
* Результат:
* 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);
2018-01-28 16:40:49 +00:00
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);
}
}
}