Деловая неделя

Удобная и приятная работа с сайтом целиком зависит от правильного деления задач программирования на блоки (или наоборот, объединения в блоки). Важно также, в каком направлении делить, в каком разрезе (вдоль или поперёк)...

ЕЖЕНЕДЕЛЬНАЯ РЕКЛАМНАЯ ГАЗЕТА ДЛЯ ПРЕДПРИЯТИЙ «Деловая неделя» (Иркутск)

Несколько функций для работы с базой данных (PHP)

Все мы, старые веб-программисты, с детства помним примеры работы с базами данных из справочника php_manual_ru:

А может, уже и не помним, замороченные всякими PEAR && PDO. Но иногда вспоминаем, как трудно было работать с "голым" PHP из справочника. А теперь вот мне захотелось понять, почему же это было так трудно – работать, следуя справочному руководству разработчиков PHP.

Ответ, в общем-то, заранее известен, и он достаточно прост: все проблемы в программировании возникают от непоследовательности. Приведённый пример из справочника плох тем, что он изначально закладывает кривой фундамент в код начинающего программиста. Почему вызов первой функции примера (mysql_connect) обрабатывается на предмет ошибки (mysql_error), а все остальные функции – нет?

Допустим, соединились мы с БД успешно, и даже случайно там оказалась таблица "users", но в этой таблице, например, нет поля "status". Что мы получим на экране? Кучу строк с сообщением об одной и той же ошибке. Поэтому крутые гуру говорят: всегда отключайте вывод ошибок на экран (ini_set("display_errors", false);). Но другие крутые гуру так же страстно призывают не скрывать никакие ошибки. И покорные ученики выбирают худшее из зол – золотую середину, в половине случаев выводя ошибки, а в половине пряча с помощью @ ("собаки").

Между тем, строго последовательные (пусть даже выглядящие иногда тупыми) действия делают программирования простым и приятным. Пример из руководства в одном из крайних выражений последовательности можно переписать так:

И нам уже неважно, существуют БД и таблица или нет, мы спокойны, потому что пользователь никогда не увидит никакой ошибки. Ну, в крайнем случае, будет страница, состоящая из одного заголовка, ну ничего ведь страшного?

Можно запрограммировать всё, весь сайт в таком стиле. И, теоретически, такой сайт может работать вечно. Но теория суха, а жизнь всё время меняется (даже в песне поётся: "Перемен! Жизнь требует перемен!"). Жизнь заставит вас рано или поздно перенести сайт на другой хостинг. Или добавить в таблицу ещё одно поле. Или ещё что-нибудь поменять. И тогда вам придётся чертыхаться, кляня себя за старательно расставленные повсюду "собаки".

Разделяй и властвуй

Удобная и приятная работа с сайтом целиком зависит от правильного деления задач на блоки (или наоборот, объединения в блоки). Важно также, в каком направлении делить, в каком разрезе (вдоль или поперёк).

Первое деление (которое, собственно, и порождает часто непоследовательность) – решить, чья это задача, кто будет её давать программе и читать потом сообщения об ошибках. Обычно таких командира бывает два: пользователь и программист. Пользователь, например, ставит программе задачу "проверить мой пароль", и должен получать от программы сообщения о своих ошибках – например, "в пароле должны быть только латинские буквы и цифры". А программист ставит, например, задачу "работать с БД `mybd`", и должен получать сообщения об ошибках вида "Unknown database 'mybd'".

Иногда у пользователя бывает возможность самому установить какую-то базу данных на сайт (например, в проекте "Инфодиск"). Тогда он соответственно своей возможности тоже должен видеть ошибку (заботливо переведённую программистом на русский язык) "База данных `mybd` не установлена". Вопрос, должен ли пользователь видеть ошибки программиста, неоднозначен и часто зависит от заказчика сайта. Но мы отвечаем на него просто: ошибки программиста обычно фатальны (сайт вообще не работает), поэтому их надо показывать всегда – немного непонятной информации на английском языке лучше полного отсутствия информации (чисто белой страницы).

Следующая отделяемая часть известна всем: это подключение к mysql. Обычно это действие выносят в отдельный файл (config.php, connect.php...), подключаемый к основным скриптам через include. В большинстве случаев туда же можно отнести и выбор текущей БД (ну, и установить кодировку до кучи):

Дальнейшее разделение задач при работе с БД является полнейшим произволом программиста. Мечтой веб-мастера является функция sql2page($sql), принимающая в качестве параметра sql-запрос и возвращающая готовую HTML-страницу. Но даже такая функция не была бы конечной и самодостаточной, хотя бы потому что sql-запрос не является константой, а при каждом вызове скрипта должен быть аккуратно сконструирован при помощи каких-то других функций.

Конструирование запроса мы рассмотрим ниже. А сейчас – разделение внутри sql2page. Ясно ведь, что такая волшебная функция не может быть простой. Внутри неё обычно используют отдельный блок – другую функцию с mysql_fetch_assoc(), превращающую ресурс, возвращаемый запросом, в (более удобный?) массив. Это не всегда очевидное разделение: зачем проходить в цикле ресурс mysql, превращая ресурс в массив, а потом ещё раз в цикле обрабатывать массив?

Если, например, ресурс большой по объёму (100К) и просто содержит данные, которые надо один раз вывести на страницу, это явно неоптимально – два раза проходить большой цикл. Но если надо получить одну «ячейку» таблицы (например, количество записей – по запросу вида select count(*) from `user`), ясно, что функция, превращающая sql-запрос в значение, здесь бы очень упростила работу. Другая причина – неоднократное использование результатов запроса (в этом случае тоже оптимальнее быстро «перекачать» ресурс в массив и дальше работать с массивом).

