Как стать автором
Обновить

Комментарии 103

— Как тебе мой новый велик? Сам сделал. Не хочешь прокатиться?
— Нет спасибо. У меня заводской ездит и не глючит.
Непонятно, что имеется в виду под заводским великом.
Если один из стардартных API — PDO или Mysqli, то это не велик, а набор запчастей «сделай сам». На них, конечно, можно ездить, но только на одной передаче и очень часто крутя педалями. И руль заклинен в положении «прямо».
Database access wrapper писать в любом случае нужно, с голым API жить невозможно.

Если имеются в виду DAL и квери-билдеры различных фреймворков, то это тоже не фабричные велосипеды. И я бы с удовольствием с ними погонялся.
Сделайте код PSR-совместимым и добавьте поддержку composer. + Тесты.

Idiorm очень лаконичная библиотека.
Спасибо, PSR и тесты в ближайших планах. Composer тоже сделаю.

Idiorm — ORM. SQL, написанный на PHP.
Для них меня всегда интересуют не банальные примеры типа
$count = ORM::for_table('contact')->count();
$contact_list = ORM::for_table('contact')->find_many();

а что-то более приближённое к реальности — какой-нибудь order by if(...), ON DUPLICATE…
Я вижу, там заявлена поддержка prepared statements — значит, кроме скаляров больше ничего от инъекций не защищается.
Очень не советую idorm для чего-то сложного. Как только всё отходит от банального crud тут же приходится писать sql руками, что там очень уж не удобно.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
И снег на яваскрипте можно написать по-новому.

Не стоит столь скептически относиться ко всему, что кажется вам банальным.
В любом деле развитие идёт по спирали, и со временем выходит на новый уровень.
Здесь ключевой момент — наличие плейсхолдеров для любого типа данных, который только может придти в запрос, а не только для самых ходовых, как в других решениях.

Куда менее обидно было бы услышать предметную критику, чем ленивый зевок «Знаем, плавали».
dklab.ru/lib/DbSimple/ — видели?
Как «вырезать» часть запроса, в зависимости от какого-то параметра? Через метод parse()?
В DbSimple это так:
$rows = $DB->select('
        SELECT *
        FROM 
            goods g
          { JOIN category c ON c.id = g.category_id AND 1 = ? }
        WHERE 
            1 = 1
          { AND c.name = ? }
        LIMIT ?d
    ',
    (empty($_POST['cat_name'])? DBSIMPLE_SKIP : 1),
    (empty($_POST['cat_name'])? DBSIMPLE_SKIP : $_POST['cat_name']),
    $pageSize
);


А какие классные фишки, как выборка связанных деревьев (ARRAY_KEY, ARRAY_KEY_N, PARENT_KEY), свой обработчик ошибок и свой логгер, возможность кеширования и тд и тп. Чудо библиотека, лучше реально не видел, хотя уже и использую давно фреймворки.

Дополню из примеров библиотеки:
foreach ($array as $item) {
  // DbSimple понимает, что prepare нужно выполнить всего один раз!
  $DB->query('INSERT INTO tbl(field) VALUES(?)', $item);
}

Видел, и даже написал об этом. У dbsimple очень неоднозначный синтаксис, каждый элемент имеет по 2-3 дначения, в зависимости от контекста.
В моей практике чаще приходится добавлять элементы в запрос, чем вырезать их. Например:
$ins = array();
foreach ($data as $row) {
    $ins[] = $db->parse("(NULL,?s,?s, NOW())",$row['name'],$row['lastname']);
}
$instr = implode(",",$ins);
$db->query("INSERT INTO table VALUES ?p",$instr);
Ваш пример, против последнего примера в моем сообщении.
Согласитесь, у Котерова именно это сделано элегантнее, библиотека сама выполнит 1 запрос.

И, если уж на это пошло, как сделать 1й пример из моего сообщения выше, при помощи вашей библиотеки (без фигурных скобок, зато при мощи php)?

Спасибо за интересный вопрос.
$join  = '';
$where = '';
if(!empty($_POST['cat_name'])) {
	$join  = $db->parse('JOIN category c ON c.id = g.category_id AND 1 = 1', 1);
	$where = $db->parse('AND c.name = ?', $_POST['cat_name']);
}
$sql  = 'SELECT * FROM goods g ?p WHERE 1 = 1 ?p LIMIT ?i';
$rows = $DB->getAll($sql, $join, $where, $pageSize);

У Котерова, пожалуй, понагляднее получается.
А как у него с множественным инсертом? :)
Только так:
dklab.ru/lib/DbSimple/manual.html#cont30 — честно говоря, сейчас засомневался, работает ли это с mysql
Mysql поддерживает, оба продвигаемых экстеншена PHP — PDO и Mysqli — тоже.
dbsimple, похоже, для mysql не умеет — надо писать драйвер для mysqli.
Но, это, по сути, отдельные запросы. А речь именно о сборке единого.
Плюс, к примеру, в prepared statement невозможно подставить динамически имя поля.

