Итак, окно форума состоит из трех фреймов. Я вынес их в 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;

Знаком доллара в ПХП обозначаются переменные. Их не надо заранее объявлять, тип определяется автоматически. Операция присваивания обозначается знаком "равно". После оператора, как в любом нормальном языке, ставится точка с запятой. А что там за точка во второй строке? А это сокращенная запись выражений в стиле Си:

$Dlinnaya_peremennaya=$Dlinnaya_peremennaya+$Eshe_odna_super_dlinnaya_peremen
эквивалентна $Super_dlinnaya_peremennaya += $Eshe_odna_super_dlinnaya_peremennaya

Короче, если есть строка $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, по которому и идет выборка.

Полный текст обработчика:

//Если юзер запросил заголоки мессаг
case "headers":
// $menuid нам необходим
if (!isset($menuid)) break;
// формируем линк `создание вопроса`
$disp.='<body><a href="show.php?action=insert&menuid='.$menuid.
'" target="messages">Добавить вопрос</a><hr><table width="100%"
CELLPADDING="0" CELLSPACING="0" BORDER="0" '.
// пошла таблица с заголовками таблицы заголовков :-)
'BGCOLOR="#FFFFFF" BORDERCOLOR="#FFFFFF"><tr><td align="left" width="60%"><font'.
' face="Verdana" size="-2"<Тема</font></td><td width="20%" align="right"><font'.
' face="Verdana" size="-2">Имя</font></td><td width="20%" align="right"><font'.
' face="Verdana" size="-2">Дата [время]</font></td></tr></table>';
$disp.='<table width="100%" CELLPADDING="0" CELLSPACING="0" BORDER="0" BGCOLOR="#FFFFFF"
BORDERCOLOR="#FFFFFF">';
//запрашиваем заголовки мессаг.
$res=mysql_query('select head,id,nick,link,instime,lev from messages where menuid='.$menuid.'
order by groupid desc,lev asc , id asc');
while ($row=mysql_fetch_row($res)) {
$disp.='<tr><td align="left" width="58%" class="myclass2">';
for ($i=0;$i<$row[5];$i++) {
$disp.='&nbsp;&nbsp;';
};
$disp.='<a href="show.php?action=messages&id='.$row[1].'"
target="messages">'.$row[0].'</a></td>'.
'<td width="20%" align="right" class="myclass2">'. ((empty($row[3])) ? $row[2] :
'<a href="mailto:'.$row[3].'">'.$row[2].'</a>') .'</td>';
$disp.='<td width="22%" align="right" class="myclass2">'.$row[4].'</td></tr>';
}
$disp.='</table>';
break;

Так как номер группы постоянно увеличивается, то, чтобы сначала показать новые сообщения, сортируем по groupid по убыванию (order by groupid desc), по lev по возрастанию - чтобы сначала первый уровень показать, затем второй.., а по id - так, для верности (см. "//запрашиваем заголовки.."). Затем в цикле вставляем линки, а перед линком надо вставить пробелы (чтобы явно сказать браузеру, что мы хотим вставить пробел, используется такая шняга: &nbsp;). Для этого мы выбрали значение lev из таблицы. Тут появляется оператор цикла for, который, как вы, наверное, уже догадались, в стиле вездесущего Си: в скобках через точку с запятой указываются три вещи: инициализация переменной цикла, условие, операция приращения переменной цикла.

Здесь надо отметить операцию ++ - это аналог inc() в паскале, но более хитрый: если его поставить после переменной ($a++), то значение этого выражения будет равно переменной до увеличения на единицу, а если перед переменной (++$a), то все это будет равно переменной, увеличенной на единицу. В цикле, конечно, пофиг, где эти плюсы ставить, но потом..

Теперь формируем линк: мы должны передать action=messages, то есть надо показать саму мессагу, и что именно показать - id записи, так как оно уникально. target="messages" укажет, что эта хрень должна загрузиться в третий фрейм по имени messages.

Хорошо, линк есть, отображаем имя как линк на емайл. А что делать, если юзер не указал мыло? Естественно, не делать имя линком. Ладно, а где тогда ветвление? Ну, внимательные, наверно, обратили внимание на эту строку:

((empty($row[3])) ? $row[2] : '<a href="mailto:'.$row[3].'">'.$row[2].'</a>')

Здесь использован так называемый тернарный оператор: это такая "встраиваемая" версия 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');

$subj=$row[0];
$menuid=$row[1];
$groupid=$row[2];
$lev=$row[3]+1;
} else {
if (!isset($menuid)) break;
$lev=0;
unset($groupid);
};

Сначала проверяем наличие 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.
# А где взять ПХП?
http://php.net
# Как мне поставить ПХП на <что-нибудь>?
Я его ставил только на Пингвинуксе под Апач, поэтому не стал рассказывать..
# У меня что-то не так работает. Может версия не та?
У меня стоит 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