PHP класс для удобной и безопасной работы с MySQL

После написания статьи про защиту от инъекций я взялся за написание класса, реализующего изложенные в ней идеи.
А точнее, поскольку ключевой функционал уже использовался в рамках рабочего фремворка, я занялся выделением его в самостоятельный класс. Пользуясь случаем, хочу поблагодарить участников PHPClub-а за помощь в исправлении нескольких критических ошибок и полезные замечания. Ниже я постараюсь описать основные особенности, но сначала небольшой
дисклеймер
Есть несколько способов работы с SQL — можно использовать квери-билдер, можно ORM, можно работать с чистым SQL. Я избрал последний вариант, потому что мне он ближе. Я совсем не считаю первые два плохими. Просто лично мне всегда было тесно в их рамках. Но я ни в коем случае не утверждаю, что мой вариант лучше. Это просто ещё один вариант. Который можно использовать, в том числе, и при написании ORM-а. В любом случае, я считаю, что наличие безопасного способа работать с чистым SQL не может принести какой-либо вред. Но при этом, возможно, поможет последним оставшимся приверженцам использования mysql_* в коде приложения, отказаться, наконец, от этой порочной практики.

В двух словах, класс строится вокруг набора функций-хелперов, позволяющих выполнять большинство операций с БД в одну строку, обеспечивая при этом (в отличие от стандартных API) полную защиту от SQL инъекций, реализованную с помощью расширенного набора плейсхолдеров, защищающих любые типы данных, которые могут попадать запрос.
В основу класса положены три базовых принципа:
  1. 100% защита от SQL инъекций
  2. При этом защита очень удобная в применении, делающая код короче, а не длиннее
  3. Универсальность, портабельность и простота освоения

Остановлюсь чуть подробнее на каждом из пунктов.

Безопасность


обеспечивается теми самыми двумя правилами, которые я сформулировал в статье:
  1. Любые — без исключений! — динамические элементы попадают в запрос только через плейсхолдеры.
  2. Всё, что не получается подставить через плейсхолдеры — прогоняется сначала через белый список.

К сожалению, стандартные библиотеки не предоставляют полной защиты от инъекций, защищая с помощью prepared statements только строки и числа.
Поэтому, чтобы сделать защиту полной, пришлось отказаться от заведомо ограниченной концепции prepared statements в пользу более широкого понятия — плейсхолдеров. Причём плейсхолдеров типизованных (эта вещь всем нам известна по семейству функций printf(): %d — плейсхолдер, который подсказывает парсеру, как обрабатывать подставляемое значение, в данном случае — как целое число). Нововведение оказалось настолько удачным, что разом решило множество проблем и значительно упростило код. Подробнее о типизованных плейсхолдерах я напишу ниже.
Поддержка же фильтрации по белым спискам обеспечивается двумя функциями, несколько притянутыми за уши, но, тем не менее, необходимыми.

Удобство и краткость кода приложения


Здесь мне также здорово помогли типизованные плейсхолдеры, которые позволили сделать вызовы функций однострочными, передавая сразу и запрос, и данные для него. Плюс набор хелперов, напоминающих таковые в PEAR::DB — функций, сразу возвращающих результат нужного типа. Все хелперы организованы по одной и той же схеме: в функцию передаётся один обязательный параметр — запрос с плейсхолдерами, и сколько угодно опциональных параметров, количество и порядок которых должны совпадать с количеством и порядком расположения плейсхолдеров в запросе. У функций семейства Ind используется ещё один обязательный параметр — имя поля, по которому осуществляется индексация возвращаемого массива.
Исходя из своего опыта, я пришёл к следующему набору возвращаемых значений (и, как следствие — хелперов):
  • query() — возвращает mysqli resource. Может использоваться традиционно, с fetch() и т.д.
  • getOne() — возвращает скаляр, первый элемент первой строки результата
  • getRow() — одномерный массив, первую строку результата
  • getCol() — одномерный массив скаляров — колонку таблицы
  • getAll() — двумерный массив, индексированный числами по порядку
  • getInd() — двумерный массив, индексированный значениями поля, указанного первым параметром
  • getIndCol() — массив скаляров, индексированный полем из первого параметра. Незаменимо для составления словарей вида key => value