Пользуюсь немного доработанным dbsimple много лет. Для получения безопасных фрагментов запросов написан метод getQuery, вызывающий какой-то из внутренних методов библиотеки и выполнящий собственно сбор sql из запроса с плейсхолдерами и значений плейсхолдеров. Работает шикарно для постгреса и мускла. А другие просто не были нужны пока. Самолично дописал еще один тип плейсхолдеров ?A ( именно заглавная а, т.к. строчная занята самой библиотекой). Используется для подготовки списка идентификаторов ( спиок полей для инсерта, например). У вашей библиотеки преимуществ перед моим решением не нашел. При этом кода написано в разы меньше и времени, судя по всему, сэкономлено не мало.
Сбрасывайте, конечно, на чужой опыт всегда бывает интересно посмотреть
А я сделал себе аналогичный класс, но только использовал pdo-функции (с возможностью расширения поддержки до нескольких СУБД).
Но пока у меня т.н. «магия» — метод getData($query,$params1="",$params2="")
в зависимости от вида запроса (select, update или delete) возвращает код ошибки, число(строку) или массив массивов.
Но думаю взять у Вас идею нескольких методов getOne(), getRow() и т.п.
Это не моя идея — она много где используется. Например, в PEAR::DB.
Но без поддержки различных типов данных она теряет половину своей прелести.
Поэтому главная идея — плейсхолдеры для всех встречающихся типов данных, а не только для строк и чисел. PDO не предоставляет никаких методов динамически добавить в запрос имя поля.
НЛО прилетело и опубликовало эту надпись здесь
Если бы Даймлер не делал «свой велосипед», мотоцикл так бы и не был изобретен.
Думаю подобные классы/функции есть у всех, кто использует PDO или mysqli без «обёрток» типа Doctrine или Propel, уж больно они (PDO b mysqli) неюзабельны в «сыром» виде, особенно в инсертах. Потому не понятно, почему пост минусуют.

Кстати, подобные обёртки или прямая работа SQL и ORM (как паттерн, а не как фреймворки для его реализации) друг друга не исключают. В текущем проекте пришёл к классам репозиториев, которые жестко (hard coded) мапят БД на объекты, получая удобство ООП и SQL без лишнего оверхида свойственного универсальным ORM.
В том-то и дело, что это не yet another DAL. А точнее — не столько. В первую очередь это попытка окончательно решить проблему инъекций. Покрыв при этом 100% случаев. В отличие от существующих решений, у которых всё гладко только на бумаге. И в примитивных примерах.
А как дело доходит до реальной жизни, то все good practices почему-то всегда куда-то испряются из кода. И SQL шпарится напрямую, с подстановками переменных, из шаблонов обращаются к базе данных (а если шаблон не нативный — значит куча HTML-а пишется в контроллере), и так далее.
Возможно, это перфекционизм, но я последовательно копаю некоторые темы в веб-разработке, пытаясь покрыть 100% применения, а не только самые ходовые случаи.
В данном случае мне это удалось (во всяком случае, пока никто не доказал обратного). Но я провалил задачу донести результат до окружающих.
>>В данном случае мне это удалось (во всяком случае, пока никто не доказал обратного).
$data = $db->getAll(«SELECT * FROM table WHERE a={$_GET['id']}»);
Легко, ничем не отличается от чайниковского mysql_query((«SELECT * FROM table WHERE a={$_GET['id']}»), ведь там тоже есть функции экранирование, но их игнорируют.

Решение на уровне драйвера не может решить проблему sql инъекций. Для её решения нужно вовсе избавтиться от sql и даже параметры типа ?s ?i должны ставиться обёрткой на основании методанных хранилища.

Обёртка однозначно удобнее PDO и mysqli, но это удобство, а никак не безопасность.
В таком случае обёртка должна дублировать 100% функционала SQL. Всякие sqlExpression не подойдут — туда точно так же руками чего угодно напихать можно. Поэтому я сильно сомневаюсь как в принципиальной достижимости этой цели, так и в осмысленности задачи «Написать SQL на РНР».

Но с критикой моего решения ты, пожалуй, прав. Не понимая принципа раздельного форматирования, невозможно и применять его. А это непонимание, похоже — и есть основная проблема.
Это я понял. Я пошёл по другому пути, но тоже целью было не просто удобную обёртку для PDO сделать, а снижение возможностей инъекций за счёт типизированных параметров в запросах. 100% исключить задачи не ставил, искал разумный компромисс между удобством использования и временем разработки. Собственно формировал (и формирую) класс по принципу DRY исходя из текущих потребностей — увидел что в двух местах приложения кода использую схожие конструкции, чтобы запрос создать — перенёс в класс (или, чаще, поставил TODO).

Попробую на днях это решение, пока API выглядит привлекательней моего. Информацию о типах я передаю отдельно.
10 лет пользуем dal в одном классе от форума vbulletin слегка переписаный. Работает. Изучать другие велосипеды лень.
1. Большое спасибо автору (жаль в профиле не указано имя) за обе статьи. Хороший труд, отличный результат, к тому же грамотно и доступно изложенный.

2. В моём проекте используется практически аналогичный подход (обёртка над PDO, такой же набор хелперов, использование плейсхолдеров — но, увы, не типизированных). Поэтому статьи вызвали у меня живейший интерес, так как тема близкая, и многие идеи отсюда я, пожалуй, буду использовать в своём коде, спасибо!

Теперь вопросы по коду класса.

3. В описании _construct() отсутствует слово public — преднамеренно?

4. В методе whiteList() параметр $default задекларирован как string, однако по дефолту прописано булево значение FALSE. Немного режет глаз =)

Кроме того, возможно, через whitelist иногда приходится прогонять и числовые данные. Учитывая, всё вышесказанное, имело бы смысл прописать @param mixed #default?

5. В том же методе (строка 380) можно написать:
return ($found === FALSE)? $default: $input;

Это ровно то же самое, но мне кажется, чуть возрастает очевидность исходного возращаемого значения.

6. У нас используются вот такие названия хелперов:
DB::Value() — для получения скаляра (аналог getOne())
DB::Row() — для ряда
DB::Rows() — для двумерной выборки (аналог getAll())
DB::Column() — для колонки (аналог getCol())

Кстати, обращу внимание — хелперы реализованы как статичные методы. а в целом работа с БД реализована через синглтон, а не через полноценный объект класса. Но это отдельная тема…

