Привет. В этот раз я расскажу про основы ПХП, классного языка для создания динамических html-страничек (да и не только станичек). Язык относительно простой, так что имея некоторый опыт программинга (наличие которого предполается), ты его легко освоишь. Я в нем лишь чайник (пока), но единственный способ научиться программировать - писать программы. Например, написать форум Хоррифика. Я не люблю разбирать все по разделам, поэтому буду рассказывать по ходу дела.
Итак, окно форума состоит из трех фреймов. Я вынес их в index.html:
<HTML>
<HEAD>
<TITLE>Programmers&Administrators boards</TITLE>
</HEAD>
<FRAMESET ROWS="100%" COLS="210,*" border=0>
<FRAME NAME="menu" SRC="show.php?action=menu" NORESIZE FRAMEBORDER=1
SCROLLING="YES" frameborder="NO" border=0 MARGINHEIGHT=5
MARGINWIDTH=5 framespacing=0>
<FRAMESET ROWS="50%,50%" border=1>
<FRAME NAME="headers" SRC="show.php?action=headers" SCROLLING="YES"
FRAMEBORDER=1 frameborder="YES" border=1 MARGINHEIGHT=5
MARGINWIDTH=5 framespacing=0>
<FRAME NAME="messages" SRC="show.php?action=messages" SCROLLING="YES"
FRAMEBORDER=1 frameborder="YES" border=1 MARGINHEIGHT=5
MARGINWIDTH=5 framespacing=0>
</FRAMESET>
</FRAMESET>
<NOFRAMES>
<BODY bgcolor="#FFFFFF">
<P>
Sorry, you must use a frames-capable browser (such as Microsoft Internet Explorer 3.0
or higher or Netscape Navigator 2.0 or higher).
</BODY>
</NOFRAMES>
</HTML>
Хорошо видны ссылки на файл show.php. Дабы в него можно было передать какие-то параметры, используется ? (знак вопроса), после которого идут параметры в формате переменная=значение&переменная=значение... Этот метод передачи параметров называется GET. Теперь будем заполнять первый фрейм. Для этого нам надо откуда-то взять названия разделов и все это вывести. Создаем файл show.php:
<?php
?>
Такой импровизированный тэг даст понять веб-серверу, что надо отдать содержимое этого тэга на съедение PHP, который все это переварит и выс.. выдаст контент, то есть станичку. (Кстати, его можно вставлять и посреди страницы, и не один раз). Теперь внутри этого "тега" пишем:
$disp='<html>';
$disp.='</html>';
echo $disp;
Знаком доллара в ПХП обозначаются переменные. Их не надо заранее объявлять, тип определяется автоматически. Операция присваивания обозначается знаком "равно". После оператора, как в любом нормальном языке, ставится точка с запятой. А что там за точка во второй строке? А это сокращенная запись выражений в стиле Си:
Короче, если есть строка $a = $a + $b, то убираем вторую $a, и _перед_ "равно" ставим знак операции (точка является операцией соединения строк).
Теперь, если второй строчкой написать $disp.='Hello, world!';, то веб-сервер выдаст то, что всегда первым делом предлагают вывести при изучении нового языка. А что такое echo? Это операция вывода. В данном случае она отдаст веб-серверу содержимое $disp, в которую мы пихаем содержимое страницы.
Ну хорошо, где/как будем данные доставать? Из базы данных. Не важно какой, ПХП поддерживает туеву хучу СУБД. Я решил воспользоваться самой распространенной среди веб-строителей - MySQL, хоть я и сторонник InterBase (научился у DJB фразе "Not reliable!"). Пусть у нас есть база "forum", в ней таблицы razd - названия разделов, и menu - пункты меню. Как их создать, см. ЗЫ. Коннектимся к СУБД:
$dbh=mysql_connect('адрес хоста','логин','пароль') or die('Не могу к базе приконнектиться');
mysql_select_db('forum',$dbh) or die('Не могу базу открыть');
mysql_query('SET OPTION CHARACTER SET cp1251_win');
В переменную $dbh заносится какая-то хрень, идентифицирующая соединение с СУБД. Функция die() заставит ПХП завершить работу и выдать указанную фразу в случае ошибки. mysql_select_db выбирает базу в данном соединении, а третья строка говорит MySQL, что мы типа русские и хотим использовать кодировку win1251.
Так, хорошо, к базе приконнектились, а страничка? Как мы узнаем, что именно генерить-то надо? Для этого мы передали параметр action, который, если включена опция register_globals, будет обычной переменной в нашем скрипте. Для маньяков, у которых register_globals отключена, есть массив $_GET ($HTTP_GET_VARS в старых версиях), из которого можно выдернуть переменную следующим образом: $_GET['action'] (если используется метод передачи данных POST, то соответственно $_POST ($HTTP_POST_VARS)).
Ну и как разбирать-то будем? Есть оператор switch, опять же, в стиле Си:
switch($action) {
case "menu":
...
break;
case "headers":
...
break;
}
Такой вот эквивалент нашему дельфяжному case. Смысл такой: $action сравнивается со значением после слова case, и, если совпадает, то выполняется код до слова break. Фигурные скобки {} эквивалентны begin end. Таким образом, если юзер запросит левый фрейм, то есть установит $action='menu', то выполнится одно, если $action='headers', то другое, а если кулхацкер Вася Пупкин задаст $action='hrehoten', то он получит пустую станицу, так как ни один обработчик не будет вызван.
Ну, давайте будем генерить меню:
$disp.='<body bgcolor="#000000" >'; - установим черный цвет фона
$res=mysql_query('select name,id from razd order by orderid,id');- запросим названия разделов. В $res записывается результат запроса.
while ($row=mysql_fetch_row($res)) {
..
}
О, что-то новенькое. Оператор while особо ничем не отличается от Делфи, а вот проверка условия опять в стиле доставшего всех Си (но удобно, блин!). Замутка такая: выражение $a=$b (a присвоить значение b) само имеет значение $b, то есть mysql_fetch_row($res) читает очередную запись из $res, это записывается в $row, да еще и while-у передается.(то есть можно написать $q=$w=$e=$r :-)). А когда mysql_fetch_row() даст NULL, то while наконец-то успокоится, так как это не что иное как false.
// это комментарий
$disp.='<table width="100%" CELLPADDING="2" CELLSPACING="0"'.
'BORDER="1" BGCOLOR=#555555 BORDERCOLOR=#999999>'.
'<th BGCOLOR="#999999"><font color="white"><b>'.$row[0].'</b></font><th>';
// создали таблицу на страничке
$res2=mysql_query('select name,id,hint from menu where groupid='.$row[1].' order by orderid,id');
// запросили пункты меню из данного раздела (groupid в menu = id в razd)
while ($row2=mysql_fetch_row($res2)) {
$disp.='<tr><td><a href="show.php?action=headers&menuid='.$row2[1].
'" target="headers"><font color="white"><ABBR title="'.$row2[2].'">'.
$row2[0].'</ABBR></font></a></tr>';
// создаем строчки таблицы
}
Здесь мы создаем линки, кликнув по которым в правый верхний фрейм (target="headers") загрузится страница show.php?action=headers&menuid=<номер меню>Я еще попытался добавить простейшие хинты к ним, но MS Експлопер их почему-то игнорирует (у меня-то Опера, там все круто)
Функция mysql_fetch_row() выдает массив с пронумерованными элементами (то есть обычный), в котором содержится очередная запись из набора данных. Однако некоторые любят mysql_fetch_assoc(), которая выдает массив с именованными полями, например: $row['name']. Есть также универсальная функция mysql_fetch_array(), которой второй параметр указывает что надо вернуть (более подробную информацию ищите в мануале).
Ну, менюшки вроде грузятся. Переходим ко второму фрейму:
case "headers":
if (!isset($menuid)) break;
Ага, всем знакомый оператор условного перехода (во как if по-умному называется :-)). Но оригинальное отличие: нет слова then (ладно хоть else оставили), то есть сразу идет оператор. Если захочешь вставить несколько операторов, то не забудь про эквивалент begin .. end;, то бишь фигурные скобки. Еще условие обязательно надо заключать в скобки, иначе ПХП будет ругаться. Функция isset() чем-то похожа на Assigned() в Delphi: она возвращает TRUE, если переменная существует, и FALSE, если наоборот. Есть еще почти такая же функция empty(), но она возвращает FALSE, если переменная равна нулю или NULL. А наличие $menuid для нас критично, тк мы должны выбрать мессаги из определенного раздела.
"Эй, чувак, неувязочка у тебя, - скажет читатель, - при наличии $menuid условие не выполнится и наоборот! Да еще восклицательный знак какой-то.."
Не, все нормально: восклицательный знак эквивалентен слову not в паскале, то есть он true меняет false, и наоборот.
Мессаги надо как-то группировать, чтобы знать, кто кому отвечает и соответственно делать отступы. Для них я сделал таблицу messages:
id - primary key
menuid - id темы, в которую постят мессаги
groupid - номер группы мессаг
lev - отступ
head - заголовок
nick - имя юзера
link - мыло
txt - текст мессаги
instime - время вставки
В приведенном выше куске исходника мы передаем menuid, по которому и идет выборка.
Так как номер группы постоянно увеличивается, то, чтобы сначала показать новые сообщения, сортируем по groupid по убыванию (order by groupid desc), по lev по возрастанию - чтобы сначала первый уровень показать, затем второй.., а по id - так, для верности (см. "//запрашиваем заголовки.."). Затем в цикле вставляем линки, а перед линком надо вставить пробелы (чтобы явно сказать браузеру, что мы хотим вставить пробел, используется такая шняга: ). Для этого мы выбрали значение lev из таблицы. Тут появляется оператор цикла for, который, как вы, наверное, уже догадались, в стиле вездесущего Си: в скобках через точку с запятой указываются три вещи: инициализация переменной цикла, условие, операция приращения переменной цикла.
Здесь надо отметить операцию ++ - это аналог inc() в паскале, но более хитрый: если его поставить после переменной ($a++), то значение этого выражения будет равно переменной до увеличения на единицу, а если перед переменной (++$a), то все это будет равно переменной, увеличенной на единицу. В цикле, конечно, пофиг, где эти плюсы ставить, но потом..
Теперь формируем линк: мы должны передать action=messages, то есть надо показать саму мессагу, и что именно показать - id записи, так как оно уникально. target="messages" укажет, что эта хрень должна загрузиться в третий фрейм по имени messages.
Хорошо, линк есть, отображаем имя как линк на емайл. А что делать, если юзер не указал мыло? Естественно, не делать имя линком. Ладно, а где тогда ветвление? Ну, внимательные, наверно, обратили внимание на эту строку:
Здесь использован так называемый тернарный оператор: это такая "встраиваемая" версия if-а. Все очень просто: сначала условие, вопросительный знак, что подставляем, если условие истинно, затем двоеточие, и что подставляем, если условие ложно. Но приоритет этой операции очень низок, поэтому рекомендуется все это хозяйство помещать в скобки. Здесь вроде все.
Теперь самое простое: показать мессагу.
//юзер запросил мессагу
case "messages":
// без id мы никуда
if (!isset($id)) break;
$disp.='<body><a href="show.php?action=insert&id='.$id.'" target="messages">
Добавить ответ</a><hr>';
$row=mysql_fetch_row(mysql_query('select txt from messages where id='.$id));
$disp.=StripSlashes($row[0]);
break;
ID нам дано, делать нечего - выбрали, показали.. А про StripSlashes немного ниже скажу.
Теперь рекомендую вручную забить несколько мессаг и посмотреть, как это работает, и работает ли вообще :-). Если не работает, то сверяйтесь с исходником.
Теперь приступаем к самому сложному - вставке мессаг. Такие вещи надо разрабатывать внимательно, так как кулхацкер Вася Пупкин не дремлет.
Что небходимо для вставки мессаги? Имя юзера, его емайл, заголовок и текст сообщения, ID темы, ID группы, если это ответ, а также lev, то есть отступ. Давайте так: если это вопрос, то передаем ID темы, а если это ответ, то передаем ID мессаги, на которую отвечаем.
case "insert":
if (isset($id)) {
$row=mysql_fetch_row(mysql_query('select head,menuid,groupid,lev from messages where id='.$id)) or die('Error in insert');
Сначала проверяем наличие ID - если есть, то запрашиваем данные этой мессаги, и явно устанавливаем их.
Если нам передали menuid, то явно устанавливаем lev и уничтожаем groupid с помощью процедуры unset().
if (empty($head) or empty($nick) or empty($text)) {
...
Затем проверяем наличие необходимых параметров - здесь необходимо использовать функцию empty(), так как переменная может существовать и быть пустой. Если чего-то не хватает, то показываем форму (не буду приводить этот кусок, он большой и неинтересный); если все в порядке, то переходим к вставке:
...
} else {
//вставка мессаги
if (empty($groupid)) {
$row=mysql_fetch_row(mysql_query('select max(groupid)+1 from messages where menuid='.$menuid));
if (empty($row[0])) { $groupid=1; } else { $groupid=$row[0]; };
}
$sql='select count(*) from messages where menuid='.$menuid. ' and groupid='.$groupid.
' and txt="'.AddSlashes(HTMLSpecialChars($text)).'"';
$row=mysql_fetch_row(mysql_query($sql));
if ($row[0]>0) break;
$sql='insert into messages (menuid,groupid,lev,head,nick,link,txt,instime)'.
'values ('.$menuid. ',' .$groupid. ',' .$lev. ',"' .AddSlashes(HTMLSpecialChars($head)).
'","' .AddSlashes(HTMLSpecialChars($nick)). '","' .AddSlashes(HTMLSpecialChars($email)).
'","' .AddSlashes(HTMLSpecialChars($text)). '", \'now\')';
mysql_query($sql);
Header('Location: send.html');
};
Сначала проверяем наличие $groupid - если отсутствует, то выбираем максимальный, вернее, на единицу больше. Про этот запрос нужно громко кричать "Not reliable!!", потому что если два юзера одновременно выполнят такой запрос и получат одинаковые результаты, то оба вопроса окажутся в одной группе. Но вероятность этого достаточно низка, поэтому, как говорят физики, этим можно пренебречь. Затем смотрим, а вдруг уже есть такая мессага? (ну любят люди F5 жать) Если есть, то выходим. Теперь формируем запрос вставки. Для того, чтобы нехорошие люди не вставляли html-тэги, в ПХП есть специальная функция HTMLSpecialChars(), которая преобразует все спецсимволы в корректные html-эквиваленты. А чтобы сам ПХП и MySQL корректно обработали всякие кавычки и прочее перед ними ставится слэш. И есть функция AddSlashes(), которая их расставляет. Обязательно используй эту функцию! Иначе кулхацкер Вася Пупкин может задать какой-нибудь параметр вот так:
"Vasya; select password from table_with_passwords ". И СУБД вместо ваших данных запросит не то, и в худшем случае Вася получит страницу с паролями. Однако, при выводе данных эти слеши нам нафиг не нужны, поэтому для их убирания используется функция StripSlashes. Процедура Header() добавляет http-заголовки, в данном случае предлагает броузеру пойти на.. страницу send.html, которая скажет юзеру, что сообщение принято. Но она работает, если ты еще ничего не вывел (именно поэтому я использую промежуточную переменную)
Ну, вроде все. Счастливого вам форумостроения!
ЗЫ:
Мини-FAQ
Вот открыл я index.html, а в окошках ПХП-шные исходники :-(
А ПХП у тебя на веб-сервере установлен? Или ты решил, что этот язык интегрирован в твой Интернет
Експлопер v.847927645+E18? Если все установлено, то попробуй сменить расширение с php на php3 или 4.
Как мне поставить ПХП на <что-нибудь>?
Я его ставил только на Пингвинуксе под Апач, поэтому не стал рассказывать..
У меня что-то не так работает. Может версия не та?
У меня стоит PHP v. 4.2.2
Я прочитал, но не все (ничего) понял. Что я должен знать?
Основы програмирования, SQL и HTML
А может я так отстойно объясняю..
Кто такой DJB и как переводится "Not reliable"?
DJB - профессор Dan J. Берштейн (из какого-то инстика из Америки), который пишет очень надежные в
плане безопасности проги, а "Not reliable" - это его любимая фраза, которая переводится
как "Не надежно"
Где включать register_globals?
См. php.ini или мануал
Строчки исходника здесь и в самом исходнике отличаются
Я постоянно дорабатывал исходник и не всегда вспоминал про статью.
Ты обещал про создание таблиц рассказать
Да, было дело.
Создание razd:
CREATE TABLE razd (
id int(10) unsigned NOT NULL auto_increment,
name varchar(20) NOT NULL, - название раздела
orderid int(11) NOT NULL default 0, - порядок сортировки
PRIMARY KEY (id));
Создание menu:
CREATE TABLE menu (
id int(10) unsigned NOT NULL auto_increment,
name varchar(30) NOT NULL, - название темы
orderid int(11) default 0, - порядок сортировки
groupid int(11) NOT NULL, - ID раздела
hint varchar(50) default NULL, - подсказка
PRIMARY KEY (id)
Создание messages:
CREATE TABLE messages (
id int(10) unsigned NOT NULL auto_increment,
txt text NOT NULL, - текст сообщения
groupid int(11) NOT NULL, - ID группы сообщений
menuid int(11) NOT NULL, - ID темы
head varchar(100) NOT NULL, - заголовок
nick varchar(30) NOT NULL, - имя юзера
link varchar(30), - мыло юзера
instime datetime NOT NULL, - дата/время вставки
lev int(11) default 0, - количесто отступаемых пробелов
PRIMARY KEY (id)
А вообще, в исходниках есть файл export.sql - там все команды. Просто запускаешь mysql -p < export.sql