Вы можете скачать её архивом, клонировать с данного сайта или загрузить через composer (ссылка на packagist.org):
composer require krugozor/database
krugozor/database
— библиотека классов на PHP >= 8.0 для простой, удобной, быстрой и безопасной работы с базой данных MySql, использующая расширение PHP mysqli.
Основные недостатки всех библиотек для работы с mysql-базой в PHP это:
- Многословность
- Чтобы предотвратить SQL-инъекции, у разработчиков есть два пути:
- Использовать подготавливаемые запросы.
- Вручную экранировать параметры идущие в тело SQL-запроса. Строковые параметры прогонять через mysqli_real_escape_string, а ожидаемые числовые параметры приводить к соответствующим типам —
int
иfloat
.
- Оба подхода имеют колоссальные недостатки:
- Подготавливаемые запросы ужасно многословны. Пользоваться "из коробки" абстракцией PDO или расширением mysqli, без агрегирования всех методов для получения данных из СУБД просто невозможно — что бы получить значение из таблицы необходимо написать минимум 5 строк кода! И так на каждый запрос!
- Экранирование вручную параметров, идущих в тело SQL-запроса — даже не обсуждается. Хороший программист — ленивый программист. Всё должно быть максимально автоматизировано.
- Чтобы предотвратить SQL-инъекции, у разработчиков есть два пути:
- Невозможность получить SQL запрос для отладки
- Что бы понять, почему в программе не работает SQL-запрос, его нужно отладить — найти либо логическую, либо синтаксическую ошибку. Что бы найти ошибку, необходимо "видеть" сам SQL-запрос, на который "ругнулась" база, с подставленными в его тело параметрами. Т.е. иметь сформированный полноценный SQL. Если разработчик использует PDO, с подготавливаемыми запросами, то это сделать... НЕВОЗМОЖНО! Никаких максимально удобных механизмов для этого в родных библиотеках НЕ ПРЕДУСМОТРЕНО. Остается либо извращаться, либо лезть в лог базы данных.
- Избавляет от многословности — вместо 3 и более строк кода для исполнения одного запроса при использовании "родной" библиотеки, вы пишите всего одну.
- Экранирует все параметры, идущие в тело запроса, согласно указанному типу заполнителей — надежная защита от SQL-инъекций.
- Не замещает функциональность "родного" mysqli адаптера, а просто дополняет его.
- Расширяема. По сути, библиотка предоставляет собой лишь парсер и исполнение SQL-запроса с гарантированной защитой от SQL-инъекций. Вы можете унаследоваться от любого класса библиотеки и используя как механизмы библиотеки, так и механизмы
mysqli
иmysqli_result
создавать необходмые вам методы для работы.
Большинство оберток под различные драйверы баз данных являются нагромождением бесполезного кода с отвратительной архитектурой. Их авторы, сами не понимая практической цели своих оберток, превращают их в подобие построителей запросов (sql builder), ActiveRecord библиотек и прочих ORM-решений.
Библиотека krugozor/database
не является ничем из перечисленных. Это лишь удобный инструмент для работы с обычным SQL в рамках СУБД MySQL — и не более!
Placeholders (англ. — заполнители) — специальные типизированные маркеры, которые пишутся в строке SQL запроса вместо явных значений (параметров запроса). А сами значения передаются "позже", в качестве последующих аргументов основного метода, выполняющего SQL-запрос:
$result = $db->query(
"SELECT * FROM `users` WHERE `name` = '?s' AND `age` = ?i",
"Д'Артаньян", 41
);
Параметры SQL-запроса, прошедшие через систему placeholders, обрабатываются специальными механизмами экранирования, в зависимости от типа заполнителей. Т.е. вам теперь нет необходимости заключать переменные в функции экранирования типа mysqli_real_escape_string()
или приводить их к числовому типу, как это было раньше:
<?php
// Раньше перед каждым запросом в СУБД мы делали
// примерно это (а многие и до сих пор `это` не делают):
$id = (int) $_POST['id'];
$value = mysqli_real_escape_string($mysql, $_POST['value']);
$result = mysqli_query($mysql, "SELECT * FROM `t` WHERE `f1` = '$value' AND `f2` = $id");
Теперь запросы стало писать легко, быстро, а главное библиотека krugozor/database
полностью предотвращает любые возможные SQL-инъекции.
Типы заполнителей и их предназначение описываются ниже. Прежде чем знакомиться с типами заполнителей, необходимо понять как работает механизм библиотеки.
PHP является слаботипизированным языком и при разработке этой библиотеки возникла идеологическая дилемма. Представим, что у нас есть таблица следующей структуры:
`name` varchar not null
`flag` tinyint not null
и библиотека ДОЛЖНА (по каким-то причинам, возможно, не зависящим от разработчика) выполнить следующий запрос:
$db->query(
"INSERT INTO `t` SET `name` = '?s', `flag` = ?i",
null, false
);
В данном примере в текстовое not null
поле name
идёт попытка записи значения null
,
а в числовое поле flag
булев тип false
. Как быть в данной ситуации?
- На кого возложить ответственность валидации параметров запроса - на клиентский код или на библиотеку?
- Нужно ли в данном случае прерывать выполнение программы или, быть может, стоить применить какие-то манипуляции, что бы данные записались в базу?
- А можем ли мы трактовать значение
false
дляtinyint
столбца как значение0
, аnull
как пустую строку для столбцаname
? - А как мы можем упростить или стандартизировать в нашем коде подобную проблематику?
Ввиду озвученных вопросов, было решено реализовать в данной библиотеке два режима работы.
- Mysql::MODE_STRICT — строгий режим соответствия типа заполнителя и типа аргумента.
В режиме
Mysql::MODE_STRICT
тип аргумента должен соответствовать типу заполнителя. Например, попытка передать в качестве аргумента значение55.5
или'55.5'
для заполнителя целочисленного типа?i
приведет к выбросу исключения:
// устанавливаем строгий режим работы
$db->setTypeMode(Mysql::MODE_STRICT);
// это выражение не будет исполнено, будет выброшено исключение:
// попытка указать для заполнителя типа "integer" значение типа "double" в шаблоне запроса "SELECT ?i"
$db->query('SELECT ?i', 55.5);
- Mysql::MODE_TRANSFORM — режим преобразования аргумента к типу заполнителя при несовпадении типа заполнителя и типа аргумента. Режим
Mysql::MODE_TRANSFORM
установлен по-умолчанию и является "толерантным" режимом — при несоответствии типа заполнителя и типа аргумента не генерирует исключение, а пытается преобразовать аргумент к нужному типу заполнителя посредством самого языка PHP. К слову сказать, я, как автор библиотеки, всегда использую именно этот режим, строгий режим (Mysql::MODE_STRICT
) в реальной работе никогда не использовал, но быть может, конкретно вам он понадобится.
Допускаются следующие преобразования в режиме Mysql::MODE_TRANSFORM:
- К типу
int
(заполнитель?i
) приводятся- числа с плавающей точкой, представленные как в типе
string
, так и в типеdouble
bool
TRUE преобразуется вint(1)
, FALSE преобразуется вint(0)
null
преобразуется вint(0)
- числа с плавающей точкой, представленные как в типе
- К типу
double
(заполнитель?d
) приводятся- целые числа, представленные как в типе
string
, так и в типеint
bool
TRUE преобразуется вfloat(1)
, FALSE преобразуется вfloat(0)
null
преобразуется вfloat(0)
- целые числа, представленные как в типе
- К типу
string
(заполнитель?s
) приводятсяbool
TRUE преобразуется вstring(1) "1"
, FALSE преобразуется вstring(1) "0"
. Это поведение отличается от приведения типаbool
кint
в PHP, т.к. зачастую, на практике, булев тип записывается в MySql именно как число.- значение типа
numeric
преобразуется в строку согласно правилам преобразования PHP null
преобразуется вstring(0) ""
- К типу
null
(заполнитель?n
) приводятся- любые аргументы.
- Для массивов, объектов и ресурсов преобразования не допускаются.
$db->query(
'SELECT * FROM `users` WHERE `id` = ?i', 123
);
SQL-запрос после преобразования шаблона:
SELECT * FROM `users` WHERE `id` = 123
ВНИМАНИЕ! Если вы оперируете числами, выходящими за пределы PHP_INT_MAX
, то:
- Оперируйте ими исключительно как строками в своих программах.
- Не используйте данный заполнитель, используйте заполнитель строки
?s
(см. ниже). Дело в том, что числа, выходящие за пределыPHP_INT_MAX
, PHP интерпретирует как числа с плавающей точкой. Парсер библиотеки постарается преобразовать параметр к типуint
, в итоге «результат будет неопределенным, так как float не имеет достаточной точности, чтобы вернуть верный результат. В этом случае не будет выведено ни предупреждения, ни даже замечания!» — php.net.
$db->query(
'SELECT * FROM `prices` WHERE `cost` IN (?d, ?d)',
12.56, '12.33'
);
SQL-запрос после преобразования шаблона:
SELECT * FROM `prices` WHERE `cost` IN (12.56, 12.33)
ВНИМАНИЕ! Если вы используете библиотеку для работы с типом данных double
, установите соответствующую локаль, что бы разделитель целой и дробной части был одинаков как на уровне PHP, так и на уровне СУБД.
Значение аргументов экранируются с помощью метода mysqli::real_escape_string()
:
$db->query(
'SELECT "?s"',
"Вы все дураки, а я - Д'Артаньян!"
);
SQL-запрос после преобразования шаблона:
SELECT "Вы все дураки, а я - Д\'Артаньян!"
Значение аргументов экранируются с помощью метода mysqli::real_escape_string()
+ экранирование спецсимволов, используемых в операторе LIKE (%
и _
):
$db->query('SELECT "?S"', '% _');
SQL-запрос после преобразования шаблона:
SELECT "\% \_"
Значение любых аргументов игнорируются, заполнители заменяются на строку NULL
в SQL запросе:
$db->query('SELECT ?n', 123);
SQL-запрос после преобразования шаблона:
SELECT NULL
?A*
— заполнитель ассоциативного множества из ассоциативного массива, генерирующий последовательность пар вида ключ = значение
где символ *
— один из заполнителей:
i
(заполнитель целого числа)d
(заполнитель числа с плавающей точкой)s
(заполнитель строкового типа)
правила преобразования и экранирования такие же, как и для одиночных скалярных типов, описанных выше. Пример:
$db->query(
'INSERT INTO `test` SET ?Ai',
['first' => '123', 'second' => 456]
);
SQL-запрос после преобразования шаблона:
INSERT INTO `test` SET `first` = "123", `second` = "456"
?a*
— заполнитель множества из простого (или также ассоциативного) массива, генерирующий последовательность значений
где *
— один из типов:
i
(заполнитель целого числа)d
(заполнитель числа с плавающей точкой)s
(заполнитель строкового типа)
правила преобразования и экранирования такие же, как и для одиночных скалярных типов, описанных выше. Пример:
$db->query(
'SELECT * FROM `test` WHERE `id` IN (?ai)',
[123, 456]
);
SQL-запрос после преобразования шаблона:
SELECT * FROM `test` WHERE `id` IN ("123", "456")
?A[?n, ?s, ?i, ...]
— заполнитель ассоциативного множества с явным указанием типа и количества аргументов, генерирующий последовательность пар ключ = значение
Пример:
$db->query(
'INSERT INTO `users` SET ?A[?i, "?s"]',
['age' => 41, 'name' => "Д'Артаньян"]
);
SQL-запрос после преобразования шаблона:
INSERT INTO `users` SET `age` = 41,`name` = "Д\'Артаньян"
?a[?n, ?s, ?i, ...]
— заполнитель множества с явным указанием типа и количества аргументов, генерирующий последовательность значений
Пример:
$db->query(
'SELECT * FROM `users` WHERE `name` IN (?a["?s", "?s"])',
["маркиз д\"Аркьен", "Д'Артаньян"]
);
SQL-запрос после преобразования шаблона:
SELECT * FROM `users` WHERE `name` IN ("маркиз д\"Аркьен", "Д\'Артаньян")
Данный заполнитель предназначен для случаев, когда имя таблицы или поля передается в запросе через параметр. Имена полей и таблиц обрамляется символом "апостроф":
$db->query(
'SELECT ?f FROM ?f',
'name',
'database.table_name'
);
SQL-запрос после преобразования шаблона:
SELECT `name` FROM `database`.`table_name`
Библиотека требует от программиста соблюдения синтаксиса SQL. Это значит, что следующий запрос работать не будет:
$db->query(
'SELECT CONCAT("Hello, ", ?s, "!")',
'world'
);
— заполнитель ?s
необходимо взять в одинарные или двойные кавычки:
$db->query(
'SELECT concat("Hello, ", "?s", "!")',
'world'
);
SQL-запрос после преобразования шаблона:
SELECT concat("Hello, ", "world", "!")
Для тех, кто привык работать с PDO это покажется странным, но реализовать механизм, определяющий, нужно ли в одном случае заключать значение заполнителя в кавычки или нет — очень нетривиальная задача, требующая написания целого парсера.
См. в файле ./console/tests.php