Здесь на сцену незаметно выходит ещё одна дилемма – «об оптимальности». На самом деле даже целый класс дилемм, которые не всегда легко разглядеть, но которые всё равно давят на нас, даже неявно:

  1. Скорость написания – скорость чтения кода (удобочитаемый код надо дольше писать).
  2. Скорость чтения интерпретатором – скорость чтения человеком.
  3. Скорость чтения (интерпретатором) – скорость исполнения.
  4. Сложность структуры кода – расширяемость приложения.

Это достаточно приблизительные метки. Можно, например, ещё смотреть на проблему под углом длины кода (чем длиннее код, тем он... читаемее? быстрее работает?.. – или "более удобный для человека код должен быть более длинным"?).

Собственно sql

Исходя из всех (или части?) этих пересекающихся дилемм, мы для начала отделили сам момент получения sql-ресурса, приблизительно в такую функцию:

Удобство такого деления мы видим вот в чём:

  1. При обработке sql-ресурса всегда проверяется количество возвращаемых строк (mysql_num_rows) – разумно это обобщить, выделить в функцию.
  2. Мы не знаем заранее, как лучше выдать результаты sql-запроса – сразу в виде готового HTML, или предварительно переведя в массив. Если сразу, функцию sql_get_resource будет использовать функция визуализации; если сначала нам нужен массив, функцию sql_get_resource будет использовать функция sql_select_col или ещё какая-нибудь sql_select_cell (хотя в последнем случае возвращается не массив, а строка).
  3. Функция всегда возвращает количество строк ресурса (даже если ресурса нет): если это значение равно false, в первом элементе возвращаемого массива можно попытаться прочитать сообщение об ошибке (или просто игнорировать дальнейшую обработку); если количество строк равно нулю, ресурс нормальный (можно, например, работать с наименованиями полей sql-таблицы), но данных для вывода (на экран или ещё куда) не содержит.

Функция получения массива из одной колонки таблицы sql_select_col – одна из самых простых и приятных. Особенно если у нас уже есть функция sql_get_resource :-):

Ещё проще будет sql_select_cell, так что о ней здесь даже и говорить смешно. Остальные функции (получения одной строки и получения многих строк) усложняются вполне предсказуемо.

До отправки запроса (конструирование sql)

Возьмём простой скрипт, которому пользователь передаёт параметры через url (GET). Например, скрипт авторизации с url вида auth.php?user=me&pw=123. Что самое очевидное, что должен сделать программист при передаче GET-параметров в sql? Дружный хор мастеров утверждает, что это защита от инъекций: что-нибудь вида $pw = mysql_real_escape_string($_GET['pw']), ну, или уж никак не меньше, чем !addslashes!. И этот совет, в отрыве от реального контекста, способен создать начинающему веб-разработчику не меньше проблем, чем атаки гипотетических хакеров.

Суровая реальность заключается в том, что на хостинге в настройках PHP может стоять !magic_quotes_gpc = on!, а начинающий (да и заканчивающий) программист может не знать (да и не очень обязан знать), что эту настройку во время исполнения скрипта изменить !нельзя! (!). Так простой на первый взгляд addslashes превращается в отдельную пользовательскую функцию:

Далее, на том или ином этапе обязательно присустствует проверка существования входящих параметров (isset($_GET['pw'])). Она усложнаяется тем, что мы не знаем заранее, что может прийти в $_GET. Но мы знаем, что нам надо для данного скрипта – отсюда возникает другой вариант проверки magic_quotes_gpc:

На входе эта функция в качестве параметра получает массив, состоящий из наименований (индексов), которые мы хотели бы получить из $_GET. Если хотя бы одно значение отсутствует, фукнция возвращает false. Если всё в порядке, можно передавать результат дальше для генерации sql-запроса. Откуда-то мы (наш скрипт) уже узнали, из какой таблицы и какие брать данные. Теперь вот ещё знаем, по каким полям и значениям производить отбор. Всё вместе – выйдет нечто вроде:

SELECT `name`, `status` FROM `users` $wherestr,

где $wherestr сгенерировано ещё одной функцией:

Н-да. И ещё одна функция понадобится:

Чем дальше, тем, блин больше. На... зачем, спрашивается, такие сложные функции, там, где можно было бы обойтись простым выражением:

А вот зачем: чтобы в WHERE могло попасть выражение вида "WHERE pw=old_password('123')", то есть с функцией MySQL (а вы думали так всё просто будет?).

Ну, зато со всеми нужными функциями сам действующий скрипт будет состоять всего из 15-ти строк. Вот он: http://ir2.ru/test_auth.php.gz. А вот несколько тестовых ссылок:

  1. http://ir2.ru/test_auth.php?pw=123
  2. http://ir2.ru/test_auth.php?pw=123&name=mi
  3. http://ir2.ru/test_auth.php?pw=123&name=me

С этого момента, собственно, только и начинается настоящая работа. Ведь это, например, совсем не царское дело – помнить, где какой old_password при генерации WHERE подрисовывать. Надо для начала хотя бы собрать сведения обо всех полях всех таблиц сайта и занести в специальную таблицу. А потом, при составлении массива WHERE извлекать нужные сведения о полях из сделанной спецтаблицы. Нужно учесть также, что, например, дата из привычного нам формата dd.mm.yy в MySQL должна превращаться в YYYY-mm-dd. После этого можно уже будет, пожалуй, переходить и к основной части: постраничному выводу информации из больших таблиц, организации поиска на сайте, организации добавления новой информации...

D.M., admin

Читать все комментарии (0)

Добавить комментарий:

*Автор:
E-Mail:
*Текст: