- Поддержка типов данных через макросы: строка, целое число, дробное число, логическое значение + возможность расширения
- Возможность компиляции запроса в PHP код (по аналогии как шаблонизаторы компилируют шаблон — к примеру так делает Twig)
- Возможность делать макросы для использования в запросе
Я не буду в очередной раз писать про защиту от SQL инъекция. Если кто еще не читал, то вот отличная статья на эту тему Защита от SQL-инъекций в PHP и MySQL от автора safemysql. Данная статья — это моё ТЗ (не работающий класс, а только его описание) для написание работающего класса, который можно использовать в своем проекте. Возможно собранная информация будет и еще кому-то полезна. Для меня же польза будет в том, что при описании для других людей лучше обдумывается тема, что способствует лучшему продумыванию архитектуры.
Базовые макросы
Все макросы будут иметь вид ?<имя макроса>(параметры). Я написал макросЫ и их действительно будет несколько — целых 2 штуки. Перечислим их:
- ?if(имя переменной){текст запроса, который должен выполняться в случае если указанная переменная ИСТИНА}[?else{текст запроса, который должен выполняться в случае если указанная переменная ЛОЖЬ}] — этот макрос служит для формирования сложных запросов в зависимости от условия. Блок else является не обязательным.
- ?_dot(текст для вставки в запрос) — этот макрос служит для вставки в запрос переменных или вызовов функций. Макрос с нижним подчеркиванием, так как он предназначен не для вызова из текста запроса, а для формирования макросов расширения, речь о которых пойдет далее.
SELECT name FROM users WHERE age > ?_dot(intval($vars['age'])) AND ?if(male){gender='1'}?else{gender = '2'}
в результате обработки получим следующий PHP код для формирования запроса$sql = "SELECT name FROM users WHERE age > ".intval($vars['age'])." AND ";
if($vars['male'])
$sql .= "gender='1'";
else
$sql .= "gender='2'";
Все остальные макросы, которые могут потребоваться (в том числе для безопасной вставки переменных в текст запроса), можно сформировать с помощью 2-х приведенных выше макросов. При этом также важен тот факт, что все переданные переменные содержатся в массиве $vars. В примере выше используются две переменные из этого массива: age и male.Архитектура классов
Каждый вариант будет располагаться в папке производитель\вариант. Базовый вариант будет находиться в папке Shasoft\Base. Также классы каждого варианта должны располагаться в аналогичном пространстве имен: базовый вариант будет находиться в пространстве имен Shasoft\Base. Такой подход позволит избежать пересечения имен классов в разных вариантах.
Основной класс
Основной класс для работы с БД — SafeSql. В базовом варианте он будет содержать только метод query, который будет вызываться для выполнения запросов.
$db->query("текст запроса",<массив переменных>,function($row,$qi) {
});
- $row — очередная строка выбранных данных или неопределенное значение если запрос не подразумевает возврат данных (INSERT, UPDATE, DELETE)
- $qi — дополнительная информация. Содержит следующие поля
- effected_rows — число строк, затронутых предыдущей операцией MySQL
- error — строка с описанием последней ошибки (или false если ошибки не было)
- insert_id — автоматически генерируемый ID
Этот класс должен переопределяться в обязательном порядке в каждом новом варианте даже если не добавляется никакого нового функционала. Также класс содержит переменную opts с параметрами, переданными в класс при создании и указатель на объект драйвера, созданного по имени драйвера. переданного в переменной driver. Речь о классе драйвера пойдет далее.
Класс макроса
Как уже писал ранее — есть всего два базовых макроса. Все остальные макросы, которые могут понадобиться, можно создать с помощью макросов расширения. В тексте запроса макрос имеет следующий вид
?<любое имя>[(параметры)] — любой определенный вами макрос. Для реализации этого макроса необходимо в папке вариант\Macros создать файл с <именем макроса>.php и определить в нем класс с именем макроса, который должен наследоваться или от абстрактного класса Shasoft\Base\Macro или от ранее определенного макроса. Для реализации замены макроса необходимо реализовать абстрактный метод sql()
//! Абстрактный класс макроса
abstract class Macro {
// Параметры
protected $opts;
// Драйвер
protected $driver;
// Конструктор
function __construct() {
}
// Выполнить преобразование
// $args - строка параметров
abstract public function sql($args);
// Разобрать параметры макроса на список параметров
// $args - строка параметров
static public function parseArgs($args) {...}
}
//! Класс аргумента макроса
class MacroArg {
// Значение
protected $value;
// Символ строки (если = false, значит это не строка)
protected $ch_string;
// Текст
public getText() { return $this->value; }
// Это строка?
public isString() { return $this->ch_string==false ? false : true; }
// Получить текст строки как константу
public getConstString() { return $this->ch_string.addslashes($this->value).$this->ch_string; }
}
Если макрос был определен в одном из родительских классов, то вы можете его заменить в текущей реализации. Если макроса нет в текущей реализации, то макрос с таким именем ищется в родительском классе, затем в родительском родителя и т.д. Именно для этого необходимо обязательно определять основной класс SafeSql для вашего варианта — необходим список родителей класса, чтобы знать где искать не определенные в данном варианте макросы.
Класс драйвера
Данный класс будет отвечать за работу с конкретной БД. Класс также можно будет расширять от версии к версии. В базовой версии драйверу необходимо реализовать всего три функции: открытие соединение с БД, закрытие соединение, генерация PHP кода для выполнения запроса.
<?php
namespace Shasoft\SafeSql;
//! Абстрактный класс драйвера для работы с БД
abstract class Driver {
// Конструктор
function __construct() {}
// Открыть соединение с БД
// result - возвращает true или текст ошибки
abstract public function connect($opts);
// Закрыть соединение с БД
abstract public function close($opts);
// Сформировать код PHP для выполнения запроса
// $varSql - имя переменной содержащей запрос (не текст запроса, а имя ПЕРЕМЕННОЙ, которая этот запрос содержит)
// result - возвращает текст PHP кода
abstract public function sql($varSql);
}
?>
Параметры базового варианта
Опишем параметры базового варианта.
- driver — имя драйвера. Короткое имя, т.е. без приставки Vendor\Variant\Drivers
- server — сервер. По умолчанию «localhost»
- database — имя базы данных
- user — имя пользователя
- pass — пароль
- path — директория для сохранения скомпилированного PHP кода
- debug — режим отладки. Т.е. в этом режиме код всё равно будет каждый раз перекомпилироваться и пересохраняться
А теперь напишу ка я базовый класс на основе этой статьи и станет ясно, насколько реально то, что я описал. За работу!
p.s.может кто подскажет класс PHP, которому можно скормить код на PHP и получить его красиво отформатированным? В общем-то это не очень и нужно, но для проверки работоспособности лучше чтобы полученный код выглядел красиво.
Комментарии (55)
andrewnester
06.08.2015 19:12+4если на то пошло, ваш SQL код под компиляцию выглядит страшнее и запутаннее, чем если бы Вы использовали prepared statements или query builder
jrip
06.08.2015 19:59+7>А теперь напишу ка я базовый класс на основе этой статьи и станет ясно, насколько реально то, что я описал. За работу!
пожалуйста не надо, в нашем мире и так хватает печали.
Lisio
06.08.2015 20:25+9Я правильно понял, что используя ваш метод нужно сформировать особый SQL-запрос, который средствами PHP сформирует код PHP, который сформирует SQL-запрос?
shasoft
06.08.2015 20:40-2Ну, если назвать особым запросом то, что обычно пишут в DbSimple и аналогах, то ДА.
Lisio
06.08.2015 20:49+2Даже сам Дмитрий Котеров пишет на гитхабе, что это «Quite old (PHP4-compatible) interface to work with various DBs.». В настоящее время ее использование не принесет ничего, кроме лишних проблем тем, кто будет поддерживать такой код.
saksmt
06.08.2015 21:09+3Если уж так хочется прям МАКРОСЫ, а не ту ерунду, что вы здесь понаписали в качестве примеров, то есть Doctrine и DQL, который вполне себе расширяем и компилируем, в том чиле в Memcache[d] и напрямую в OpCache.
А ежели нужны «макросы» в вашем понимании, то тут и PDO хватит, как уже писали выше.
P.S. Действительно, помогите мне это развидеть…Fesor
06.08.2015 21:20напрямую в OpCache.
А с каких пор Doctrine умеет компилировать и кешировать DQL? Я серьезно об этом не знаю. И уж тем более как это оно может кешировать в opcache? Кеширование в opcache подразумавает генерацию PHP кода и его выполнение (не через eval), может быть вы имеете в виду APCu?
В целом я согласен, подход доктрины намного более правильный. Там тебе и возможность через AstWalker поменять все что угодно в запросе в рантайме, и расширять его удобно и вообще ништяк. А так как там используются prepared statements и имеется вся необходимая инфа о метаданных (которая кешируется), то вероятность SQL инъекций нивелируется.
Ну и да, мэппинг на объекты… люблю я доктрину.jrip
06.08.2015 21:53+1>А с каких пор Doctrine умеет компилировать и кешировать DQL?
Меня никак не может покинуть один вопрос: нафига кешировать сам код запросов?Fesor
06.08.2015 23:30В контексте DQL — это не SQL и оперирует он сущностями, объектами и т.д. Там есть море кастомных функций и т.д. То есть это нормальная идея сохранять результат работы этой штуки — SQL, так как это не просто query builder а query builder основанный на метаданных мэппинга таблиц на объекты, с учетом вложенных объектов и трансформаций. Но это крайне сложно сделать и проще кешировать на уровень выше — в репозитории.
jrip
07.08.2015 01:38>Но это крайне сложно сделать и
>проще кешировать на уровень выше — в репозитории.
Ну так вот как бы да — кешировать данные это нормально, но зачем кешировать уже сам код запроса, причем еще и вместе с пхп. Это же плюсов не несет никаких — сформировать на основе данных запрос, даже очень сложный дело для PHP не напряжное, при этом будут работать всякие акселераторы и тд. При этом данные мэппинга то скорее всего будут общие для многих запросов.Fesor
07.08.2015 01:42Повторю еще раз. вам надо преобразовать:
$dql = "SELECT u FROM \App\Domain\User\User u where u.createdAt = :date"; $query = $this->createQuery($dql)->setParameter('date', new \DateTime('last friday'));
в
$sql = 'SELECT u.* FROM users u WHERE u.created_at = ?';
что-то в этом духе. Но да, в этом смысла нет потому этого и не реализовали. Я больше к этому вел мысль — доктрина не умеет это делать и не собирается.
saksmt
07.08.2015 08:54+1Умеет, и в APCu и в OpCache через сгенерированные PHP файлы (Doctrine\Common\Cache\PhpFileCache), штука эта называется QueryCache она кеширует именно сгенерированные запросы, есть ещё MetadataCache и ResultCache, думаю назначение разъяснять не нужно. В доктрине и компонентах симфони вообще много малоизвестных штук происходит :)
jrip
06.08.2015 21:55+2>p.s.может кто подскажет класс PHP, которому можно скормить код на PHP и получить его >красиво отформатированным?
Я за вас уже боюсь, надеюсь вы тут так всех тролите.
Код можно отформатировать с помощью IDE, PhpStorm например умеет.shasoft
06.08.2015 22:15Вы не поняли суть вопроса: генерируется PHP код для выполнения запроса. Мне нужно перед тем как сохранить в файл, его красиво отформатировать. Чтобы при просмотре было все более наглядно, но при этом при генерации кода не заморачиваться с его красивым представлением.
jrip
07.08.2015 01:49+1Я вас понял, я просто намекаю что ваша идея кажется немного бредовой)
Но в креативности вам не откажешь, я такой идеи еще не встречал)
Суть то вот в чем. Вы таким способом не получите никаких плюсов по сути. Генерация этой штуки на лету, с кешированием каких-то данных возможно будет даже быстрее, чем кеширование подобных php файлов на диск. Просто сама генерация операция не тяжелая, а как будут себя вести акселераторы, при постоянном создании php файлов я даже не знаю, ни разу так не пробовал.
А также есть и другие минусы в таком подходе — во первых файлов в реальном проекте у вас таких будет просто дофига, на каждый запрос с разными параметрами. Макросами вы эту проблему не решите — вам для этого нужно будет придумать свой php внутри php.
Во вторых вы все равно придете к тому, что кеш нужно как-то валидировать, а такой валидировать будет трудно.
В третьих тот кто будет работать с кодом после вас, будет переполняться ненавистью.
>?_dot(intval($vars['age'])) AND ?if(male){gender='1'}?else{gender = '2'}
Вы же тут на самом деле язык в языке начинаете придумывать, причем с не очень то удобным синтаксисом, а его придется изучить и запомнить.shasoft
07.08.2015 08:15-1ОДИН запрос с РАЗНЫМИ параметрами => один PHP файл. Файл создается ОДИН раз при ПЕРВОМ вызове.
jrip
07.08.2015 10:08+1А ну т.е. в результате получается совсем небольшой ограниченный набор запросов? Тогда чего их руками не написать? Зачем весь этот гемор? Сокращение нажатий кнопок в ущерб понятности когда? Так можно пойти и на Perl`е писать.
Защита от SQL-инъекций? Такая же вероятность начудить в макросах.shasoft
07.08.2015 10:40Если исходить из «начудить», то можно ошибиться если писать запросы на QueryBuilder-е или любом другим способом.
На мой взгляд проще писать ?i(age), чем intval($vars['age']).
Как я уже писал — это просто уровень абстракции.jrip
07.08.2015 11:05>?i(age), чем intval($vars['age']).
Это не абстракция, это изменение синтаксиса.
>то можно ошибиться если писать запросы на QueryBuilder-е или любом другим способом.
Там начудить труднее, спец символы в нормальном QueryBuilder-е будут таки экранироваться.
Кстати как в вашем подходе будет выглядть вставка в таблицу поля, которое содержит html и которое нельзя меня?
Как будет выглядеть запрос с поиском по полю через LIKE%?shasoft
07.08.2015 11:25>>Это не абстракция, это изменение синтаксиса.
Так QueryBuilder тоже по сути изменение синтаксиса. ;)
>>Кстати как в вашем подходе будет выглядть вставка в таблицу поля, которое содержит html и которое нельзя меня
если вы про константное значение, то вот так ?s("Хабр")
>>Как будет выглядеть запрос с поиском по полю через LIKE%?
?f(field) like ?s("%хабр")
как вариант, сделать макрос соответствующий
?like(field,"%хабр")jrip
07.08.2015 11:58+1>>Это не абстракция, это изменение синтаксиса.
>Так QueryBuilder тоже по сути изменение синтаксиса. ;)
Нет. QueryBuilder использует снтаксис языка, там понятно где метод, где класс и что означают спецсимволы.
?i(age), — а вот тут человек, знающий синтаксис PHP может и не понять что происходит.
разница в том, что код QueryBuilder обрабатывается PHP, а ваш код обрабатывается вашим кодом на PHP превращается в код PHP и уже потом обрабатывается PHP.
>если вы про константное значение, то вот так ?s(«Хабр»)
И что дальше происходи с «Хабр»? Если написать «Хабр\»"?
>?like(field,"%хабр")
А если тупо ?like(field,"%хабр\"") то что будет?
Ну в плане где тут защита от инъекций у вас?shasoft
07.08.2015 13:13>>И что дальше происходи с «Хабр»?
Вызовется real_escape(«Хабр») (или его аналог в конкретной БД) и строка вставиться в запрос.jrip
07.08.2015 13:27+1Ну так это получается подход query('SELECT * FROM u WHERE field = ?', $param), который уже малость устарел на самом деле.
Только вы еще и логику свою добавляете, которую нужно понять и заучить в виде макросов.
Смысл то какой?
И еще — создавать php файлы, особенно в разных велосипедах, это пугающая штука.
По мимо возможных инъекций в MySQL вы еще и возможность инъекций в PHP добавляетеshasoft
07.08.2015 15:35Смысл в облегчении себе работы. Если часто пишешь одну и туже команду — закатал её в макрос и минимизировал шанс написать команду ошибочно.
>>возможность инъекций в PHP добавляете
Это каким-же образом? Только если использовать библиотеку, которую написал неизвестно кто и в которой есть макрос, который делает эту PHP инъекцию.Fesor
07.08.2015 15:54Смысл в облегчении себе работы.
Я не вижу облегчения… Опять же — DQL в Doctrine2 делает то что вы хотите. Вот если вы сделаете что-то подобное — то да, спору нет, штука полезная (с автоматическим ресолвом типов из схемы базы данных что бы не нужно было руками это указывать где-либо).jrip
07.08.2015 16:18>Doctrine2
Оно не делает то что он хочет, да и сферы применения у Doctrine2 далеко не безграничны.
jrip
07.08.2015 16:14+1>Смысл в облегчении себе работы.
>Если часто пишешь одну и туже команду — закатал её в макрос и
>минимизировал шанс написать команду ошибочно.
Так сделайте макрос на уровне IDE, вот опять напишу про phpStorm — там такое возможно и вообщем-то как раз так многие и делают например для отладки, хочется выводить красивый var_dump — переопределяют его и не копируют постоянно большой кусок кода. Там будет ровно такое же разворачивание кода, как делаете вы.
>Это каким-же образом? Только если использовать библиотеку,
>которую написал неизвестно кто и в которой есть макрос, который делает эту PHP инъекцию.
Есть методология оценки подходов и качества, грубо говоря в вашем подходе есть, возможно не большая, но олтичная от нуля возможность накосячить и пропустить в PHP код что-то нехорошее, просто потому что вы его генерите.
Опять же, если вы все равно хотите что такое делать — это должна быть консольная утилита, генератор кода.
voidMan
07.08.2015 01:48+5Вот из-за таких вот постов, PHP считают языком второго сорта ;).
Рад за автора что ему удалось решить свою проблему, однако такое действительно не стоит показывать, не дай Бог новички начнут думать, что так надо делать.Temirkhan
07.08.2015 02:15+1Потому и существуют комментарии.
jrip
07.08.2015 11:15+1Но к сожалению практика показывает, что многие юношы с горящими глазами комментарии не осиливают.
У меня тут был случай, когда в доказательство своей бредовой теории, человек мне статью на хабре показывал.
В комментариях у людей была истерика, а оценка статьи была краисво красной. Но человек решил на это внимания не обращать.
qrasik
13.09.2015 18:48+1Уважаемый shasoft, сама идея шаблонизации SQL интересна и востребована, но вот именно такое решение несколько не очень. Я бы посоветовал вам просмотреть презентацию на эту тему.
По поводу же PDO, я советую не брать близко к сердцу. К сожалению, он умеет только подставлять параметры и собственно на этом все. Типизованных placeholder-ов от него не дождешься не говоря уже про условные :-)
p.s.может кто подскажет класс PHP, которому можно скормить код на PHP и получить его красиво отформатированным
Посмотрите на PHP-FMT из sublime text 3shasoft
13.09.2015 20:24За класс форматирования PHP спасибо.
Что касательно презентации, не очень понял разницу между моим решением и тем что там. К примеру на странице 60 дается пример, который только синтаксисом отличается от того что я предлагал, в остальном же похоже. Возможно смысл презентации я не очень понял, так как связующего текста между слайдами нет.qrasik
13.09.2015 20:49На первый взгляд разницы в самом деле нет.
Главной идеей подхода в презентации было то, что с учетом шаблона, код является полноценным SQL, который можно вставить в SQL-консоль и запустить. Удобство отлаживания и последующего изменения из коробки. Но из-за синтаксиса комментариев видок страшноват, что есть то есть.
Ну и реализация… Если вы заметили, основная претензия к вам была именно из-за реализации. За реализацию бить нынче принято ибо модно. Особенно если не сразу понятны плюсы. Напирать надо было именно на «плюшки и конфеты» для разработчика. Идея то богатая. Сам из-за типизованных placeholder-ов не побежал за всеми в сторону orm. :-)shasoft
15.09.2015 09:45Что мешает сделать консоль для проверки выражений?
Я реализовал все описанное в статье, хотя были некоторые изменения, которые пришли в голову при реализации. Вообще планирую сделать страницу для проверки и демонстрации работы. Т.е. вводим запрос с макросами — он показывается в виде PHP. Однако в связи с тем, что интереса у народа сама идея не вызвала — решил сначала испытать в «боевых» условиях, может еще какие изменения будут. Тогда уже и сделаю консоль для проверки.qrasik
15.09.2015 10:09Наверное повторюсь, но все же…
Например шаблонизатор twig или тот же смарти, собирают шаблон в PHP код. Но само по себе, ценности это не имеет. Одно дело когда этот самый код сидит в некоем кеше и глаза не мозолит и другое дело когда он превращается в еще одну сущность за которой придется следить.
P.S.
Только-только отгремела священная война за разделение HTML тегов от PHP кода, а тут вы предлагаете автоматическую фабрику по фаршированию PHP но уже SQL-ем. ;-)shasoft
16.09.2015 10:50Так фактически код и находится в КЕШ-е — в файловом. Все что нужно, это при обновлении сайта удалить из КЕШ-а этот код, вот и все слежение.
Fesor
ммм… как это развидеть…
вроде как уже лет 10 есть PDO и prepared statements… потому никто и не делает «компиляции»
shasoft
Однако все CMS все-равно делают свой слой абстракции для работы с БД. С PDO никто вроде не работает.
p.s.Чтобы это «развидеть» — достаточно закрыть страницу.
Fesor
это не отменяет того что не нужно делать свои кастыли эмулирующие работу prepared statements (mysqli тоже их поддерживает, если хочется еще более низкоуровневое API юзать). Да и насколько я помню все адекватные CMS уже давно перешли на PDO.
Давайте сравним…
и
shasoft
Так никто не мешает написать драйвер работы с PDO. Тогда будет генерироваться указанный вами код. В общем-то при проектировании учитывалась такая ситуация.
Fesor
погодите, у вас написано вот это:
Вот эти вот intval не позволяют нам получить информацию о типе и избавиться от необходимости вручную контролировать что мы пихаем в запрос. Мы просто усложняем систему (причем страдает читабельность) и при этом не получаем ровным счетом никакого профита.
shasoft
В данном случае у нас вообще нет информации о типе. Это БАЗОВЫЙ вариант. Предполагается что он будет расширен макросами s, i,f — которые как раз и будут задавать тип данных. Т.е. макрос _dot вообще не будет встречаться в запросе (о чем и указано в статье).
Т.е. будет макрос i — целое число. И тогда запрос
SELECT name FROM users WHERE age > ?i(age) AND ?if(male){gender='1'}?else{gender = '2'}
будет в одном случае разворачиваться в ?_dot(intval($vars['age'])), а в случае с PDO в он будет разворачиваться в ":age" и будет запоминаться что эта переменная имеет тип i.
Как то так.
Stalker_RED
Вы не хотите дополнить статью и привести несколько примеров «было/стало»? Потому что судя по комментариям, сейчас сложно понять зачем оно вообще. (Я вот, вообще ничего не понял.)
shasoft
Сделаю базовую реализацию, тогда приведу примеры. пока же, судя по всему, это не очень понятно большинству читающих.
Fesor
только ради мога, возьмите за основу что-нибудь вменяемое, например doctrine/dbal. То что я вижу в статье можно сделать на базе того же twig, и это будет ужасно…
shasoft
doctrine/dbal. — это ORM. Т.е. там подход «немного» другой
Fesor
dbal это dbal, database abstraction layer, к ORM никакого отношения не имеет, кроме того что является зависимостью doctrine/orm.
shasoft
Вообще же в данном случае это синтетический пример. Т.е. пример просто демонстрировал условное составление запроса. По аналогии DbSimple тоже использует фигурные скобки для этого. Т.е. на реальном примере пришлось бы запрос тоже составлять с помощью if/else. Данный пример так легко лег в одну строку запроса потому что это просто пример. Вот такой запрос уже все-равно придется писать с if
SELECT name FROM users WHERE age > ?_dot(intval($vars['age'])) AND ?if(male){gender='1'}
Fesor
Ну вот а теперь сравните читабельность:
это легко прочитать а значит легко поддерживать и разбираться в коде. Да, в вашем примере меньше символов, но поддерживать его будет ад и холокост. Меньше кода не значит лучше.
Invision70
>>> С PDO никто вроде не работает.
Например слой абстракции идет поверх PDO, за счет этого используя Query Builder прослойку — меняем БД и работаем дальше.