В итоге большинство обращений к БД сводится одно-двух строчным конструкциям (вместо 5-10 при традиционном подходе):
$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit);

В этом коде есть только необходимые и значащие элементы, но нет ничего лишнего и повторяющегося. Все потроха аккуратно упрятаны внутрь класса: хелпер getAll() позволяет получить сразу нужный результат без написания циклов в коде приложения, а типизованные плейсхолдеры позволяют безопасно добавлять в запрос динамические элементы любых типов без прописывания привязок (bind_param) вручную. Extra DRY код! В случаях использования плейсхолдеров ?a и ?u разница в количестве кода становится ещё больше:
$data = $db->getAll("SELECT * FROM table WHERE category IN (?a)",$ids);

Универсальность и простота освоения


стоят на трех китах:
  1. Очень маленький API — пол-дюжины плейсхолдеров и столько же хелперов.
  2. Мы работаем со старым добрым SQL, который не надо заново учить.
  3. На первый взгляд незаметная, но невероятно полезная функция parse(), которая изначально предназначалась только для отладки, но в итоге выросла до ключевого элемента при составлении сложных запросов.

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

$sqlpart = '';
if (!empty($var)) {
    $sqlpart = $db->parse(" AND field = ?s", $var);
}
$data = $db->getAll("SELECT * FROM table WHERE a=?i ?p", $id, $sqlpart);

Здесь важно отметить несколько моментов.
Во-первых поскольку мы не связаны родным API, никто не запрещает нам пропарсить не весь запрос целиком, а только его часть. Это оказывается супер-удобно для запросов, собирающихся в соответствии какой-либо логикой: мы парсим только часть запроса, а затем она подставляется в основной запрос через специальный «холостой» плейсхолдер, чтобы избежать повторного парсинга (и соблюсти правило «любые элементы подставляются только через плейсхолдер»).
Но, к сожалению, это является слабым местом всего класса. В отличие от всех остальных плейсхолдеров (которые, даже будучи использованы неверно, никогда не приведут к инъекции) некорректное использование плейсхолдера ?p может к ней привести.
Однако защита от дурака сильно усложнила бы класс, но при этом все равно никак не защитила бы от тупой вставки переменной в строку запроса. Поэтому я решил оставить как есть. Но если вы знаете способ, как без слишком большого оверинжиниринга решить эту проблему — я был бы благодарен за идеи.

Тем не менее, в итоге мы получили мощный и лёгкий генератор запросов, который с лихвой оправдывает этот небольшой недостаток.
Мощный потому, что мы не ограничены синтаксисом квери-билдера, «SQL-ем, написанным на PHP» — мы пишем чистый SQL.
Лёгкий потому, что весь API составления запросов состоит из полудюжины плейсхолдеров и функции parse()
Вот мой любимый пример — вставка с использованием функций Mysql
$data = array('field'=>$value,'field2'=>$value);
$sql  = "INSERT INTO table SET ts=unix_timestamp(), ip=inet_aton(?s),?u";
$db->query($sql, $ip, $data);

С одной стороны, мы сохраняем синтаксис SQL, с другой — делаем его безопасным, а с третьей — капитально сокращаем количество кода.

Подробнее о типизованных плейсхолдерах


Сначала ответим на вопрос, почему плейсхолдеры вообще?
Это, в общем, уже общее место, но, тем не менее, повторюсь — любые динамические данные должны попадать в запрос только через плейсхолдеры по следующим причинам:
  • самое главное — безопасность. Добавив переменную черз плейсхолдер, мы можем быть уверены в том, что она будет корректно отформатирована.
  • локальность форматирования. Это не менее важный момент. Во-первых, данные форматируются непосредственно перед попаданием в запрос, и не затрагивают исходную переменную, которая потом может быть использована где-то ещё. Во-вторых, данные форматируются ровно там, где нужно, а не до начала работы скрипта, как при magic quotes, и не в десяти возможных местах кода несколькими разработчиками, каждый из которых может понадеяться на другого.

Развивая эту концепцию далее, мы приходим к мысли, что пейсхолдеры обязательно должны быть типизованными. Но почему?
Тут я бы хотел ненадолго остановиться, и проследить историю развития программистской мысли в области защиты от инъекций.
Сначала был хаос — вообще никакой защиты, пихаем всё как есть.
Дальше стало не сильно лучше, с парадигмой «искейпим всё, что пришло в скрипт от пользователя» и кульминацией в виде magic quotes.
Дальше лучшие умы пришли к тому, что правильно говорить не об экранировании, а о форматировании. Поскольку форматирование не всегда сводится к одному искейпингу. Так в PDO появился метод quote(), который делал законченное форматирование строки — не только экранировал в ней спецсимволы, но и заключал её в кавычки, не надеясь на программиста. В итоге, даже если программист использовал эту функцию не на месте (например, для числа), то инъекция всё равно не проходила (а в случае с голым экранированием через mysql_real_escape_string она легко проходит, если мы поместили в запрос число, не заключая его в кавычки). Будучи же использованной для форматирования идентификатора, эта функция приводила к ошибке на этапе разработки, что подсказывало автору кода о том, что он немножечко неправ.
К сожалению, на этом авторы PDO и остановились, поскольку в головах разработчиков до сих пор крепко сидит мысль о том, что форматировать в запросе надо только строки. Но на самом деле в запросе гораздо больше элементов самых различных типов. И для каждого нужен свой собственный тип форматирования! То есть, единственный метод quote() нас никак не устроит — нужно много разных quotes. Причём не в качестве исключения, «нате вам quoteName()», а как одна из главных концепций: каждому типу — свой формат. Ну а раз типов форматирования оказывается много — тип надо как-то указывать. И типизованный плейсхолдер для этого подходит лучше всего.

Кроме того, типизованный плейсхолдер — это ОЧЕНЬ удобно!
Во-первых, потому что становится ненужным специальный оператор для привязки значения к плейсхолдеру (но при этом сохраняется возможность указать тип передаваемого значения!)
Во-вторых, раз уж мы изобрели типизованный плейсхолдер — мы можем налепить этих плейсхолдеров огромное количество, для решения множества рутинных задач по составлению SQL запросов.
В первую очередь сделаем плейсхолдер для идентификаторов — нам его отчаянно не хватает в реальной, а не воображаемой авторами стандартных API, жизни. Как только девелопер сталкивается с необходимостью динамически добавить в запрос имя поля — каждый начинает извращаться по-своему, кто в лес, кто по дрова. Здесь же всё унифицировано с остальными элементами запроса, и добавление идентификатора становится не сложнее добавления строки. Но при этом идентификатор форматируется не как строка, а в соответствии со своими собственными правилами — заключается в обратные кавычки, а внутри эти кавычки экранируются удвоением.
Дальше — больше. Следующая головная боль любого разработчика, когда-либо пытавшегося использовать стандартные prepared statements в реальной жизни — оператор IN(). Вуаля, у нас есть плейсхолдер и для этой операции! Подстановка массива становится не сложнее любых других элементов, плюс она унифицирована с ними — никаких отдельных функций, меняется всего лишь буква в плейсхолдере.
Точно таким же образом делаем и плейсхолдер для SET. Не удержусь и продемонстрирую, насколько простым становится код для такого замороченного запроса, как INSERT… ON DUPLICATE:
$data = array('offers_in' => $in, 'offers_out' => $out);
$sql  = "INSERT INTO stats SET pid=?i,dt=CURDATE(),?u ON DUPLICATE KEY UPDATE ?u";
$db->query($sql,$pid,$data,$data); 

В данный момент классом поддерживается 6 типов плейсхолдеров
  • ?s («string») — строки (а также DATE, FLOAT и DECIMAL).
  • ?i («integer») — целые числа.
  • ?n («name») — имена полей и таблиц
  • ?p («parsed») — для вставки уже обработанных частей запроса
  • ?a («array») — набор значений для IN (строка вида 'a','b','c')
  • ?u («update») — набор значений для SET (строка вида `field`='value',`field`='value')

Что вполне достаточно для моих задач, но этот набор всегда можно расширить любыми другими плейсхолдерами, например, для дробных чисел. Делать отдельный плейсхолдер для NULL я не вижу смысла — его можно всегда вписать прямо в запрос.
Автоматическую трансляцию PHP-шного NULL в SQL-евский NULL я решил не делать. Возможно, это чуть усложнит код (в тех редких случаях, когда это нужно), но зато уменьшит его неоднозначность.

Кстати, как многие могли заметить, этот класс во многом напоминает библиотеку DbSimple Дмитрия Котерова. Но у меня есть принципиальные расхождения с некоторыми идеями, заложенными в неё.
Во-первых, я противник любой магии, когда одна и та же функция может возвращать различный результат в зависимости от типа переданных данных. Это, возможно, чуть упрощает написание, но при этом чудовищно затрудняет сопровождение и отладку кода. Поэтому в моем классе вся магия сведена к минимуму, а все операции и типы данных всегда прописываются явно.
Во-вторых, в DbSimple немного, на мой взгляд, переусложнённый синтаксис. С одной стороны, фигурные скобки — гениальная идея. С другой — а зачем, если в нашем распоряжении вся мощь PHP? Поэтому я решил пойти другим путём и весто «внутренней» — заведомо ограниченной — логики ввёл «внешнюю», ограниченную лишь синтаксисом РНР. Главное, чтобы любые динамические элементы попадали в запрос только через плейсхолдеры, а остальное зависит лишь от фантазии разработчика (и функции parse()).

Код класса доступен на Гитхабе, github.com/colshrapnel/safemysql/blob/master/safemysql.class.php
Cheat sheet с основными командами и примерами: phpfaq.ru/misc/safemysql_cheatsheet_ru.pdf
Хорошее представление о возможностях можно получить на странице примеров документации (к сожалению, ещё не законченной), phpfaq.ru/safemysql
Там же есть ответы на часто задаваемые вопросы, такие как «почему ты не используешь родные prepared statements?» и пр.
Тем не менее, буду рад ответить на любые вопросы в комментариях, а так же улучшить по вашим замечаниям как сам класс, так и эту статью.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 103

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

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

        Idiorm очень лаконичная библиотека.
          0
          Спасибо, 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 — значит, кроме скаляров больше ничего от инъекций не защищается.
            +1
            Очень не советую idorm для чего-то сложного. Как только всё отходит от банального crud тут же приходится писать sql руками, что там очень уж не удобно.
          +4
          Интересно, что категория людей, говорящих про велосипеды, и категория людей, читавших оба поста автора (с причинами написания этой библиотеки) не пересекаются.
          +24
          С нетерпением жду статью про снег на JavaScript.
            0
            И снег на яваскрипте можно написать по-новому.

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

            Куда менее обидно было бы услышать предметную критику, чем ленивый зевок «Знаем, плавали».
            +4
            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);
            }
            

              –1
              Видел, и даже написал об этом. У 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 запрос.

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

                  –1
                  Спасибо за интересный вопрос.
                  $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);
                  

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

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

                        Кстати, подобные обёртки или прямая работа SQL и ORM (как паттерн, а не как фреймворки для его реализации) друг друга не исключают. В текущем проекте пришёл к классам репозиториев, которые жестко (hard coded) мапят БД на объекты, получая удобство ООП и SQL без лишнего оверхида свойственного универсальным ORM.
                          0
                          В том-то и дело, что это не yet another DAL. А точнее — не столько. В первую очередь это попытка окончательно решить проблему инъекций. Покрыв при этом 100% случаев. В отличие от существующих решений, у которых всё гладко только на бумаге. И в примитивных примерах.
                          А как дело доходит до реальной жизни, то все good practices почему-то всегда куда-то испряются из кода. И SQL шпарится напрямую, с подстановками переменных, из шаблонов обращаются к базе данных (а если шаблон не нативный — значит куча HTML-а пишется в контроллере), и так далее.
                          Возможно, это перфекционизм, но я последовательно копаю некоторые темы в веб-разработке, пытаясь покрыть 100% применения, а не только самые ходовые случаи.
                          В данном случае мне это удалось (во всяком случае, пока никто не доказал обратного). Но я провалил задачу донести результат до окружающих.
                            +1
                            >>В данном случае мне это удалось (во всяком случае, пока никто не доказал обратного).
                            $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, но это удобство, а никак не безопасность.
                              0
                              В таком случае обёртка должна дублировать 100% функционала SQL. Всякие sqlExpression не подойдут — туда точно так же руками чего угодно напихать можно. Поэтому я сильно сомневаюсь как в принципиальной достижимости этой цели, так и в осмысленности задачи «Написать SQL на РНР».

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

                              Попробую на днях это решение, пока API выглядит привлекательней моего. Информацию о типах я передаю отдельно.
                            –3
                            10 лет пользуем dal в одном классе от форума vbulletin слегка переписаный. Работает. Изучать другие велосипеды лень.
                              +4
                              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? Я в своей работе использую её. Мне кажется, это был бы интересный и полезный для общественности форк.
                                0
                                Да, пожалуйста, изложите аналог для ПГ.
                                  0
                                  Спасибо за развёрнутый комментарий. Он требует времени на написание развернутого ответа, поэтому пока только по нескольким пункту:
                                  Для pg — вот ниже привели ссылку на библиотеку, реализующую тот же самый принцип — pyha.ru/go/godb/
                                  в которой есть драйвер для Постгре. Но в любом случае, если появится драйвер (потребуется переопределить функции, взаимодействующие с API и методы искейпинга), то это будет просто замечательно.

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

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

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

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

                                  ps: На гитхабе запрещено по русске писать! :)
                                    +1
                                    Also *Col, *Ind, *IndCol м.б. конвертнуть в более полные формы?
                                    • UFO just landed and posted this here
                                        0
                                        to FanatPHP — просто читаю сейчас, что кто писал по теме )

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

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

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

                                              Да и вместо 0=1 можно написать FALSE. Ведь 0=1 это незнание, а не способ оптимизации, ведь так?!?! :-)
                                                0
                                                Ну я сомневаюсь что Кьенг или Александр, делали это от незнания, может просто так решили именно оставить эту ситуацию. Да, это генератор запросов, commandBuilder.
                                              0
                                              А если копать в сторону того, что MySQL легко перегоняет различные типы данных в строки и обратно, и на основе этого подставлять что нибудь вроде пустой строки
                                              SELECT * FROM id NOT IN ('')
                                              Я подразумеваю что в данном случае будут получаться только те строки, где id установлен и не пустой, что поидее будет выдавать ожидаемый результат.
                                              Однако я не могу предусмотреть, что может получиться в других случаях подстановки пустых массивов…
                                                0
                                                К сожалению, это «грязный» вариант.
                                                Ведь речь не только о числах. но и о любых данных. и пустая строка вполне может быть законным значением…
                                                  +1
                                                  Ну вообще да…
                                                  Но если так подумать… ведь как я понял, смысл класса — генерация строки запроса, т.е. фактическая подстановка переданных параметров, но в определенном виде.
                                                  Если смысл классов таков, то на мой взгляд логичным исходом подстановки пустого массива является как раз пропуск этого массива.
                                                  Всмысле получение строки
                                                  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);
                                                  

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

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

                                                    Всё это мой последний вариант.
                                                    0
                                                    Добавляй иссую на гитхаб. Верная мысль.
                                                      0
                                                      Я, кстати, тогда жестко затупил с твоим вопросом на СО :)
                                                      Память на ники никакая. Плюс, поторопился, конечно — толком не прочитал вопрос.
                                                      Имеет смысл, кстати, на dba.* спросить.
                                                      Я вот всё думаю, можно ли как-то инвертировать NULL, а точнее, сконвертировать в BOOL.
                                                        0
                                                        Может всё не так страшно. У нас в проекте NOT IN используется только один раз. Только жаль, что приходится вручную следить за этим.
                                                          0
                                                          Ну, тут два, на самом деле, критерия: частный и общий.
                                                          Для частного понятно, что беда небольшая.
                                                          Но вот класс, которые используют люди, не понимая его тонкостей (я тут на одном форуме обнаружил чувака, который вовсю использует… без единого плейсхолдера!) — это меня немного поднапрягает. Я же ведь наоборот старался, чтобы не было никаких неоднозначностей. Впрочем, здесь всегда можно со спокойной душой перевести стрелки на Mysql :)
                                                          Ну, и плюс к тому — чисто исследовательский интерес — можно ли проблему решить в принципе
                                                      0
                                                      Я когда-то сам пользовался классами DbSimple, но потом узрел всю прелесть работы с PDO и теперь все новые проекты я делаю именно с PDO. Мне даже кажется, что PDO значительно более гибкая, чем DbSimple. Да и PDO внушает больше доверия, чем всё остальное.
                                                      Хотя соглашусь с тем, что по объёму кода DbSimple немного выигрывает, его часто необходимо меньше, чем в PDO.
                                                        0
                                                        Ну, API всегда будет «гибче» абстракции.
                                                        Но, как говорилось выше, API — это набор «сделай сам». Слишком многое приходится делать руками.
                                                        Те же подстановки имени поля, массивов для IN и SET. Чтобы INSERT с динамическим числом полей, приходится очень сильно попотеть. Я бы не назвал разницу между одной строкой и экраном кода словом «немного».
                                                        При этом я как-то не вижу какой-то особой «негибкости» у DbSimple. Есть конкретный пример того, что не смогла она, но смог чистый PDO?
                                                          0
                                                          Сам хотел попросить вас привести пример, когда что-то можно сделать вашим классом и что нельзя/сложно сделать через PDO.
                                                            0
                                                            Ой, это я с огромным удовольствием.
                                                            Скажем, пользователь выбрал в чекбоксами разделы новостей, которые он хочет смотреть. Они лежат в массиве $_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, вроде бы, должен бы избавить.
                                                            Всякий раз, когда я смотрю на мучения посетителей стаковерфлоя с динамическими инсертами — у меня сердце кровью обливается.
                                                              +1
                                                              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)), и подставлять в запрос.

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

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

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

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

                                                                        Именно это и делает код, который я привел выше
                                                                          0
                                                                          >> 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`' и выполнила его как надо. Я удивлён.
                                                                            0
                                                                            Да и вообще тут надо было просто использовать mysql_escape_string.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                              Ради бога. Какое это имеет отношение к database layer?
                                                                              Вы переворачиваете всё сног на голову. Вопрос был, как подставить значение в запрос. Ну так значение подставляется через плейсхолдер. Хотим подставить дату — ?s. Хотим добавить целое число секунд — ?i. Никаких проблем такие подстановки не вызывают, и более того — являются единственно возможным способом форматирования данных для соответствующих типов полей.
                                                                              Отдельный «плейсхолдер для дат», разумеется, не нужен. Исходя из приведённого примера, он может понадобиться разве что для магии, чтобы волшебным образом догадаться, что из int-а надо сделать строку. Это неправильный подход. Но даже если он кому-то и нравится, то к работе с SQL это не имеет ни малейшего отношения.
                                                                                0
                                                                                Ок, ваша позиция по поводу 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);
                                                                                
                                                                                  0
                                                                                  Я согласен с тем, что некоторые вещи на билдере выглядят элегантнее. Главное отличие в том, что доступ к элементам запроса произвольный, а не последовательный, как при сборке запроса руками.
                                                                                  Можно приготовить объект заранее, а потом менять ему условия. Это здорово удобно. Вот это как раз безоговорочное преимущество билдеров, которое я не оспариваю.
                                                                                  Но в случае достаточно сложных запросов код у них становится совершенно нечитабельным, и большинство известных мне разработчиков в таких случаях отказываются от билдера и пишут запрос вручную.

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


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

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

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

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

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

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

                                                                        Если придётся изучать — обязательно спрашивайте — подробно отвечу, как лучше сделать!
                                                                        0
                                                                        Стиль состаления SQL запросов, мне очень напоминает на работу с БД в CS cart.
                                                                        Пример docs.cs-cart.com/database-standards
                                                                          0
                                                                          Удивительно! Действительно, очень совпадает.
                                                                          Это уже третья библиотека, использующая подобный подход, на которую указали в комментах — и я не видел до этого ни одной. Статью стоило писать только для этого :)

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

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

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

                                                                                  0
                                                                                  Спасибо за класс, именно такой и искал.

                                                                                  Only users with full accounts can post comments. Log in, please.