7. Я бы написал phpDoc-и и для приватных методов. Или есть какая-то философия насчёт того, чтобы ими пренебрегать?..

8. Интересует ли автора (а также других читателей) адаптация safemysql к БД PostgreSQL? Я в своей работе использую её. Мне кажется, это был бы интересный и полезный для общественности форк.
НЛО прилетело и опубликовало эту надпись здесь
Спасибо за развёрнутый комментарий. Он требует времени на написание развернутого ответа, поэтому пока только по нескольким пункту:
Для pg — вот ниже привели ссылку на библиотеку, реализующую тот же самый принцип — pyha.ru/go/godb/
в которой есть драйвер для Постгре. Но в любом случае, если появится драйвер (потребуется переопределить функции, взаимодействующие с API и методы искейпинга), то это будет просто замечательно.

идея типизованных плейсхолдеров приходит нам в голову ровно в тот момент, когда мы осознаём, что типов форматирования элементов запроса оказывается сильно больше одного (а в PDO по умолчанию таки и используется один — строковый, когда мы тупо закидываем в execute() массив, без ручной привязки). Так что очень рекомендую :)

Выше уже писано:

1. PSR
2. Тесты
3. Доки нормальные на github`е — README.md in ingles (сконвертнуть с phpfaq`а)
4. Поддержку разных вариантов — «raw» mysql, mysqli, PDO
5. Как сделать «named»-переменные? Чтобы в запросе писать что-то типа «SET bla = :mynamed», и передавать внутрь ['mynamed' => 123]?

Накидаю issue`ями в гитхабе.

ps: На гитхабе запрещено по русске писать! :)
Also *Col, *Ind, *IndCol м.б. конвертнуть в более полные формы?
НЛО прилетело и опубликовало эту надпись здесь
to FanatPHP — просто читаю сейчас, что кто писал по теме )

Жесть :)
IN (?a) и NOT IN(?a) при пустом массиве дадут IN(null), NOT IN(null), что даст в свою очередь один и тот же результат, а хотелось бы разные (об это в исходном посте упоминалось).

Кстати, может массив сделать тоже типизированным: "?ai, ?as, ?an"?
Может, если массив пустой, то не выполнять запрос и возвращать пустой результат?
В данный момент так и происходит — запрос, правда, выполняется, но возвращает пустой результат.
Для IN это логично — если мы передаём в IN пустой массив, то хотим получить строки, в которых есть соответствие переданным в массиве значениям. А если массив пустой, то и возвращать нечего.
SELECT * FROM id IN (NULL) так и поступает.
Но вот для запроса
SELECT * FROM id NOT IN (NULL) логично ожидать обратного поведения, но он тоже возвращает ноль строк.

Просто по пустому массиву вываливаться нельзя — запрос может быть «SELECT * FROM id IN (NULL) OR что-то» и прекрасно строки возвращать…
В Yii сделано тем что если массив пустой то 0=1 условие подставляется и все, просто и со вкусом ;)
Ну там, судя по всему, генератор запросов. Модифицировать цельную строку запроса — не совсем тривиальная задача.

Да и вместо 0=1 можно написать FALSE. Ведь 0=1 это незнание, а не способ оптимизации, ведь так?!?! :-)
Ну я сомневаюсь что Кьенг или Александр, делали это от незнания, может просто так решили именно оставить эту ситуацию. Да, это генератор запросов, commandBuilder.
А если копать в сторону того, что MySQL легко перегоняет различные типы данных в строки и обратно, и на основе этого подставлять что нибудь вроде пустой строки
SELECT * FROM id NOT IN ('')
Я подразумеваю что в данном случае будут получаться только те строки, где id установлен и не пустой, что поидее будет выдавать ожидаемый результат.
Однако я не могу предусмотреть, что может получиться в других случаях подстановки пустых массивов…
К сожалению, это «грязный» вариант.
Ведь речь не только о числах. но и о любых данных. и пустая строка вполне может быть законным значением…
Ну вообще да…
Но если так подумать… ведь как я понял, смысл класса — генерация строки запроса, т.е. фактическая подстановка переданных параметров, но в определенном виде.
Если смысл классов таков, то на мой взгляд логичным исходом подстановки пустого массива является как раз пропуск этого массива.
Всмысле получение строки
SELECT * FROM id NOT IN ()
В результате получается ошибка, но и по логике, мы не собирались ничего подставлять.
Я имею ввиду, ведь задача класса просто составить текст запроса на основе введенных данных. Проверка корректности введенных данных уже не является задачей данного класса.
Если бы человек хотел получить все данные, без каких либо ограничений, то написал другой запрос, а в данном случае, необходимо скорее использовать
$sqlpart = '';
if (!empty($var)) {
    $sqlpart = $db->parse(" AND field = ?s", $var);
}
$data = $db->getAll("SELECT * FROM table WHERE a=?i ?p", $id, $sqlpart);

Как по мне, так здесь уже начинают решаться надуманные задачи…
В принципе, я согласен с этой позицией.
Здесь, скорее, уже стремление к перфекционизму. «Неаккуратненько» получается, и хочется исправить :)
Но спасибо за поддержку. Я, пожалуй, подожду ваять какие-то сильно усложняющие код варианты ради такого, прямо скажем, исключительного случая.
Опа. Там я пропустил, по уважительной причине.

Действительно, хотелось бы разные.
Но у меня что-то пока никаких идей, как это реализовать…
Мои кривые решения.
1. Невозможное значение. Подставлять в in при пустом значении такое значение, которого не может быть в базе.
2. Модификация запроса с in, но тут не обойтись без анализа синтаксиса запроса.
3. Кидать какой-нибудь нотис(и выполнять запрос), или, вообще, исключение(и не выполнять запрос).
4. Подставлять всё выражение 'SELECT * FROM users WHERE user ?in'.
И при пустом массиве заменять на 'SELECT * FROM users WHERE user BETWEEN 1 AND 0', других вариантов не вижу. Но при таком подходе можно словить кучу варнингов. Ну лишние проверки на стороне mysql.
5. Свой синтаксис: 'SELECT * FROM users WHERE ?in(user)', чтобы в случае пустого массива заменять на '0=1'. Помимо того, что тут нужен более сложный парсер, тут своя тонкость. Если стоит условие NOT IN или IN, то при любом наборе данных не выберутся строки, в поле которых, по которым делается условие, стоит null. Если же мы будет заменять такое условие на '0=1', то есть шанс получить неожиданный результат.
Поэтому самым правильным в данном случае, с точки зрения результата, будет заменить на '(user AND user IS NULL)'.
6. Подставлять пусто. Заменять на 'SELECT * FROM users WHERE user IN()'. Пусть mysql сам на ошибку укажет.

Всё это мой последний вариант.
Добавляй иссую на гитхаб. Верная мысль.
Я, кстати, тогда жестко затупил с твоим вопросом на СО :)
Память на ники никакая. Плюс, поторопился, конечно — толком не прочитал вопрос.
Имеет смысл, кстати, на dba.* спросить.
Я вот всё думаю, можно ли как-то инвертировать NULL, а точнее, сконвертировать в BOOL.
Может всё не так страшно. У нас в проекте NOT IN используется только один раз. Только жаль, что приходится вручную следить за этим.
Ну, тут два, на самом деле, критерия: частный и общий.
Для частного понятно, что беда небольшая.
Но вот класс, которые используют люди, не понимая его тонкостей (я тут на одном форуме обнаружил чувака, который вовсю использует… без единого плейсхолдера!) — это меня немного поднапрягает. Я же ведь наоборот старался, чтобы не было никаких неоднозначностей. Впрочем, здесь всегда можно со спокойной душой перевести стрелки на Mysql :)
Ну, и плюс к тому — чисто исследовательский интерес — можно ли проблему решить в принципе
Я когда-то сам пользовался классами DbSimple, но потом узрел всю прелесть работы с PDO и теперь все новые проекты я делаю именно с PDO. Мне даже кажется, что PDO значительно более гибкая, чем DbSimple. Да и PDO внушает больше доверия, чем всё остальное.
Хотя соглашусь с тем, что по объёму кода DbSimple немного выигрывает, его часто необходимо меньше, чем в PDO.
Ну, API всегда будет «гибче» абстракции.
Но, как говорилось выше, API — это набор «сделай сам». Слишком многое приходится делать руками.
Те же подстановки имени поля, массивов для IN и SET. Чтобы INSERT с динамическим числом полей, приходится очень сильно попотеть. Я бы не назвал разницу между одной строкой и экраном кода словом «немного».
При этом я как-то не вижу какой-то особой «негибкости» у DbSimple. Есть конкретный пример того, что не смогла она, но смог чистый PDO?
Сам хотел попросить вас привести пример, когда что-то можно сделать вашим классом и что нельзя/сложно сделать через PDO.
Ой, это я с огромным удовольствием.
Скажем, пользователь выбрал в чекбоксами разделы новостей, которые он хочет смотреть. Они лежат в массиве $_GET['themes'] (массив чисел)
Плюс выбрал сортировку по количеству комментариев, она лежит в переменной $_GET['order']
На SafeMySQL это будет две строчки
$sql  = "SELECT * FROM news WHERE theme IN(?a) ORDER BY ?n";
$data = $db->getAll($sql,$_GET['themes'],$_GET['order']);

На PDO что-то вроде
$in = trim(str_repeat('?,',count($_GET['themes'])).",");
$order = str_replace('`','``',$_GET['order']);
$stmt = $db->prepare("SELECT * FROM news WHERE theme IN($in) ORDER BY `$order`");
$stmt->execute($_GET['themes']);
$data = $stmt->fetchAll();

Ну, в общем, конечно, кода тут не на экран, но попахивает он конкретно. Плюс — здравствуй ручное экранирование! — от которого PDO, вроде бы, должен бы избавить.
Всякий раз, когда я смотрю на мучения посетителей стаковерфлоя с динамическими инсертами — у меня сердце кровью обливается.
IN без ручного экранирования выглядит еще страшнее:

$ids     = array(1, 2, 3, 7, 8, 9);
$inQuery = implode(',', array_fill(0, count($ids), '?'));

$db = new PDO(...);
$stmt = $db->prepare(
    'SELECT *
     FROM table
     WHERE id IN(' . $inQuery . ')'
);

// bindvalue is 1-indexed, so $k+1
foreach ($ids as $k => $id)
    $stmt->bindValue(($k+1), $id);

$stmt->execute();


Куда проще в случае интов делать что-то вроде join(',', array_map('intval', $values)), и подставлять в запрос.

Конечно, это все ужасно.
не-не! пдошечка же умеет в массив. так что биндить руками хотя бы не обязательно.
я ж этот же вариант и нарисовал :)
Я бы сделал вот так:

$order = PDO::quote($order);
$sth = $db->prepare(«SELECT * FROM `news` WHERE `theme` IN(:in) ORDER BY `$order`»);
$sth->bindParam(':in', implode(',', (int) $in), PDO::PARAM_STR);
$sth->execute();
$data = $sth->fetchAll(PDO::FETCH_ASSOC);

Есть и другие варианты, но этот самый безопасный.
Меньше кода — не значит лучше, качественнее или безопаснее. Как вы думаете, какой вариант будет работать быстрее, с вашими классами или с PDO?

P.S.: К сожалению, не имею возможности оформлять код красиво в комментариях из-за сами знаете чего.
Чёрт возьми. Сам же ошибку допустил.
$sth->bindParam(':in', implode(',', $in), PDO::PARAM_STR);
Это не поможет
Ещё две остались :)
Что-то не вижу) Какие, не подскажете?)
Вообще, такие вещи лучше смотреть на живой базе, оно получается нагляднее.
С другой стороны, отладка запросов в PDO — ад, и винить разработчика за то, что он не может тупо посмотреть сгенерированный запрос, нельзя.
Поэтому будем экстраполировать сами
SELECT * FROM `news` WHERE `theme` IN('1,2,3') ORDER BY `'comments'`

В приведённом коде две пары лишних кавычек. Поэтому сначала база выдаст ошибку, что нет такого поля — 'comments' (прямо с кавычками), а потом, если решить эту проблему (решение есть в моем коде) — найдет новости только из первой категории.

Чтобы кошерно составить запрос по правилам PDO, надо делать плейсхолдер на кадждое подставляемое значение, чтобы запрос выглядел
SELECT * FROM `news` WHERE `theme` IN(?,?,?) ORDER BY `comments`

Именно это и делает код, который я привел выше
>> ORDER BY `'comments'`
Когда читал документацию заметил, что экранирует всё лишнее, но упустил «обрамляет кавычками, если это нужно». Косяк, серьёзный, согласен. Не заметил.
Заметил ещё одну ошибку.
$order = PDO::quote($order); // так работать не будет
$order = $db->quote($order); // а так будет
Да и вообще тут надо было просто использовать mysql_escape_string.

>> IN('1,2,3')
Тут тоже вижу выход. Только он увеличит реализацию ещё на несколько строчек. Вставить в цикле именованные плейсхолдеры с итеративным индексом и потом в цикле прогнать bindParam по этим плейсхолдерам.

Позвольте, я немного оправдаюсь)
На PDO ещё не приходилось так извращаться над запросами. Обычно решаемые мною задачи более тривиальны.
Но, чтобы подставить в конец строки правильный order by для выбранной пользователем сортировки, можно использовать массив. Количество возможных полей, ведь, ограничено, поэтому под каждое возможно поле создаём значение под нужным индексом, можно даже числовым и просто сравниваем полученную цифру из запроса пользователя с индексами в массивах, если есть такая, то подставляем название поля в запрос. Согласен, извращение, за-то какая защищённость!)

Да, и ещё, БД простила мне запрос с ORDER BY '`comments`' и выполнила его как надо. Я удивлён.
Да и вообще тут надо было просто использовать mysql_escape_string.

Это такое чудовищное заблуждение, что я даже комментировать не буду.
Чудовищное потому, что в это верят миллионы пользователей пхп.
(и, кстати, по этой же самой причине они считают мой подход бессмысленной поделкой)
Тут тоже вижу выход.

Ну разумеется, есть выход. Всё делается, с помощью кувалды и такой-то матери. Выше я привел пример как можно обойтись ещё меньшим кодом.
Изначально-то речь шла о том, что PDO никак не помогает в таких задачах. Ну, а дальше вылезло ваше непонимание того, как работают prepared statements — что произвольный кусок запроса в них запихнуть нельзя, только отдельный литерал.
чтобы подставить в конец строки правильный order by

Это не извращение, а как раз очень правильный подход.
Фильтрация по белым спискам — единственно правильное решение.
Мой код выше, хоть и не пропустит инъекцию, но вызовет ошибку запроса. Чтобы её не было, надо как раз проверять поле по заранее прописанному списку.
Вот здесь phpfaq.ru/examples#whitelist как раз показаны примеры правильного подхода к фильтрации.
БД простила мне запрос с ORDER BY '`comments`'

а вот это меня тоже удивило. Но, в любом случае, от инъекции это не спасёт.
>> Это не извращение, а как раз очень правильный подход.
Значит я на правильном пути. А извращением я это назвал исходя из количества кода. Чем больше кода — тем больше извращения. Я не говорю, что всегда так. Я так выразился только для данного контекста.
Тут есть только один нюанс.
По-хорошему, вайтлистить надо только операторы — AND, DESC и прочее.
Поскольку не существует других методов их валидации.

В то время как идентификатор можно правильно отформатировать (что и делает приведенный мной код).
Заниматься же проверкой на валидность значения, должен не уровень работы с БД, а уровень работы с внешними данными — контроллер.
Задача же драйвера БД — только синтаксически корректно оформить запрос.

Поэтому проверка идентификаторов по белому списку на этапе составления запроса — это костыль, на случай, если это не было сделано раньше.
Разность в скорости здесь не имеет значения. Я тут как раз тестировал на скорую руку, отвечая на один вопрос. Очень примерная разница составила 3 десятитысячных доли секунды. Положа руку на сердце — это не тот участок который надо оптимизировать. Но если так хочется, то prepared statements работают чуть медленнее, поскольку им приходится в среднем по два раза обращаться к базе на один запрос. Но я в своем классе их не использую не поэтому.

Самый безопасный вариант для PDO я привел выше.

Лучше, качественнее и безопаснее будет работать вариант на SafeMySQL. Поскольку он делает всё сам, не полагаясь на программиста. В этом и состоит суть безопасного составления запросов вообще, и моего класса в частности :)

А приведённый запрос не будет работать. Как минимум, по трём причинам.
Именованные плейсхолдеры и bindParam().
Черт возьми, жаль, что я не знал об этой библиотеке раньше. Отлично сделано.
Придраться можно только к двум вещам — довольно замороченный парсер с колбэками — у меня сделано гораздо проще.
И, конечно, слишком перегруженная функция query() — её сам бог велел разбить на несколько специализированных методов.
Предположу, что это рудимент с тех времён, когда класс ещё был функцией — судя по всему, был такой период в его биографии :)
Параметр, который определяет поведение функции, а не её результат, нужен только при процедурном подходе. При объектом удобнее будет разделить функционал на несколько методов.
Не знаю, как по мне то это задача никак не для класса работы с БД, возможно как security прослойка но не больше, по мне — лучше уж использовать всевозможные «построители» sql запросов и передавать в них корректные, обработанные данные тогда и работа с «частями» sql станет прозрачнее чтоли…
Вот такой код мне бы понравился больше…
А вот внутри он может делать всю ту работу которую делает ваш код, потому как обрабатываемых типов то всего: строка, число и «набор». Остальные относятся к деталям реализации… А определить тип данных (is_numeric, is_array etc) и вставить вместо? какой нибудь ?i или ?s не большая проблема.
Автоматом определить не получится.
Строка может быть как строкой, так и идентификатором.
Массив может быть передан как для in, так и для set.

К выбору людей, которые предпочитают построитель запросов я отношусь уважительно. Это отличная штука, и многие вещи позволяет сделать гораздо красивее, чем мой класс. Лично же для себя мнехочется сократить максимальную близость к SQL. Мне кажется, оба подхода имеют право на существование.
Ну например $some_id = '5'; и наш «анализатор» говорит что это строка, в where попадает что то вида «SELECT… WHERE id = ?s» что в итоге приведет к обработке переменной $some_id как строки с которой будут проделаны все «подготовительные» операции перед вставкой… Тоже самое если «анализатор» определит ее как число, просто набор «подготовительных» операций разный…
Передача данных в разное место запроса без «построителя» ставит вопрос «а куда собственно должны быть вставлены данные?» в случае с «построителем» такой вопрос не возникнет.
Какую задачу решает ваш код? В какой предметной области? Какие преимущества в использованном подходе? Вот что бы я хотел услышать…
Каждой задаче — свое решение, но как поведет себя ваш код в запросе на 200 строк? Удобно ли будет оперировать плейсхолдерами говорящими что вот в конкретное место вставляется строковые(числовые и т.п.) данные, но не говорящими откуда приходят данные или что за сущности в них… А это очень важно чтобы ваш код был легко читаемым…
Mysqli, self-made placeholders и в итоге код без prepare statement? В чем соль?
… наличие плейсхолдеров для любого типа данных...

А вот и проблема оптимальности и универсальности, как только вы начнете поддерживать «любой тип данных» код обрастет кучей сложной логики и перестанет быть простым, быстрым и отпимизированным, но пока вы этого не сделаете код не станет универсальным :)
В любом случае успехов вам в ваших исследованиях, надеюсь не остановитесь! Это просто стадия развития проекта такая — когда все вокруг говорят что ты делаешь велосипед. Ясно станет потом…
$some_id = '5' может быть подставлено в LIMIT. И. будучи обработанным, как строка, вызвать ошибку запроса.

$some_id может быть равно 'title'
И, следовательно, являться как именем поля, так и его содержанием.
Автоматически определить это невозможно. Да и не нужно.

$some_array может содержать массив.
Который может предназначаться как для оператора IN, так и для оператора SET. Каждый из которых требует своего форматирования

Вопроса, «а куда собственно должны быть вставлены данные?» я не понял. Я составляю SQL запрос, и, разумеется, я знаю, в какое место запроса идут те или иные данные.

Вопрос про предметную область очень интересный.
Область такая — применение SQL в реальной жизни.
Я тут в последнее время стал замечать, что все прекрасные инструменты остаются прекрасными только в рекламных проспектах. А в реальной жизни почему-то не работают.
И рядом с красивым ормом вырастает SQL, собранный руками по-старинке, а в контроллерах появляется куча HTML. Потому что, оказывается, красивый шаблонизатор не позволяет решать требуемые задачи, а если позволяет — то таким кодом, что лучше бы его не было.

Поэтому задачу класс решает очень скромную — безопасная работа с SQL. Но делает это хорошо.
Плюс попутно убирает бессмысленные повторяющиеся телодвижения из кода — просто потому, что я их терпеть ненавижу.

и в итоге код без prepare statement? В чем соль?

В том, что prepared statements не нужны. Они усложняют код, но не дают никакой пользы на практике. Да ещё и затрудняют отладку.
Единственная годная вещь в них — идея плейсхолдера.
Ну так её можно и саму по себе юзать, без prepare. Что я и делаю :)

как только вы начнете поддерживать «любой тип данных»

Не понимаю, почему вы пишете в будущем времени. Они уже поддерживаются. И парсер тут требуется совершенно примитивный.
А проблему «сложной логики» как раз решает функция parse()! :) Я не реализую никакую логику внутри класса — вся логика делается средствами PHP.
$some_id может быть равно 'title'
И, следовательно, являться как именем поля, так и его содержанием.
Так оно же не может, мы же там предполагаем идентификатор, вы же сами написали:
Я составляю SQL запрос, и, разумеется, я знаю, в какое место запроса идут те или иные данные.

Еще момент
Вопроса, «а куда собственно должны быть вставлены данные?» я не понял
Который может предназначаться как для оператора IN, так и для оператора SET
уловили?
В момент вставки данных вам нужно знать а что же надо будет сделать с данными и выбрать для этого соответствующий placeholder, при использовании bulder-ов об этом не приходится думать…

В предыдущей статье вы писали правильные вещи:
Тот же эскейпинг защиту не гарантирует
и рассказали про то чем хорош prepared statement и placeholders, но почему теперь пишете про то что:
prepared statements не нужны. Они усложняют код, но не дают никакой пользы на практике. Да ещё и затрудняют отладку.

По поводу типов: а как же enum, datetime, timestamp etc. Только не говорите что для них вполне подходит ?s и ?i
А, понятно. Вы говорите о билдере.
Это было очевидно с самого начала, но я был занят своими мыслями. Прошу прощения.
Да, для билдера, наверное, тип указывать не обязательно.
Я там выше сказал, что билдер — отдельный вариант, который имеет своих приверженцев, против которых я ничего не имею.
При этом необходимость указывать тип данных не считаю такой уж большой проблемой, особенно если взамен получаю чистый и наглядный SQL, который я уже знаю и который является lingua franca для разработчиков — его понимают все, в отличие от отдельно взятого билдера, который понимают только его адепты.
но почему теперь пишете

А, тут все просто. Prepared statement — не синоним плейсхолдера — вот тут я как раз специально написал об этом. Так что я не использую prepared statements, но очень активно использую плейсхолдеры — они являются основой всего класса.
По поводу типов: а как же enum, datetime, timestamp

А что с ними не так? Для них вполне подходит ?s. Можете привести пример обратного?
да я понимаю что это не одно и то же, и вы понимаете, и в предыдущей статье указали, так почему же в реализации с упором на безопасность вы игнорируете prepared statement?
Они усложняют код, но не дают никакой пользы на практике. Да ещё и затрудняют отладку.
По каждому пункту могу высказать совершенно обратное, поэтому без аргументов это всего лишь слова…

Ну вот вам пример с датами: в базе поле datetime а вы в скрипте оперируете unix_timestamp-ом, при подстановке вам нужно или отформатировать значение локальной переменной или заняться конвертированием на стороне БД…

Ну я предполагаю что вы мне ответите
SELECT * FROM table WHERE my_date_field > FROM_UNIXTIME(?i)
Просто как по мне то это несколько некрасиво чтоли… Понятно что все равно это делать придется, вопрос лишь в том «где это делать?» и «делать ли это руками?» Как по мне то с builder-ом проблема решается прощще… Вам же для автоматизации не хватает placeholder-а дат.

P.S.: В общем я про то что с builder-ом код остается читабельным даже если его разорвать до мелочей, в случае с плейсхолдерами код теряет в красоте и я не вижу ни каких преимуществ в таком подходе и собственно веду я к тому что посмотрите поближе на какой нибудь QueryBuilder, например от Zend вы его обязательно полюбите…
По каждому пункту могу высказать совершенно обратное, поэтому без аргументов это всего лишь слова…

Да что тут можно высказать-то?
Мне казалось, это очевидно из моих слов.
Пользу не приносят потому, что в 99.99% случаев у нас нет повторяющихся запросов в скриптах — значит, зря по два раза дергаем базу. Затрудняют отладку — опять же я выше уже писал аргумент — получить канонический запрос для отладки весьма проблематично.
При этом корректно отформатированное значение ничуть не менее безопасно, чем prepared statement. Следовательно, никаких преимуществ перед плейхолдером, обрабатываемым на клиенте, у prepared statements нет. А минусы — есть.
Если не согласны — приведите конкретный пример.
Единственный случай, когда prepared могут проявить себя — это микроскопический прирост в скорости, когда нам надо выполнить 100500 одинаковых запросов в консольном скрипте. Случай настолько исключительный, что лично я предпочитаю им пренебречь. И тот факт, что столь высоконагруженный проект как Badoo, не использует prepared statements, говорит нам о том, что слухи об увеличенной производительности несколько преувеличены.
Ну вот вам пример с датами: в базе поле datetime а вы в скрипте оперируете unix_timestamp-ом

Ради бога. Какое это имеет отношение к database layer?
Вы переворачиваете всё сног на голову. Вопрос был, как подставить значение в запрос. Ну так значение подставляется через плейсхолдер. Хотим подставить дату — ?s. Хотим добавить целое число секунд — ?i. Никаких проблем такие подстановки не вызывают, и более того — являются единственно возможным способом форматирования данных для соответствующих типов полей.
Отдельный «плейсхолдер для дат», разумеется, не нужен. Исходя из приведённого примера, он может понадобиться разве что для магии, чтобы волшебным образом догадаться, что из int-а надо сделать строку. Это неправильный подход. Но даже если он кому-то и нравится, то к работе с SQL это не имеет ни малейшего отношения.
Ок, ваша позиция по поводу prepared statements мне ясна, это конечно следовало ожидать но, явно, до этого, вроде, вы это нигде не написали…
Мы же говорим не только о подходе но и о «Классе для удобной и безопасной работы с MySQL» а из за различия в понимании «удобный» весь диалог…
Еще интересна ваша позиция по поводу «разорванного» sql, который в вашем случае, как я понимаю, будет приводить к
SELECT ?p FROM table ?p WHERE ?p GROUP BY ?p ORDER ?p LIMIT ?p
и само WHERE Это что то вида
?p ?p ?p ?p
или
$previous_where = '1=1';// какое то предустановленное условие
$where = $db->prepare('some_condition AND ?p', $previous_where);
//... 100500 строк логики
$where = $db->prepare('(?p) OR another_condition', $where);
//... 100500 строк логики
$where = $db->prepare('yet_another_condition AND((?p) OR another_condition2)', $where);

Вместо:
$stmt = new QueryBuilder();
$stmt->where('1=1');
$stmt->where('some_condition');
//... 100500 строк логики
$stmt->orWhere('another_condition');
//... 100500 строк логики
$stmt->orWhere('another_condition');
$stmt->where('yet_another_condition', QueryBuilder::prepend);
Я согласен с тем, что некоторые вещи на билдере выглядят элегантнее. Главное отличие в том, что доступ к элементам запроса произвольный, а не последовательный, как при сборке запроса руками.
Можно приготовить объект заранее, а потом менять ему условия. Это здорово удобно. Вот это как раз безоговорочное преимущество билдеров, которое я не оспариваю.
Но в случае достаточно сложных запросов код у них становится совершенно нечитабельным, и большинство известных мне разработчиков в таких случаях отказываются от билдера и пишут запрос вручную.

Кстати, PDO по умолчанию тоже использует не prepared statements, а форматирование на клиенте. Так что любые упрёки в небезопасности данного подхода предлагаю адресовать сначала им :)
А определить тип данных (is_numeric, is_array etc) и вставить вместо? какой нибудь ?i или ?s не большая проблема.


Это будет неверным решением. Определять тип данных нужно не по типу переменных (это же PHP), а по типу столбца (в самых простых случаях, в более сложных — проводить анализ запроса и схемы).Если приводить какой-нибудь $_GET['id'] к int ручками, то теряется смысл библиотеки.
Конечно же вы правы, просто вас сбили с толку имена стандартных php функций в скобках… Как анализатор будет анализировать типы данных — по заготовленной схеме, извлекать схему БД или как то еще я во внимание не беру. Кроме того это «ожидаемые» типы а я писал о «входящих» типах, тех, которые мы имеем для передачи в запрос. Их соответствие «ожидаемым» и обработка исключений в случае несоответствия — отдельная тема. Важно то, что теряется смысл использовать какой либо другой плейсхолдер кроме "?"
В случае с «построителем» запроса сохраняется контекст вставки данных и многие проблемы с этим связанные не возникают… Кроме того: разбить запрос на кучку строк и после собрать их воедино пускай и плейсхолдерами или работать с методами одного объекта в разных местах? Вот про что я!
Спасибо за проделанную работу! Интересный класс получается.
Хотелось бы всё-таки сравнение скорости для вашего класса, DbSimple и PDO. DbSimple ругают за медлительность (по крайней мере, кое-где видел).
Сам использую DbSimple, очень нравится, «магия» в возвращаемом значении select() не пугает.
Спасибо за добрые слова :)

Честно говоря, я не очень люблю тесты скорости. На моей памяти DAL ещё никогда не был узким местом.
Но, пожалуй, надо будет сделать. Как раз, сделать нормальный тест.
Небольшую страничку, на которой выполняется штук 5 стандартных запросов. Юзера там авторизовать, новости вывести.
И натравить на неё siege.
И, хоть я и уверен, что статистически значимое различие получить не удастся, цифры в любом случае будут красноречивее слов.

Всё-таки, при нынешних ценах на процессоры затраты на парсинг ничтожны.
Недавно сравнивал PDO в режиме эмуляции, и без. Разница составила 3 десятитысячные доли секунды на запрос. Три миллисекунды на страницу. При том, что затраты на исполнение самих запросов на порядок выше. Поэтому я буду оптимизировать запросы, а не обертку для их запуска.
>>При том, что затраты на исполнение самих запросов на порядок выше. Поэтому я буду оптимизировать запросы, а не обертку для их запуска

вот «золотые» слова, полностью полностью поддерживаю все принципы, которыми Вы руководствовались при написании класса. Буду пользовать однозначно)
Хоть я и не сталкиваюсь с проблемами описанными в вашей статье, да и к PHP особого отношения не имею, но читать было интересно и приятно. Спасибо за статью.

Заодно вопрос: как я понял метод parse() принимает текст sql запроса с параметрами и возвращает строку. Означает ли это, что я могу таким способом выделить, скажем, подзапрос из всего текста, пропарсить его отдельно, и потом использовать выданную строку в разных запросах, имеющих одинаковый подзапрос?

Если придется столкнутся с созданием SQL запросов из PHP, то обязательно изучу ваш класс :)
В принципе да, так можно, конечно. Но я бы не выделял работу с подзапросами отдельно.
Идея в том, чтобы таким образом обрабатывать любые элементы SQL — запросы, подзапросы, дополнительные условия.

Если придётся изучать — обязательно спрашивайте — подробно отвечу, как лучше сделать!
Удивительно! Действительно, очень совпадает.
Это уже третья библиотека, использующая подобный подход, на которую указали в комментах — и я не видел до этого ни одной. Статью стоило писать только для этого :)

У меня класс тоже сначала был функцией, как там :)
Но это, всё-таки, совсем неудобно. Хотя по сравнению с ручной сборкой запросов это всё равно огромный шаг вперёд
/* offtopic */

Развивая эту концепцию далее, мы приходим к мысли, что пейсхолдеры ...

Таки спасибо вам, вы обогатили мою речь новым расово верным словом! :-)
Хехе, прекрасно!
В словаре плейсхолдеров нет, а глаз у меня замылился. Отлично получилось, буду вводить в обиход :-)
Неплохо. Уже второй раз встречаю такой подход — добавить в плейсхолдер информацию о повторении [через запятую].
В общем, идея типизованных плейсхолдеров идёт в массы, и это не может не радовать. Может, через годик-другой увидим в мажорных фреймоворках, а там, глядишь, и до официальных API недалеко :)
Озаботился защитой от mysql инъекций. Спасибо, Ваш класс прост для освоения, и судя по всему эффективно решает свою задачу.
Скажите, как можно одним запросом вставить строку у получить ёё id (сгенерированный автоинкрементом)?
Запрос в любом случае будет один.
Если речь об обращении к функциям API, то никак — обращений будет два.
Первый оператор выполнит запрос, а второй получит id
$data = array(
    'name' => $name,
    'topic' => $topic,
);
$db->query($sql = "INSERT INTO table SET ?u", $data);
$id = $db->insertId()

Спасибо за класс, именно такой и искал.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации