Веб-программирование на PHP. (Часть I)

Руслан Курепин
http://kurepin.ru/main.phtml

Начало
Каталоги и БД
ООП и наши классы
vars.class
mysql.class
Язык SQL
Язык SQL и class_utils
class_utils (продолжение)
class_in (начало)
class_in (рубрики)

Веб-программирование на PHP (Часть II)


^^^   Начало

PHP — язык очень интересный и очень эффективный, если речь идет о web-программировании (далее, для простоты общения, я не буду употреблять приставку web). Но, к моему глубокому изумлению, обнаружилось, что очень многие программисты на том же perl продолжают игнорировать PHP только потому, что не могут найти помощи в освоении нового языка. Действительно, perl очень распространенный и очень удобный язык. Но во многих случаях гораздо удобнее, быстрее и функциональнее писать на PHP. Мои выпуски, посвященные PHP, направлены в сторону тех, кто уже знаком с каким-либо языком программирования. Это не обязательно, но крайне желательно. Ибо я не собираюсь учить вас писать

<? echo "Hello world!"; ?>

но и постараюсь не использовать слов и терминов, пугающих нормального человека, не ведущего тяжелую жизнь системного программиста. Если дать краткую характеристику читателю, на которого нацелена серия выпусков "Пишем на PHP", то это будет человек, увлекающийся созданием сайтов, не боящийся экспериментировать и знакомый с основами верстки html.

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

Несколько ответов на вопросы.

Вопрос: Как читать курс?
Ответ: Подряд. Это обычный текст. Я стараюсь делать его последовательным. Важное я буду отмечать красным, лирические отступления серым, а полезные замечания — зеленым.

Вопрос: А если что-то непонятно?
Ответ: Спроси. Где? В форуме. Адрес форума: http://forum.kurepin.ru

Вопрос: Какое программное обеспечение понадобится для работы?
Ответ: На сервере: Apache, PHP, MySQL. Можно еще добавить старенькую графическую библиотеку GD, вдруг меня занесет в сторону издевательства над GIF-ами.
На рабочей машине вам понадобится любой текстовый редактор. Лично я предпочитаю работать в FAR, подключив к нему colorer. Ничего удобнее я все равно так и не нашел.

Вопрос: А как собрать Apache с PHP и MySQL?
Ответ: Спросите об этом своего админа, спросите на форуме или у google.

Чем PHP отличается от других языков?

По большому счету, ничем. Лично я считаю основным отличием PHP от других языков для web — удобство использования в html-текстах. Нет почти никаких ограничений в использовании PHP-вставок.

Не надо использовать PHP и SSI в одном файле. Apache, а я надеюсь, что вы пользуетесь именно им в качестве web-сервера, не будет интерпретировать вам и то и другое в одном флаконе. Да это и не надо. Обращаю на сей факт внимание только потому, что многие поначалу пытаются подружить SSI и PHP. Не нужно этого. Не тратьте время.

В любом месте html-кода вы можете смело вставить заветную комбинацию:

<? ... ?>

где вместо многоточия может быть любой сложности скрипт.

Дам несколько наглядных примеров "прямого программирования", а затем перейдем сразу к написанию какого-нибудь цельного сайта со всеми примочками. Так вам будет проще все понять.

Цвет строки

Создаем файл example-1.php и пишем в него простейший код:

<font color=#<? echo $color; ?>>Подопытная строка</font>
<form action="" method=GET>
<input type="text" name=color>
<input type="submit" value="сменить цвет">
</form>

Все. Программа готова. Если ваш Apache собран с PHP, то можете попробовать ввести в окошко разные цвета и нажать на кнопочку. Попробуйте у меня:

Подопытная строка

Напомню, что цвет задается шестнадцатеричным числом (RGB) от 0000000(черный) до FFFFFF(белый).
FF0000 — красный
00FF00 — зеленый
0000FF — голубой

Видите, как все просто. PHP сам распознал переменную $color, в которой вы передали код цвета. Вам не пришлось делать парсинг строки (разбирать строку на составляющие), чтобы выяснить, какие переменные были переданы в html-запросе и какие значения они содержат.

Я специально дал форме method=GET, чтобы вы в строке URL увидели свою переменну

В нашем коде, из PHP используется всего одна функция echo. Она выводит все, что вы ей дадите: строки, цифры, значения переменных. В других языках она чаше называется print.

В PHP print тоже есть, но мне более по душе echo. Теперь вы смело можете экспериментировать. Например, добавим еще один вывод той же переменной.

<font color=#<? echo $color; ?>>Наш цвет: <? echo $color; ?></font>
<form action="" method=GET>
<input type="text" name=color>
<input type="submit" value="сменить цвет">
</form>

Теперь наша программа выводит нам наш код цвета, окрашенный в этот самый цвет. Забавно. Попробуем:

Наш цвет:

Если у на вашем сервере вместо результата использования кода вы видите сам код, значит вам надо трясти вашего админа или менять хостинг: ваш веб-сервер не поддерживает PHP.


^^^   Каталоги и БД

Мне думается, что описывать общие принципы — это не совсем то, чего вы от меня ждете. Я предлагаю несколько иное. Давайте просто возьмем, да и напишем вместе цельный web-проект! По всем правилам. Да так, чтобы был он полезен многим, да чтобы на него ложился любой дизайн, да чтобы все у нас было как у взрослых: замысел, план, блок-схема и все остальное. С использованием PHP, MySQL и особенностей Apache. Договорились? Ну вот и ладненько.

Я сегодня посоветовался с Александром Малюковым и решил принять его идею. Пишем систему персонального паблишинга. Или "колонку обозревателя", как назвал ее Саша.

Задача. Написать комплект скриптов на языке PHP(4), предоставляющие следующие возможности пользователю:

В общем, у нас должно получиться нечто, напоминающее данный сайт (http://kurepin.ru). Только созданный не в виде десятка примитивных функций, а в виде солидного пакета скриптов со всеми современными примочками: back office, mail-информер, полный контроль над ошибками, и прочей фигней, без которой крупный проект просто не сможет эффективно существовать.

Начнем мы с планирования. Но-но, попрошу без соплей в этом месте! Планирование — важнейшая часть создания большого проекта. Мы же пишем большой проект, а не фигню какую-то.

Для начала спланируем место в нашей директории, чтобы правильно сохранять то, что у нас будет рождаться. Предположим, что вы выбрали правильного провайдера, который хостит ваши страницы на одной из UNIX-систем. Например, на FreeBSD. И полный путь к вашему каталогу выглядит так:

/home/roma/ Мне почему-то показалось, что вас непременно зовут Романом.

А директория (или "папка", — как сейчас принято говорить), в которой должны лежать html-файлы сайта, называется:

/home/roma/WWW/

А что, так оно чаше всего и бывает. Еще эту папку называют html или public_html.

Теперь, нам нужна папка, где мы будем хранить все наши скрипты. Это должна быть папка НЕ в директории WWW, иначе все желающие смогу посмотреть ваши скрипты и найти в них лазейки для кряка. Нам это надо? Нет, нам это не надо. Поэтому, для необходимых нам файлов-скриптов мы создаем:

/home/roma/req/

req — это от слова require (нуждаться).

Еще нам понадобится папка, в которую мы сложим куски html для динамической сборки страниц. Назовем ее inc (от include).

/home/roma/inc/

Еще папка для хранения данных:

/home/roma/data/

И папка для хранения периодически выполняемых программ службой cron:

/home/roma/cron/

Давайте до кучи создадим в папке WWW папку pic (от picture) для хранения всякой графики.

Все. Наш каталог /home/roma/ выглядит вот так:

./req
./data
./inc
./WWW
./WWW/pic

Следующим этапом я предлагаю создать базу данных для хранения данных, простите за тавтологию.

Если вы не знакомы с языком SQL, то это вас не должно пугать. Во-первых, я постараюсь прокомментировать то, что будет написано на этом языке. А во-вторых, если вы потратите час на прочтение любого общего руководства по языку запросов к БД SQL, то сможете без каких-либо проблем читать запросы на этом языке и писать свои. Там и надо-то знать с десяток ключевых слов и несколько стандартных конструкций запросов. Все остальное — ваша фантазия (я преклоняюсь перед изобретателями этого языка. Ничего более совершенного, созданного человеком в области программирования, я в жизни не встречал). Найти документацию по стандартным SQL-возможностям можно на том же www.citforum.ru.

Базу назовем так же, как и account у провайдера — ROMA. Во-первых, чтобы не путаться, а во-вторых, обычно так провайдер и выдает доступ к SQL-серверу. Если база еще не создана, то мы ее создаем:

create database roma;

и переходим в нее:

use roma;

Нам потребуются три таблицы: таблица разделов, таблица текстов и таблица статистики. Создадим пока две. Третья подождет.

Информацию по работе с MySQL-сервером вы найдетет на сайте производителя: www.mysql.com. Или поищите FAQ-и при помощи поисковых серверов. Их полным-полно.

Таблица tbl_cats (категории, разделы):

create table tbl_cats(
c_id int not null auto_increment primary key,
c_name varchar(50) not null default ""
);

Поясняю. Тут написано, что таблица tbl_cats имеет два поля: c_id — уникальный целочисленный идентификатор записи и c_name — буквенное название раздела, собственно. Можем сразу добавить в нашу таблицу первый раздел:

insert into tbl_cats(c_name) values('web-обзоры');

Теперь вторая таблица:

create table tbl_texts(
t_id int not null auto_increment primary key,
t_name varchar(200) not null default "",
t_dt datetime not null
);

в которой написано почти тоже самое: идентификатор, название текста и дата+время публикации работы (t_dt).


^^^   ООП и наши классы

У нас уже есть спланированные директории на диске и база данных для наших будущих обзоров. Теперь пора перейти к определению функций нашей будущей системы и разделению их на тематические группы.

Сразу хочу вас обрадовать. Писать мы будем все в ООП (Объектно-Ориентированном Программировании). Бояться тут нечего. В PHP ООП развито не на самом высоком уровне, никакими страшными ООП-конструкциями я вас пугать не собираюсь. Но и без ООП нам не обойтись, если мы пишем большой серьезный проект.

Для тех, кто писал когда-нибудь на каком-нибудь языке, но не писал в ООП, я расскажу о ключевых отличиях ООП от обычного "прямого" программирования. В ООП все функции и переменные объединяются в классы (class). Классы между собой объединяются "наследованием" (extends). Наследование происходит в одну сторону: от отца к сыну. Причем, сын наследует все возможности отца. Предположим, что класс "first" имеет в своем теле две переменные и две функции, работающие с этими переменными:

class first
{
 var $a1;
 var $b2;

 function f_increment($x) 
 {
  settype($x,"integer");
  $this->$a1=$this->a1+$x;
  return(0);
 }

 function f_ab() 
 {
  $this->$a1=$this->a1+$this->$b2;
  return(0);
 }

}

Это мы объявили переменные $a1 и b2, и две функции. Первая получает в качестве параметра некое значение x, которое приводит к целому числу, и прибавляет его (число) к глобальной переменной $a1, возвращает "0" в качестве кода ошибки (0 — нет ошибок). Вторая функция просто плюсует наши две объявленные переменные и результат записывает в переменную $a. Тоже возвращает код ошибки. Привыкайте всегда возвращать код ошибки выполнения любой функции!

Напомню, что я стараюсь писать понятно, а не эффективно. Эффективность пусть каждый для себя ищет сам. Это замечание для снобов, которые могут сейчас сказать, что мой демонстрационный код можно было написать намного короче. Да это так. Но кто меня поймет, если я напишу все тело первой функции вот так:
$this->$a+=(int)$x;
Можно так написать. Но это не очень понятно для тех, кто слабо знаком с синтаксисом PHP или C. Давайте уважать психику других.

Обратите внимание на конструкцию $this->. Таким образом внутри класса мы обращаемся к любому объявленному объекту класса. Будь то переменная или функция.

ООП тем и приятно, что нам в конструкциях вызова процедур или переменных не надо ничего менять. Функция отличается от переменной во время вызова только тем, что содержит у себя "на хвосте" дополнительные параметры или пару пустых скобок, если параметров нет.

$this->labuda — это переменная.
$this->labuda() — это функция.

echo "Результаты работы: ".$this->labuda()." и ".$this->labuda; — вывод обоих значений: значение переменной будет выведено как есть, а функция отработает и выдаст результат своей работы не хуже переменной. Точка "." в PHP используется для слияния двух строк:
"123"."abc" = "123abc"

Обращаю внимание: переменная и функция вполне могут иметь одинаковые имена, интерпретатор PHP сам разберет "кто есть who", если вы не будете забывать про скобки на хвосте у функций. В больших проектах я этим часто пользуюсь, чтобы не путаться.

Возвращаясь к наследованию. Мы создаем класс "second", от класса "first":

class second extends first
{
...
}

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

Таким образом, в ООП создают классы и наследуют их, двигаясь от общего к частному. Получается четкая древовидная структура классов, которую мы и создадим для нашего проекта. В PHP не реализовано множественное наследование. Т.е. каждый новый класс может рождаться только от одного родителя. Нам этого будет вполне достаточно. Это пока основное, что вам надо знать про отличия ООП от обычного.

Вернемся к планированию. Скажу вам по секрету, что это почти все мои веб-проекты делятся на следующие тематические группы скриптов (я эти группы буду сразу называть классами, пользуясь терминологией ООП. А каждый класс мы будем хранить в отдельном файле с окончанием ".class"):

1. Класс переменных описывающих свойства проекта (class_var). Этот класс я обычно делаю изначальным, в котором описываю глобальные переменные, влияющие на работу всего проекта. Такие, как: пути на диске, время кеширования страниц, стандартные названия, форматы отображения времени и даты, и так далее.

2. Класс class_mysql наследуется от class_var. По названию класса не сложно догадаться, что он посвящен работе с базой данных MySQL. Это будет небольшой класс.

3. Класс class_util содержит все глобальные утилиты, которые могут нам понадобиться на любом из этапов выполнения нашей задачи. Кроме того, в этом классе мы опишем важнейшую часть проекта: обработку ошибок.

Эти три класса можно было бы объединить в один, т.к. все остальные классы будут наследоваться от class_util, но я всегда держу их именно в разных файлах. Так проще вносить изменения в нужное место.

Давайте думать далее. У нас есть три основные группы скриптов. Группа, отвечающая за добавление данных в базу сайта. Группа, отвечающая за вывод данных из базы. И группа, отвечающая за выполнение периодических (cron) задач. Вот мы и определим три основных класса:

class_in
class_out
class_cron

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


^^^   vars.class

С чего начать писать? Начинать надо с самого начала. Что мы там, в планировании писали? Если мне не изменяет память, то первым у нас шел класс, описывающий основные переменные и параметры нашей системы. Вот с этого класса и начнем. Тем более, что в нем не будет сложных языковых конструкций, попривыкните к синтаксису PHP.

Класс: class_vars.
Файл: vars.class
Место расположения: /home/roma/req/

Итак, создаем новый файл с указанным названием, открываем начало PHP-кода (<?) и начинаем писать.

Шапка:

<?
class class_vars
{

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

В PHP комментарий начинается с двойного слеша "/". А в именах переменных учитывается регистр (высота букв), будьте осторожны. Сразу выработайте для себя схему использования... строчные, прописные:никогда не знал, что как называется... БОЛЬШИХ и обычных букв. Лично я предпочитаю глобальные писать БОЛЬШИМИ буквами, а все остальные переменные - обычными.

Пишем:

// Пути к папкам.
var $PATH="/home/roma"; // основной путь к проекту
var $PATH_INC="/home/roma/inc"; 
var $PATH_REQ="/home/roma/req";
var $PATH_DATA="/home/roma/data";
var $PATH_WWW="/home/roma/WWW";
var $PATH_WWW_PIC="/home/roma/WWW/pic";

Понятно, что я сделал? Я определил путь для каждой папки. И этими переменными мы будем пользоваться в скриптах, вместо того, чтобы писать каждый раз реальный путь. Если вам вдруг понадобится перенести какие-то данные в другое место (а в больших проектах подобное случается), то вам надо будет просто поменять вот эти самые пути. А не изучать все скрипты, в поисках обращений к нужным директориям. А если ваш друг Дима захочет запустить у себя копию вашего сайта, то ему будет достаточно скопировать ваши данные и отредактировать класс vars. Это может сделать даже человек, не пишущий на PHP.

Теперь я предлагаю описать виртуальные пути к вашему сайту. Т.е. URL-ы. А заодно опишем сразу полное название нашего проекта, короткое его название и некоторые другие понятные параметры.

Открываем наш vars.class и пишем дальше:

// Основной URL
var $PATH_HTTP="http://roma.21.ru/";

// Полное название и короткое
var $NAME_FULL="Персональная страница Ромы-обозревателя";
var $NAME_SHORT=''Рома-обозреватель";

// адрес хозяина страницы
var $EMAIL_ADMIN=array("roma@21.ru");

// техническая служба сайта
var $EMAIL_NOC=array("roma@21.ru","noc@21.ru");

// Время кеширования страниц "Expires" (в секундах)
 var $CACHE_TIME=300; 

// Максимальный размер подгружаемого в базу текста
 var $TEXT_SIZE_MAX= 1048576; // это мегабайт

// Минимальный размер подгружаемого в базу текста
 var $TEXT_SIZE_MIN=100; // сто байт

// Формат вывода времени (из SQL-базы)
 var $TIME_FORMAT="%H:%i:%S"; // ЧЧ:ММ:СС

// Формат вывода даты (из SQL-базы)
 var $DATE_FORMAT="%d.%m.%Y"; // ДД.ММ.ГГГГ

Ну вот, хватит нам пока переменных. В процессе дальнейшей работы мы будем обращаться к этому классу, чтобы пополнить его новыми и новыми данными.

Сохраняем файл, не забыв предварительно закрыть класс и закрыть главный PHP-тэг (?>):

}
?>
Я прошу вас, пользуйтесь нормальными текстовыми редакторами для работы с PHP. Редакторами, в которых понятно, где заканчивается строка и где заканчивается весь текст. Если вы не хотите ошибок "непонятного происхождения", -- проверьте: после закрывающего PHP-тэга не должно быть никаких символов, включая символ перевода строки. Закрывающая треугольная скобка (>) должна быть последним символом файла.

Вот, мы и написали с вами первый, базовый класс нашего проекта. Правда, это не сложно? На мой взгляд -- проще не бывает. Теперь от этого класса мы будем растить древо нашего проекта. Следующим классом у нас будет класс, обслуживающий доступ к базе MySQL.

Теперь перечитайте вашу программу, которая сложилась у вас в файле vars.class и найдите там слова и фрагменты, которые вам не понятны. Я отвечу на все ваши вопросы в форуме.

На данный момент файл vars.class выглядит вот так:

<?
class class_vars
{
 // Пути к папкам.
 var $PATH="/home/roma"; // основной путь к проекту
 var $PATH_INC="/home/roma/inc"; 
 var $PATH_REQ="/home/roma/req";
 var $PATH_DATA="/home/roma/data";
 var $PATH_WWW="/home/roma/WWW";
 var $PATH_WWW_PIC="/home/roma/WWW/pic";

 // Основной URL
 var $PATH_HTTP="http://roma.21.ru/";

 // Полное название и короткое
 var $NAME_FULL="Персональная страница Ромы-обозревателя";
 var $NAME_SHORT=''Рома-обозреватель";

 // адрес хозяина страницы
 var $EMAIL_ADMIN=array("roma@21.ru");

 // техническая служба сайта
 var $EMAIL_NOC=array("roma@21.ru","noc@21.ru");

 // Время кеширования страниц "Expires" (в секундах)
 var $CACHE_TIME=300; 

 // Максимальный размер подгружаемого в базу текста
 var $TEXT_SIZE_MAX= 1048576; // это мегабайт

 // Минимальный размер подгружаемого в базу текста
 var $TEXT_SIZE_MIN=100; // сто байт

 // Формат вывода времени (из SQL-базы)
 var $TIME_FORMAT="%H:%i:%S"; // ЧЧ:ММ:СС

 // Формат вывода даты (из SQL-базы)
 var $DATE_FORMAT="%d.%m.%Y"; // ДД.ММ.ГГГГ
}
?>

^^^   mysql.class

Будем изучать самый бестолковый класс нашего проекта - класс, обслуживающий контакты с базой данных.

На самом деле, все три первых наших класса (vars, mysql, utils) можно один раз внимательно и с расстановкой написать, а потом просто пользоваться ими во всех остальных своих проектах. Но если в переменных окружения могут быть какие-то перемены, как и в обслуживающих утилитах, то в классе mysql нам вряд ли что-то когда-то придется менять, разве что реквизиты доступа к базе (логин, пароль...). Вы можете вообще не напрягаться и особенно не разбираться в том, что я сейчас напишу. То есть, понять все это, безусловно, вам придется, но сразу после прочтения вы имеете полное право забыть все это как страшный сон - не так важно.

Итак. Давайте разбираться, какие основные функции нам нужны при работе с базой MySQL?

Во-первых, нам нужна процедура подключения к базе. Ее еще можно назвать процедурой авторизации или активации соединения. Или, как говорят программисты, "открытие соединения с базой".

Во-вторых, нам нужна возможность оформить запрос в базу и механизм исполнения этого запроса.

В-последних, за собой надо прибирать. Закрытие соединения.

Все, по большому счету.

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

Итак, давайте перейдем к делу. Я бы не стал повторять, как оформлять новый класс, но сегодняшний класс имеет одно малозаметное, но очень важное отличие от предыдущего (vars): класс class_mysql рождается от класса class_vars. То есть class_vars, являясь родительским к классу class_mysql, передает ему все свои возможности. В нашем случае, это значения тех переменных, которые мы описали на прошлом занятии.

Открываем (создаем) новый файл class_mysql и пишем в нем:

<?
 require("/home/roma/req/vars.class");
 class class_mysql extends class_vars

Видите, прежде всего, мы должны указать, что для нам для работы необходим файл "/home/roma/req/vars.class", т.к. в нем содержится фрагмент нашей программы, описывающий родительский class_vars.

Затем, мы объявляем новый класс. Объявляем его точно так же, как и class_vars, только дописываем в конце строки ссылку на родительский класс: extends class_vars.

Помните, я рассказывал вам, как наследуются классы в PHP? Вот это тот самый случай. Теперь перейдем к телу класса, и сначала добавим нужные нам переменные.

  var $sql_login="roma";
  var $sql_passwd="parol";
  var $sql_database="roma";
  var $sql_host="127.0.0.1";

  var $conn_id;
  var $sql_query;
  var $sql_err;
  var $sql_res;

В первых четырех переменных мы определили реквизиты доступа к базе MySQL: регистрационное имя (логин), пароль, название нашей базы и IP-адрес, по которому физически располагается MySQL.

Эти четыре переменные мы вполне можем перенести в класс vars, но я этого предпочитаю не делать. Почему? Ну, бывают такие случаи, когда мы доверяем управление нашим сайтом кому-то еще. При этом, мы можем дать этому человеку отдельный доступ именно к файлу с переменными, чтобы он мог влиять на манеру поведения и внешний вид нашего сайта. Но не всегда при этом мы даем ему доступ к самим скриптам и не сообщаем наших реквизитов для доступа к БД.

Вторые четыре переменные мы будем активно эксплуатировать в нашей работе.

Первая из них ($conn_id)- идентификатор соединения. Он несет в себе информацию о полученном соединении к базе.

Вторая (sql_query) - строка запроса. В нее мы будем помешать наш запрос к БД, прежде чем его активировать.

Третья (sql_err) - код ошибки. Сервер MySQL, как и любая друга программа, может давать различные сбои как по своей вине, так и по нашей: когда наш запрос написан с ошибками. Вот эта переменная и будет работать у нас "флагом", который будет "подниматься", если произошла какая-то ошибка.

А четвертая переменная (sql_res) будет содержать в себе полученную информацию из базы данных после выполнения запроса. Из этой переменной мы будем извлекать эту информацию удобным нам способом.

Других переменных нам тут пока не понадобится, переходим к функциям.

Функция подключения к базе данных.

 function sql_connect()
 {
  $this->conn_id=mysql_connect($this->sql_host,$this->sql_login,$this->sql_passwd);
  mysql_select_db($this->sql_database);
 }

Поясню построчно:

Понятно, что мы сделали? Мы подключились к серверу MySQL, используя свои реквизиты, после чего, выбрали интересующую нас базу данных. В $conn_id останется идентификатор нашего соединения, он нам пригодится в следующих двух функциях.

Функция, выполняющая наш запрос к БД:

 function sql_execute()
 {
  $this->sql_res=mysql_query($this->sql_query,$this->conn_id);
  $this->sql_err=mysql_error();
 }

Первая строка (в теле функции) выполняет отправку запроса (функция PHP mysql_query), а результат помещает в объявленную нами переменную $sql_res.

Вторая строка запрашивает код ошибки выполнения SQL-запроса (функция PHP mysql_error) и записывает ее в другую переменную, которую мы с вами тоже определили заранее — $sql_err.

Вот такая простая функция. Как видите, PHP очень много делает за нас. На языке C это выглядело бы настолько страшно, что даже показывать это не стану, чтобы не отбить у вас вкус к изучению программирования.

Нам осталось реализовать последнюю, на текущий момент, функцию класса class_mysql. Нам осталось зарыть наше соединение с сервером MySQL.

Закрываем.

 function sql_close()
 {
  mysql_close($this->conn_id);
 }

Функция языка PHP mysql_close получает в качестве параметра идентификатор открытого соединения и закрывает это самое соединение.

Закрывайте оставшиеся тэги и сохраняете файл. Он у вас должен выглядеть вот так:

<?
 require("/home/roma/req/vars.class");

 class class_mysql extends class_vars
 {
  var $sql_login="roma";
  var $sql_passwd="parol";
  var $sql_database="roma";
  var $sql_host="127.0.0.1";

  var $conn_id;
  var $sql_query;
  var $sql_err;
  var $sql_res;

  function sql_connect()
  {
   $this->conn_id=mysql_connect($this->sql_host,$this->sql_login,$this->sql_passwd);
   $this->conn_log_id=mysql_connect($this->sql_host,$this->sql_login,$this->sql_passwd);
   mysql_select_db($this->sql_database);
  }

  function sql_close()
  {
   mysql_close($this->conn_id);
  }

  function sql_execute()
  {
   $this->sql_res=mysql_query($this->sql_query,$this->conn_id);
   $this->sql_err=mysql_error();
  }

}
?>

За исключением, разумеется, реквизитов подключения к _вашей_ базе данных MySQL.


^^^   Язык SQL

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

Это необходимо по двум причинам. Во-первых, не хочу, чтобы вы смотрели на SQL-фрагменты последующих скриптов как парнокопытные животные на только что построенное заградительное сооружение. А во-вторых, мне хочется донести до вас красоту этого языка, скрытую в его простоте и "человечности".

Что такое база данных? Это хранилище информации. Структурированное хранилище. В базе данных можно хранить любой тип информации: от цифр до видеороликов.

А все запросы к базе данных можно разделить на три основные группы:

Как создаются таблицы мы уже видели. На этом этапе я останавливаться не буду, ибо тут ничего интересного нет. Как и стандартного. В разных БД создание таблиц, функций и индексации может производиться по-разному. Это не так важно. Мы остановимся на двух других группах. Добавление, изменение, удаление информации производится почти человеческим языком. Поэтому все команды SQL так легко запомнить.

Предположим, у нас есть таблица books (книги), в которой мы храним информацию о своей домашней библиотеке. Для этого мы создали таблицу books в следующем виде:

create table books(
book_id    bigint not null auto_increment primary key,
book_name  varchar(100) not null default "",
book_date  datetime not null,
book_comm  varchar(200) not null default ""
);

где book_id -- порядковый номер книги (создает автоматически, при добавлении новой записи в таблицу, путем прибавления единицы к внутреннему счетчику), book_name -- автор и название книги, book_date -- дата (и время) приобретения книги. А book_comm -- свободный комментарий до двухсот символов. Например, мы можем в это поле записывать имя человека, взявшего у нас книгу, или интересную подробность, связанную с ее приобретением. Давайте перейдем непосредственно к добавлению информации.

По-хорошему, надо было создать как минимум две таблицы: для книг и для авторов. И связать их внешним ключом для целостности. Но сейчас наша задача состоит в ином, поэтому простите мне мой топорный подход.

Нам надо добавить в нашу электронную библиотеку книгу "Алексей Толстой. 'Золотой ключик или приключения Буратино'". Как бы мы это сделали, если бы база данных управлялась человеческим голосом? Наверное, что-то вроде: "добавить в таблицу КНИГИ название и дату покупки: А. Толстой. 'Золотой ключик...', 27-е августа 2001 года". Вот мы так и запишем, а SQL с удовольствием нас поймет. Только по-английски.

insert into books(book_name, book_date) values("А.Толстой. 'Золотой ключик или приключения Буратино'","2001-08-27 00:00:00");

Вот так. Почти по-человечески. Следующей книгой для контраста добавим "А.Платов. 'Котлован'", неизвестно когда подаренную нам любимым преподавателем.

insert into books (book_name, book_comm) values("А.Платов. 'Котлован'", "Подарена Пустоваловым Петром Семеновичем");

Ну и еще одну...

insert into books(book_name) values('Поваренная книга');

Вот так. Теперь у нас в базе есть аж три книги. Попробуем в этом убедиться? Как мы попросим нашу базу выбрать нам все, что в ней есть, без каких-либо условий? По-русски это бы могло звучать так: "Выбери все из таблицы книг". Так и напишем (звездочкой в SQL обозначается любой набор параметров, символов и прочего, как почти во всем компьютерном):

select * from books;

И получаем список наших книг. Я не буду приводить тут полностью ответ MySQL, т.к. слишком широкая табличка получилась. Мы сейчас это подправим. Попросим выбрать только названия книг:

select book_name from books;

+------------------------------------------------------+
| book_name                                            |
+------------------------------------------------------+
| А.Толстой. 'Золотой ключик или приключения Буратино' |
| А.Платов. 'Котлован'                                 |
| Поваренная книга                                     |
+------------------------------------------------------+
3 rows in set (0.00 sec)

Понятно, не так ли? А теперь присовокупим их порядковые номера:

select book_id, book_name from books;

+---------+------------------------------------------------------+
| book_id | book_name                                            |
+---------+------------------------------------------------------+
|       1 | А.Толстой. 'Золотой ключик или приключения Буратино' |
|       2 | А.Платов. 'Котлован'                                 |
|       3 | Поваренная книга                                     |
+---------+------------------------------------------------------+
3 rows in set (0.01 sec)

Вот так просто. Теперь давайте попросим нашу понятливую БД выдать нам книгу под номером 2:

select book_id, book_name from books where book_id=2;

+---------+----------------------+
| book_id | book_name            |
+---------+----------------------+
|       2 | А.Платов. 'Котлован' |
+---------+----------------------+
1 row in set (0.01 sec)

Ты смотри, нашел! А если мы хотим все книги от номера 2 и больше?

select book_id, book_name from books where book_id>1;

+---------+----------------------+
| book_id | book_name            |
+---------+----------------------+
|       2 | А.Платов. 'Котлован' |
|       3 | Поваренная книга     |
+---------+----------------------+
2 rows in set (0.00 sec)

Ну хорошо, давайте зададим в запросе что-нибудь похитрее. Например, нам нужна книга, которую мы купили на днях, но не помним про нее буквально ничего. Для этого так и попросим:

select book_id, book_name from books where book_date between subdate(now(), interval 3 day) and now();

Поясню.
Зарезервированное слово between используется тут дословно - "между".
Функция now() дает текущую дату и время.
Функция subdate уменьшает указанную дату на указанный период времени.

Т.е. по-русски наш запрос звучал: "выбрать номер книги и название из базы книги, где дата книги находится между датой трехдневной давности и текущей датой". И что же нам выдала база?

select book_id, book_name from books where book_date between subdate(now(), interval 3 day) and now();

+---------+------------------------------------------------------+
| book_id | book_name                                            |
+---------+------------------------------------------------------+
|       1 | А.Толстой. 'Золотой ключик или приключения Буратино' |
+---------+------------------------------------------------------+
1 row in set (0.01 sec)

Как в сказке! Еще один примерчик хочу продемонстрировать. Попробуйте понять его сами, без подсказок.

select book_name from books where book_name like "%Толстой%";

+------------------------------------------------------+
| book_name                                            |
+------------------------------------------------------+
| А.Толстой. 'Золотой ключик или приключения Буратино' |
+------------------------------------------------------+
1 row in set (0.01 sec)

^^^   Язык SQL и class_utils

Давайте разберем еще один мощный select, затем посмотрим методы удаления и на этом закончим наше отступление в сторону MySQL.

select date_format(book_date,'%d.%m.%Y') as date, book_name from books
 where book_name like "А.__%" && book_id<100 && book_date<"2002-01-01 00:00:00"
 order by book_date desc limit 0,2;

+------------+------------------------------------------------------+
| date       | book_name                                            |
+------------+------------------------------------------------------+
| 28.08.2001 | А.Толстой. 'Золотой ключик или приключения Буратино' |
| 00.00.0000 | А.Платов. 'Котлован'                                 |
+------------+------------------------------------------------------+
2 rows in set (0.00 sec)

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

Выбрать даты в формате "дд.мм.гггг" и называния книг из базы данных "книги", в тех записях, которые удовлетворяют условиям:
1. Название книги начинается на "А.", имеет еще минимум два символа после этого "__" и продолжается как угодно до конца"%".
2. Порядковый номер в базе не превышает цифры 100.
3. Дата меньше, чем 2002-й год.

Полученные данные отсортировать по дате, в обратном порядке: от большего к меньшему. Из полученных данных взять только две строки, начиная с первой же позиции (0 — первая запись). Вот так расшифровывается наш запрос.

Интереснее поиграть с запросами сразу из нескольких таблиц, конечно. Будем надеяться, что наша задача предоставит нам такую возможность. Посмотрим. Все же по SQL написано много умных книг, и я пока не планировал писать учебника по нему. Эту главу и предыдущую я посвятил сему языку, чтобы вы могли нормально читать те несложные запросы, которые мы будем составлять по мере создания нашего проекта.

Теперь об удалении из базы данных.

Удалять из базы записи так же просто, как и выбирать их select-ом. Все то же самое, только вместо слова select используется слово delete. При этом не указываются названия полей (т.к. удаление производится по целым записям), и не используются сортировки и группировки.

Например:

delete from books where b_id>2 && book_name like "___"

"Удалить из таблицы books записи, id которых больше 2 и имя книги состоит из трех любых символов". Надеюсь, других примеров удаление приводить не надо, и так должно быть все понятно. Если возникли попутные вопросы — Добро пожаловать на форум.

Возвращаемся к нашей программе. Если я ничего не напутал, то у нас на очереди наименее нудный класс, который мы назвали utils. Почему наименее нудный? А потому, что состоять он у нас будет из большого количества процедур, имеющих самое разное предназначение: обработка ошибок, система оповещения, контроль кеширования страниц, преобразование и приведение форматов дат и времени, и так далее. В течение всего нашего "учебного курса" мы будем возвращаться к этому классу, чтобы добавить в его тело очередную нужную нам функцию.

А для начала я предлагаю определить три функции работы с ошибками: получение ошибки по номеру, получение html-сообщения об ошибке для пользователя и отправка сообщения об ошибке модератору сайта. Думаю, вы уже сами догадались, что надо создать файл utils.class и породить в нем новый класс class_utils от класса class_mysql. Эту процедуру мы неоднократно повторяли, не будем задерживаться, перейдем к "ошибкам".

Позвольте дать вам один совет. Многие начинающие программисты, пишущие на C, perl или php часто совершают одну небольшую ошибку, приводящую порою к страшной нервотрепке. Это проблема открывающих и закрывающих элементов конструкций. Чаще всего это выражается в том, что открыв очередную конструкцию той же фигурной скобкой, они не закрывают ее сразу, а откладывают это дело на потом. Но в процессе написания функции накапливается столько открывающих и закрывающих скобок, что не сложно запутаться самому и запутать интерпретатор. Последний может запутаться так, что укажет вам место ошибки очень далеко от ее настоящего места, совсем в другом месте вашего листинга. Старайтесь всегда ставить скобки, теги, кавычки и прочих "парных тварей" сразу парами. Поставил пару кавычек, а уж потом вписывай между ними свой текст. Это сэкономит вам кучу времени, уверяю.

Мы еще не знаем, какие ошибки у нас будут возникать в процессе работы наших скриптов. Поэтому создадим саму процедуру с несколькими ошибками, а в процессе работы над проектом будем добавлять в нее новые данные.

Для наглядности будем использовать в качестве базы хранения ошибок обычный массив переменных. Для больших проектов это не самый разумный способ, но он очень нагляден и понятен. А место хранения описания ошибок мы всегда можем изменить, если того потребуют обстоятельства.

В общем, функция возврата текста ошибки по ее номеру феноменально примитивна. Ее можно разделить на две составляющие: описание базы ошибок и собственно возврат описания ошибки по ее номеру. Смотрим.

function err_to_str($num)
{
 // Фатальные ошибки
 $err[1]="Ошибка управления или попытка взлома системы. Администратору отправлено сообщение!";
 $err[2]="Ошибка авторизации.";
 
 // Ошибки работы с SQL
 $err[11]="Ошибка выполнения SQL-запроса";
  
 // Ошибки для web-серфера
$err[101]="Ошибка выполнения SQL-запроса";


 // Отправка ошибки администратору
 if($this->DEBUG_LEVEL>=$num) $this->mail_to_noc($err[$num]);

 // Возврат текста ошибки
 return($err[$num]);
}

Почти все понятно без объяснений.

Мы передаем номер ошибки нашей функции, которая хранит этот номер в переменной $num.

$err[2]="Ошибка авторизации."; — это мы занесли троку "Ошибка авторизации" в ячейку массива под номером 2. Т.е. 2 — это у нас уже зафиксированный номер ошибки, которые будет возбуждаться при нарушении правил авторизации: при неверной попытке доступа в защищенную часть сайта.

return($err[$num]); — собственно, возврат текста ошибки. То, ради чего создана эта функция.

И только одна строка требует более детальных пояснений:

if($this->DEBUG_LEVEL>=$num) $this->mail_to_noc($err[$num]);

Дело в том, что есть ошибки, о появлении которых необходимо знать администратору айта: ошибки базы данных, попытки взлома, попытки подбора паролей и тому подобное. А есть такие, которые будут возникать в больших количествах для посетителей страниц, но не являющиеся при этом нарушениями работы системы: "вы забыли ввести ваше имя", "по этому материалу нет комментариев", "по вашему запросу ничего не найдено" и прочее. Вот, чтобы администратор получал только те ошибки, которые ему интересны, мы вводим в наш первый класс class_vars еще одну переменную, определяющую текущий уровень отладки скриптов (добавьте в файл vars.class):

// уровень отладки
 var $DEBUG_LEVEL=100;

Вы обратили внимание, что я разделил все наши ошибки на несколько групп? И не просто так, а с некоторой особенностью. Чем серьезнее и фатальнее ошибка, тем ниже ее порядковый номер. "Страшные" ошибки нумеруются 1, 2, 3..., ошибки попроще (вроде MySQL) нумеруются с номера 11. А совсем неинтересные администратору сообщения — начинают нумероваться с числа 101.

Это сделано для того, чтобы удобно нам было выставлять уровень отладки. Смотрим еще раз на строку:

if($this->DEBUG_LEVEL>=$num) $this->mail_to_noc($err[$num]);

Из нее не сложно понять, что мы вызываем некую функцию mail_to_noc() (она отсылает администратору сообщение об ошибке по почте), но только в том случае, если номер активной ошибки меньше или равен той, что мы указали в нашей переменной $DEBUG_LEVEL.

Сейчас мы установили DEBUG_LEVEL в состояние 100. Это значит, что администратор получит сообщение об ошибке по почте только в том случае, когда эта ошибка не имеет отношения к сообщениям для web-серфера.

При отладке наших скриптов мы можем поднять уровень отладки до любой большой цифры, чтобы получать полную информацию о том, что творится на сайте, но после отладки этот уровень вполне можно уменьшить обратно до 100 и спасть спокойно.

Очень подробно я разжевал эту простейшую функцию только для того, чтобы вы поняли важность "ошибки" для программы. Если мы построим грамотную систему самоанализа программы, то никогда не будем иметь проблем со взломами, добавлением ошибочных данных и другими неприятностями, которые не только могут подпортить нам нервы, но и подпортить результат многолетнего труда. Кстати, по уровню отношения к ошибкам и самоанализу системы можно судить о серьезности программиста. Научиться кодировать для web — совсем не сложно. Гораздо сложнее сделать это так, чтобы система могла за себя постоять, чтобы пользователь знал — что он сделал не так, чтобы администратор сайта знал, что на нем творится, чтобы другие программисты могли после тебя разобраться в том, что ты написал. Хотя последнее — не обязательно. У каждого программиста своя манера, как у художника. Поэтому, не всегда удается дописать талантливый фрагмент к чужому творению. Художникам это и не надо, а вот программистам — наоборот.

Нам осталось разобрать еще две функции.

Совсем коротенькая функция получения html-версии ошибки:

function err_to_html($num) {
$text="<b>Error:</b> <font color=#FF0000>".$this->err_to_str($num)."</font>
<p>Если у вас есть время, пожалуйста, пришлите по адресу
 <a href=mailto:noc@21.ru>noc@21.ru</a> подробное описание ситуации, при 
которой возникла данная ошибка. Будем вам за это очень признателены!
<br><br><div align=right>Рома</div><br><br><br>";

return $text;
}

Тут даже пояснять нечего. Мы вызываем написанную нами err_to_str с номером ошибки, чтобы получить ее текст. А затем оформляем все это html-тэгами и просьбой прислать описание ситуации, при которой произошла данная ошибка. Все просто, даже языком тут почесать не обо что.

Лучше освоим новую для нас функцию обращения с почтовыми сообщениями.

В PHP известно два способа отправлять почтовые сообщения: при помощи стандартной почтовой службы сервера и отправка сообщения без посторонней помощи через socket. Мы остановимся на первом варианте. В 99.9% случаев его оказывается достаточно. Кроме того, у меня просто не хватит преподавательской квалификации, чтобы разжевать вам второй способ.

Давайте напишем сразу две функции. Первая будет у нас заниматься отправкой почты как таковой, а вторая будет вызывать первую, чтобы отправить именно письмо с ошибкой.

Первую функцию назовем mailer. Вот так она выглядит.

function mailer($from, $to, $subj, $body) {
 $from="From: $from\nReply-To: $from\nX-Priority: 1\nContent-Type: text/plain;
 charset=\"koi8-r\"\nContent-Transfer-Encoding: 8bit";
 $from=convert_cyr_string($from,"w","k");
 $to=convert_cyr_string($to,"w","k");
 $subj=convert_cyr_string($subj,"w","k");
 $body=convert_cyr_string($body,"w","k");
 mail($to,$subj,$body,$from);
}

В качестве аргументов этой функции мы передаем параметры:
$from — от кого
$to — кому
$subj — тема сообщения
$body — само сообщение (тело сообщения — так правильно это называется)

Далее мы производим магические пассы над нашими аргументами. Я не буду объяснять, что означают эти странные добавления, вроде "nContent-Type: text/plain; charset=\"koi8-r\"\nContent-Transfer-Encoding: 8bit", вам это знать не так уж обязательно. Скажу только, что это служебная информация для вашего outlook или TheBat, уж не знаю, чем вы там пользуетесь для чтения почты. Если этой информации в заголовке письма не будет, то письмо вы увидите в жутком формате, русский язык в виде всяких "зюков" и так далее.

А функция convert_cyr_string() просто преобразует текст из формата win-1251 в формат KOI-8, который является стандартом для почтовых сообщений в русскоязычном Интернете (к сожалению, далеко не все это знают).

Опустив все эти "пассы", мы подходим к функции PHP mail(), которая и отправит наше письмо при помощи почтовой службы сервера. Ей мы и предаем подготовленные нами параметры: отправителя, получателя, тему и сообщение.

Ну вот. А теперь можно и послать администратору сообщение об ошибке.

// Отправка сообщения об ошибке
function mail_to_noc($message) {
 global $REQUEST_URI;
 for($i=0;$iEMAIL_NOC);$i++) {
 $this->mailer("Robot", $this->EMAIL_NOC[$i], "Fatal
 error!", "Error: $message\nDateTime:".$this->today_date()." ".$this->today_time()."\n"."Remote
 IP:".$this->remote_ip()."\n\nURI: $REQUEST_URI\n\n$SQL_QUERY:
 ".$this->sql_query."\nSQL_ERROR:".$this->sql_err);
 usleep(100000);
 }
}

^^^   class_utils (продолжение)

В качестве аргумента мы передаем функции mail_to_noc сообщение об ошибке, которое необходимо отправить в службу поддержки сайта. Адреса службы поддержки мы описали среди наших переменных, помните?

// техническая служба сайта
var $EMAIL_NOC=array("roma@21.ru","noc@21.ru");

В качестве вспомогательной информации мы возьмем некоторые данные, которые сопровождают прочти любую ошибку. Эти данные помогут нам (службе поддержки сайта) сориентироваться в ситуации и принять правильное решение. Какая же информация нам не помешает? Я думаю, что нам пригодится знать URL, по которому произошла ошибка. Вдруг какой-то кретин нагородил в строке вызова страницы всякого бреда, что и привело к ошибке (обратите внимание, если пришла ошибка, значит сработала защита от этого самого бреда).

В PHP есть масса зарезервированных переменных, к которым можно обратиться из любого места наших скриптов. Просмотреть полный список, так называемых, переменных окружения PHP можно очень просто. Достаточно написать в файле (назовем его phpinfo.php):

<?
 echo phpinfo();
?>

И обратиться вызвать в браузере эту страницу.

Зарезервированная функция phpinfo() покажет нам в красивой табличке все переменные окружения и особенности настройки конкретной версии PHP, другую полезную информацию.

Посмотрите сами: http://kurepin.ru/phpinfo.php

Среди них есть и та, о которой мы только что говорили — REQUEST_URI. Это переменная, в которой храниться URL запрашиваемой страницы. В нашем примере вы видите ее значение — "/phpinfo.php" — что мы и запрашивали.

Так вот, возвращаясь к нашей функции, в первой строке ее вы видите как можно заполучить эту самую информацию.

global $REQUEST_URI (обратите внимание, последняя буква "I", а не "L").

Тут надо заметить, что в обычном скрипте в мы могли бы обраться к данной переменной напрямую, но в PHP принято, что глобальная еременная (распространяемая на весь скрипт) не известна в отдельной функции, пока ее к этому не принудят. Принуждение (объявление/вызов) производится при помощи конструкции global имя глобальной переменной. После этого к ней можно обращаться точно так же, как и к другим переменным, определенным в данной функции. Запомните, что в разных функциях и в глобальном скрипте вы можете пользоваться одинаковыми именами для переменных: и каждый раз это будут разные переменные.

Теперь, каждое сообщение об ошибке мы будем сопровождать строкой URL. Это полезная информация, уверяю вас. Особенно в больших проектах (надеюсь, вы еще не забыли, что мы пишем БОЛЬШОЙ проект), когда один вид ошибки может возникнуть на любой из тысяч страниц. Мне почему-то кажется, что кроме места, нам понадобится еще и время происхождения ошибки. В письме всегда указывается время отправления письма. Но мы не будем полагаться на совесть почтового сервера и сами проставим время с точностью до секунды. Вообще, дата и время — полезная штука. Давайте сразу напишем в коллекцию наших утилит две коротенькие функции — приведения даты и времени к приличному виду.

function today_date()
{
 $ret=date("d.m.Yг.",time());
 return($ret);
}
и
function today_time()
{
 $ret=date("G:i:s",time());
 return($ret);
}

Очень просто. Мы запрашиваем у PHP дату и время в том виде, что предоставляет нам сервер (число секунд, прошедших с 1970-го года) и преобразуем их в удобоваримый формат: "дд.мм.гггг" и "чч:мм:сс".

Теперь мы можем в любом месте наших скриптов вставить текущую дату и текущее время.

Что нам понадобится еще? У нас уже есть место "преступления", его дата и время, имеется текстовое его определение. Чего не хватает? Любой следователь вам без запинки ответит, что нам необходимо узнать самого нарушителя спокойствия. Правильно. Это единственное, что нам недостает.

Мы уже позаботились об этом, помните? Мы вставили в html-сообщение об ошибке просьбу описать ситуацию, при которой возникла эта самая ошибка. Хорошо. Но не каждый посетитель захочет нам отписать такое письмо, это во-первых. А во-вторых, уж точно не станет писать нам письмо, если он сознательно пытается нанести урон нашему сайту, или просто прощупывает сайт на предмет "дыр". Поэтому мы, без специального на то разрешения, обнаружим IP-адрес посетителя, нарушившего спокойствие нервного админа, и приложим его к письму (адрес, конечно, а не админа).

Получить IP посетителя так же просто, как и URL: адрес содержится в переменной окружения $REMOTE_ADDR.

Уж простите мне мое занудство, но я предпочитаю определить это в отдельную функцию.

function remote_ip()
{
 global $REMOTE_ADDR;
 return($REMOTE_ADDR);
}

Почему я поступаю так "неразумно"? Дело в том, что запрашивать эту информацию можно из разных мест скрипта. А что, если потребуется присовокупить к адресу еще какую-то информацию? Или произвести его регистрацию в логах? А есть добавить проверку на наличие прокси-сервера и попытаться получить реальный IP-адрес? В общем, может произойти все, что угодно, и необходимо забронировать себе удобное расширение возможностей в любом направлении. Если вы не догадались, то дату и время я заключил в отдельные функции по той же причине: нам не составит труда заменить разделители разрядов даты и времени с точек и двоеточий на слеши или что-то иное. Хоть на слова.

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

Вот, мы и натыкаемся на первую конструкцию типа цикл. Не думаю, что следует останавливаться на подробном объяснении цикла for: этот вид цикла существует во всех языках, начиная с Бейсика, и называется везде так же. Скажу только , что тело цикла выполняется до тех пор, пока выполняется условие, заключенное в скобки. В данном случае, это количество email-адресов, указанных нами в переменной EMAIL_NOC. У нас, насколько я вижу, их два. А функция PHP подсчета количества ячеек массива count() поможет нам сделать количество наших адресов произвольным. Т.е. нам не надо будет ничего менять в скриптах, когда потребуется добавить или убрать какие-то адреса из списка службы поддержки.

Таким образом тело нашей конструкции for выполняется до тех пор, пока не кончатся адреса. При каждом проходе функции к переменной $i прибавляется единица, указывая не следующую ячейку массива.

Все тело функции состоит из двух строк.

Первая функция вызывает написанную до этого mailer(), передавая ей нужные параметры. А именно:

"Robot" — это from (от кого присьмо)

$this->EMAIL_NOC[$i] — это to (кому мы отправляем письмо. Обратите внимание: это как раз обращение к очередной ячейке массива email-адресов).

"Fatal error!" — это поле письма subj (тема сообщения)

"Error: $message\nDateTime:".$this->today_date()." ".$this->today_time()."\n"."Remote IP:".$this->remote_ip()."\n\nURI: $REQUEST_URI\n\n$SQL_QUERY:".$this->sql_query."\nSQL_ERROR:".$this->sql_err — само сообщение, составленное из подготовленных нами данных.

Теперь, я уверен, вам не составит труда самостоятельно разобрать на части тело сообщения и без ошибки определить, как оно будет выглядеть в виде письма. Осталось только подсказать, что сочетание символов "\n" — не что иное, как символ перевода строки (enter).


^^^   class_in (начало)

Кто возмущен тем, что я не дал финального листинга класса utils? А зачем? Вы вполне можете составить его сами и дополнять по мере продвижения к финишу. А там и сравним наши результаты.

Кстати, вы заметили, что мы закончили с "общими" классами? Т.е. мы написали основной набор функций, который пригодится практически для любой задачи. А теперь переходим уже к более специфическим классам и функциям. У нас есть две основные ветви скриптов. Два основных направления программирования. Первое, — это возможность ввода данных в недра сайта, а второе — вывод данных для зрителя-посетителя. Почему ввод данных идет первым? А потому, что прежде чем показать что-то, надо это что-то добавить. Вот разработкой этой ветви и займемся. Давайте соберем все данные, которыми мы оперируем.

Есть:
1. Рубрики нашего сайта
2. Материалы, размещаемые на сайте.

Рубрики состоят только из их названий и уникального номера. Помните таблицу, которую мы для них создали?

А Материалы — более сложные данные. Они состоят из названия,текста и уникального номера. И не только. Но об этом позже.

Для начала займемся рубриками.

Необходимо создать функцию добавления рубрик, редактирования рубрик и их удаления.

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

Для начала, я вас прошу, создайте новый класс. Класс, который будет отвечать за добавление данных. По нашей с вами терминологии он называется class.in. А файл, соответственно, in.class.

Создали? Все заголовки для нового класса оформили? Все парные теги и скобочки закрыли? Ай, молодцы. Приятно работать с внимательными учениками.

Ввод данных.

Прежде чем броситься писать функцию, пихающую в базу какие-то данные, давайте поразмыслим о том, какие подготовительные этапы могут нам понадобиться.

"Ну какие еще подготовительные этапы", — скажет нетерпеливый программист. Написать один запрос в базу, запихивающий строку-название раздела, и всего делов.

Так-то оно так... да не так. А если сайтом управляет полный неуч? Или, того хуже, — в руки к злоумышленнику попала панель управления сайтом, и он начнет искать уязвимые места, чтобы нанести урон системе?

Вот именно! Первым этапом мы обязаны проверить те данные, которые собираемся заносить в базу. Это не сложно. Надо для себя описать условия, которым должна удовлетворять строка текста, которую мы добавляем в качестве названия раздела. Я говорю добавляем, так как сначала пишем функцию добавления нового раздела. Если я не ошибаюсь, то наша строка-название должна удовлетворять следующим параметрам: быть не короче одного символа, быть не длиннее пятидесяти символов и не должна дублировать уже существующую рубрику. Вот так и напишем (готовьте файл utils, будем добавлять в него первые сообщения об ошибках).

function in_cat_add_check($name)
{
 // добавление слешей перед управляющими символами .
 $name=AddSlashes($name);

 // проверка на кол-во символов
 if(strlen($name)==0) return(21);
 if(strlen($name)>50) return(22);

 // проверяем на существование такой рубрики
 $this->sql_res="select c_id from tbl_cats where c_name='$name'"; 
 $this->sql_execute();
 if($this->sql_err) return(11);
 if(mysql_num_rows($this->sql_res)) return(23);

 return(0);
}

И сразу, чтобы ничего не забыть, добавляем в список ошибок (файл utils.class) наши новые ошибки.

// Ошибки работы с рубриками
 $err[21]="Вы не задали название рубрики";
 $err[22]="Длинна рубрики превышает допустимые 50 символов";
 $err[23]="Такая рубрика уже есть в базе";

Вот так. Теперь, уже по традиции, пережевываю написанное в кашку.

$name=AddSlashes($name); — это очень важная и очень удобная функция PHP, которая добавляет слеши "\" ко всем символам, которые MySQL может интерпретировать как управляющие команды, превращая их в обычные текстовые символы.

Более всего, это относится к кавычкам. Если в названии нашей рубрики встретится кавычка, то интерпретатор MySQL воспримет эту кавычку как управляющую (открывающую или закрывающую) и наш запрос к базе данных будет иметь совсем не тот вид, на который мы рассчитываем. К сожалению, многие забывают об этом (или не обращают на это внимание), за что и расплачиваются. В Сети добрая половина скриптов не имеет подобных превентивных проверок, что приводит к частым взломам сайтов и проникновению в их базы данных. >Я бы мог показать вам серию несложных приемов, с помощью которых можно прощупать систему и нарушить работу стандартных функций сайтов: гостевых книг, форумов, форм оправки почты и получения чужих паролей, содержащихся в базах. Но я вас учу не взламывать чужие сайты, а защищать свои. Поэтому, будем заострять внимание на том, как надо оформлять безопасность скриптов, а не наоборот.

Вам не надо помнить весь набор управляющих символов. Функция PHP AddSlashes() сама знает, куда надо добавить слеши.

Обращаю ваше внимание на то, что в конфигурационном файле php.ini, где лежат настройки правил работы php-интерпретатора, есть параметр, отвечающий да добавление слешей в данные, передаваемые скрипту через web-форму. Проконсультируйтесь в службе поддержки вашего провайдера: включен ли этот параметр управления. Я предпочитаю его выключать и самостоятельно заботиться о безопасности. В противном случае мои скрипты могут в миг оказаться "дырявыми", если этот параметр по чьей-то воле изменит свое значение.

Далее мы производим проверку на длину строки, которую получили в качестве параметра. Функция PHP strlen() вернет нам количество символов в той строке (или переменной), которую мы передали ей в качестве параметра. Тут все очевидно и пояснять нечего, только обратите внимание, что в PHP знак сравнения обозначается в виде сдвоенного "равно". Ибо одинарное "=" — во всех ситуациях выполняет присваивание. Будьте с эти внимательны.

И, наконец, мы производим проверку на существование такого раздела в базе. В четыре строки.

1. Оформляем запрос в базу. Запрос совсем простой — комментировать его не стану.

2. Вызываем нашу функцию активации запроса (кто забыл, как она работает, обратитесь к главе "PHP: mysql.class").

3. Проверяем, не случилось ли ошибки. Обратите внимание, в классическом виде строка в скобках if() должна была бы иметь условие:
   if($this->sql_err>0) return(11);
Но, так как 0 — это "правда" по логике PHP, а любое другое число — "ложь", мы опускаем знак "больше", будучи уверенными, что условие сработает и без этого, если значение переменной sql_err не равно 0 — нормальному завершению операции.

4. И четвертой строкой мы проводим ту самую проверку на дублирование строки в базе. Функция PHP mysql_num_rows(), получая в качестве параметра ссылку на результат запросу в БД, возвращает нам количество найденных запросом строк. И если возвращенное значение не равно 0, значит такая строка в базе найдена, и нам остается только вернуть соответствующий код ошибки — 23. Поэтому, тут мы тоже можем смело опустить условие ">0".

Не знаю, догадались ли вы уже или нет, команда PHP return() не только возвращает из нашей функции указанное в скобках значение, но и немедленно прекращает выполнение данной функции, возвращая управление той, из которой произошло обращение. Другими словами, в цепочке наших проверок остановка выполнения скрипта произойдет на той строке, где прежде всего сработает команда return. Это очень удобно.

Мы с вами написали функцию проверки корректности добавляемого названия раздела и приведения строки к безопасному виду, готовому к употреблению в MySQL.


^^^   class_in (рубрики)

На предыдущем уроке мы научились подготавливать названия рубрик для добавления их в базу. Давайте теперь их добавим. Только прежде я хочу немного изменить функцию in_cat_add_check(), которою мы написали. Давайте не будем передавать указанной функции в качестве параметра название рубрики. Лучше мы для нее объявим глобальную переменную, которой и будем пользоваться для хранения названия. И будем так поступать и впредь -- каждое значение будет у нас иметь свою обособленную переменную. Так будет удобнее для понимания и красивее, что тоже приятно. То есть, функция должна выглядеть вот так:

var $in_cat_name;

function in_cat_add_check()
{
 // добавление слешей перед управляющими символами .
 $this->in_cat_name=AddSlashes($this->in_cat_name);

 // проверка на кол-во символов
 if(strlen($this->in_cat_name)==0) return(21);
 if(strlen($this->in_cat_name)>50) return(22);

 // проверяем на существование такой рубрики
 $this->sql_res="select c_id from tbl_cats where c_name='$name'"; 
 $this->sql_execute();
 if($this->sql_err) return(11);
 if(mysql_num_rows($this->sql_res)) return(23);

 return(0);
}

Немного изменений. Мы добавили перед описанием функции декларацию новой переменной in_cat_name, которая будет содержать название рубрики, убрали из объявления функции локальную переменную name, а в теле функции заменили все $name на $this->in_cat_name. Не сложно.

Вообще, я сам так делаю, и вам рекомендую: во-первых, старайтесь называть ваши функции и переменные по какой-то одной схеме, чтобы не запутаться. А во-вторых, декларируйте переменные в начале того класса, для которого они декларируются. Вот переменные, начинающиеся на in_... -- относятся к классу class_in, поэтому они так начинаются и декларировать их надо в начале этого класса, чтобы всегда было видно -- какие переменные уже объявлены и как они называются. Кстати, следующая часть за in_ указывает на то, к какой группе функций переменная относится. В нашем случае речь идет о рубриках, поэтому переменная имеет продолжение cat_. Не ленитесь давать функциям и переменным длинные понятные имена. Поверьте, лучше потратить время на написание длинных понятных имен, чем потом на вспоминание их имен и их предназначений.

Так вот, давайте теперь воспользуемся переделанной функцией для проверки данных, и добавим эти самые данные в базу.

function in_cat_add()
{
 // Проверяем данные на корректность
 $err=$this->in_cat_add_check();
 if($err) return($err);

 // Формируем запрос в БД
 $this->sql_query="insert into tbl_cats(c_name) values('".$this->in_cat_name."')";
 $this->sql_execute();
 if($this->sql_err) return(11);

 return(0);
} 

Мне уже начинает надоедать это... ни одной сложной функции пока не написано. Все как-то очень просто и примитивно. Я уж и запутываю вас, и заставляю переписывать функции на новый лад, добрались уже до самого сердца, если можно так сказать, нашего движка, а все ничего сложного...

Итак, из чего же состоит функция добавления в базу названия рубрики. Она состоит из:

1. Вызова функции проверки на правильность названия. Видите, мы сразу вызываем in_cat_add_check(), чтобы получить от нее ошибку выполнения. Если название, содержащееся в переменной in_cat_name пройдет все проверки, и в базе не окажется такого же названия, мы получим в нашу переменную err номер ошибки -- ноль. Если же это не так, то мы получим какой-то другой номер ошибки. В последнем случае мы его тут же возвращаем дальше по цепочке, не пытаясь ничего более делать: нам надо донести до пользователя информацию об ошибке. Мы еще увидим, как работает эта система передачи номера ошибки.

2. Формируем примитивный запрос в БД: добавление новой записи. Убежден, разбирать строку запроса мне не придется, вы уже отлично с эти справляетесь сами. Если это не так, обратитесь к главе язык SQL.

3. Возвращаем 0 в качестве ошибки -- подтверждение того, что функция наша отработала успешно.

Вот и все, собственно. Мы умеем добавлять в базу данных новую рубрику. Теперь ее удалим. Только удалять из базы рубрику надо по ее идентификационному номеру, а не по названию. Для того он и нужен, этот самый идентификатор, чтобы обращаться к нужной записи в базе.

Добавьте еще одну переменную, и написать функцию удаления записи из таблицы рубрик не составит никакого труда.

var $in_cat_id;

function in_cat_delete()
{
 // Приводим номер рубрики к целочисленному виду
 $this->in_cat_id=(int)$this->in_cat_id;

// Формируем запрос в БД
 $this->sql_query="delete from tbl_cats where c_id='".$this->in_cat_id."'";
 $this->sql_execute();
 if($this->sql_err) return(11);

 return(0);
} 

Все. Удалили.

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

Но необходимую защиту мы все же поставили -- преобразовав содержимое переменной in_cat_id в целое число. И если по ошибке или по причине вредительства в данной переменной окажется какая-то вредоносная строка, то она в процессе преобразования попросту превратится в значение "0". И функция нормально отработает запрос в базу: "удалить из таблицы рубрику под номером 0". Такого номера рубрики у нас быть не может, поэтому ничего страшного и не произойдет.

Другое дело -- переименование рубрики. Вот тут нам опять придется проверять название на соответствие нашим правилам. Но это не страшно. Не зря же я вынес проверку в отдельную функцию. Мы просто ее вызовем, как вызывали в in_cat_add.

function in_cat_rename()
{
 // Приводим номер рубрики к целочисленному виду
 $this->in_cat_id=(int)$this->in_cat_id;

 // Проверяем наличие в базе изменяемой рубрики
 $this->sql_query="select c_id from tbl_cats where c_id='".$this->in_cat_id."'";
 $this->sql_execute();
 if($this->sql_err) return(11);

 if(mysql_num_rows($this->sql_res)==0) return(24); // нет такой рубрики в базе

 // Проверяем данные на корректность
 $err=$this->in_cat_add_check();
 if($err) return($err);

 // Формируем запрос в БД
 $this->sql_query="update tbl_cats set c_name='".$this->in_cat_name."' where c_id='".$this->in_cat_id."'";
 $this->sql_execute();
 if($this->sql_err) return(11);

 return(0);
} 

Готово.

1. Приводим номер рубрики к целому числу.

2. Запрашиваем из базы запись с этим номером. Если такого нет, то возвращаем соответственную ошибку. Не забудьте добавить в файл utils.class ошибку под номером 24:
   $err[24]="Нет такой рубрики в базе";
Хотя, если быть честным, можно было бы тут возвратить номер ошибки "1". Ведь это больше походит на сбой в системе или на ее взлом, чем на обычную ошибку. Ну да ладно, распишем ее отдельно, пусть будет все подробно.

3. Проверяем данные на корректность, воспользовавшись уже написанной проверкой.

4. И запрашиваем базу на предмет обновления данных (update).

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


Веб-программирование на PHP (Часть II)