Программа: Хранение сообщений форума, разбитых на темы

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

CREATE TABLE pc_message (
id INT UNSIGNED NOT NULL,
posted_on DATETIME NOT NULL,
author CHAR(255),
subject CHAR(255),
body MEDIUMTEXT,
thread_id INT UNSIGNED NOT NULL,
parent_id INT UNSIGNED NOT NULL,
level INT UNSIGNED NOT NULL,
thread_pos INT UNSIGNED NOT NULL,
PRIMARY KEY(id)
);

Первичный ключ id – это уникальное целочисленное значение, идентифицирующее конкретное сообщение. Время и дата отправки сообщения хранится в поле posted_on, а поля author (автор), subject (тема) и body (содержимое) представляют (кто бы мог подумать!) автора, тему и содержимое сообщения.


Остальные четыре поля отслеживают связи между сообщениями в потоке. Целочисленное значение thread_id определяет каждый поток. Все сообщения в определенном потоке имеют одинаковое значение поля thread_id. Если сообщение является ответом на другое сообщение, то поле parent_id представляет идентификатор сообщения, на которое отвечают. Поле level показывает уровень вложенности ответа на сообщение в потоке. Первое сообщение в потоке имеет уровень 0. Ответ на это сообщение нулевого уровня имеет уровень 1, а ответ на ответное сообщение уровня 1 имеет уровень 2. Несколько сообщений в потоке могут иметь одинаковые значения поля level и одинаковые значение поля parent_id. Например, если кто-то начинает поток сообщений о преимуществах операционной системы BeOS перед CP/M, все сердитые отклики на это сообщение от многочисленных приверженцев системы CP/M имеют уровень 1 и значение поля parent_id, равное идентификатору исходного сообщения.

Именно последнее поле, thread_pos, собственно, и позволяет упростить показ сообщения. Все сообщения потока при отображении упорядочиваются по значению их поля thread_pos.
Ниже приведены правила вычисления значения поля thread_pos:

• Первое сообщение в потоке имеет thread_pos = 0.

• Для нового сообщения N, при условии отсутствия в потоке сообщений, имеющих того же родителя, что и у N, значение поля thread_pos на единицу больше значения поля thread_pos его родителя.

• Для нового сообщения N, если в потоке есть сообщения с тем же родителем, что и у сообщения N, значение поля thread_pos на единицу больше, чем самое большое значение thread_pos у сообщений с тем же родителем.

• После того как определено значение поля thread_pos нового сообщения, все сообщения того же потока со значением поля thread_pos, большим или равным значению поля сообщения N, получают значение поля thread_pos, увеличенное на 1 (чтобы освободить пространство для сообщения N).

Программа форума, message.php, показанная в примере 10.4, сохраняет сообщения и вычисляет значения поля.


Простой вывод показан на рис. 10.5.

Пример 10.4. message.php
require 'DB.php';
// полезная функция для отладки базы данных
function log_die($ob) { print '
'; print_r($ob); print '
'; }// соединяемся с базой данных
$dbh = DB::connect('mysql://test:@localhost/test') or die("Can't connect");
if (DB::isError($dbh)) { log_die($dbh); }
$dbh->setFetchMode(DB_FETCHMODE_OBJECT);
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK,'log_die');
// Значение $_REQUEST['cmd'] говорит нам, что делать
switch ($_REQUEST['cmd']) {
case 'read': // читаем отдельное сообщение
pc_message_read();
break;
case 'post': // отображаем форму для посылки сообщения
pc_message_post();
break;
case 'save': // записываем посланное сообщение
if (pc_message_validate()) { // если сообщение допустимое,
pc_message_save(); // то сохраняем его
pc_message_list(); // и выводим список сообщений
} else {
pc_message_post(); // в противном случае, снова выводим
форму для сообщения
}
break;
case 'list': // выводим список сообщений по умолчанию
default:
pc_message_list();
break;
}
// функция pc_message_save() записывает сообщение в базу данных
function pc_message_save() {
Рис.


10.5. Доска сообщений, разделенных на потокиglobal $dbh;
$parent_id = intval($_REQUEST['parent_id']);
/* синтаксис MySQL, гарантирующий, что pc_message не изменяет
* значение, с которым мы работаем. Необходимо также заблокировать
* таблицы, которые содержат поток и последовательности pc_message
*/
$dbh->query('LOCK TABLES pc_message WRITE, thread_seq WRITE,
pc_message_seq WRITE');
// является ли сообщение ответом?
if ($parent_id) {
// получаем поток, уровень и thread_pos родительского сообщения
$parent = $dbh->getRow("SELECT thread_id,level,thread_pos
FROM pc_message
WHERE id = $parent_id");
// уровень ответа на единицу больше, чем у его родителя
$level = $parent->level + 1;
/* каково максимальное значение thread_pos среди сообщений потока
с тем же самым родителем? */
$thread_pos = $dbh->getOne("SELECT MAX(thread_pos) FROM pc_message
WHERE thread_id = $parent->thread_id AND parent_id = $parent_id");
// существуют ли ответы для данного родителя?
if ($thread_pos) {
// это thread_pos следует сразу за наибольшим
$thread_pos++;
} else {
// это первый ответ, поэтому помещаем его сразу после родителя
$thread_pos = $parent->thread_pos + 1;
}
/* увеличиваем значение thread_pos всех сообщений потока, которые
идут вслед за этим сообщением */
$dbh->query("UPDATE pc_message SET thread_pos = thread_pos + 1
WHERE thread_id = $parent->thread_id AND thread_pos >=
$thread_pos");
// новое сообщение должно быть записано с родительским thread_id
$thread_id = $parent->thread_id;
} else {
// сообщение не является ответом, поэтому оно открывает новый поток
$thread_id = $dbh->nextId('thread');
$level = 0;
$thread_pos = 0;
}
// получаем новый идентификатор для этого сообщения
$id = $dbh->nextId('pc_message');
/* вставляем сообщение в базу данных.С помощью
функций prepare() и execute() обеспечиваем соответствующеезаключение всех полей в кавычки */
$prh =
$dbh->prepare("INSERT INTO pc_message (id,thread_id,parent_id,
thread_pos,posted_on,level,author,subject,body)
VALUES (?,?,?,?,NOW(),?,?,?,?)");
$dbh->execute($prh,array($id,$thread_id,$parent_id,$thread_pos,$level,
$_REQUEST['author'],$_REQUEST['subject'],
$_REQUEST['body']));
// Сообщаем MySQL, что остальные могут теперь использовать
// таблицу pc_message
$dbh->query('UNLOCK TABLES');
}
// функция pc_message_list() выводит список всех сообщений
function pc_message_list() {
global $dbh;
print '

Message List

';
/* упорядочиваем сообщения в соответствии с их потоком (thread_id)
и их позицией внутри потока (thread_pos) */
$sth = $dbh->query("SELECT id,author,subject,LENGTH(body) AS body_length,
posted_on,level FROM pc_message
ORDER BY thread_id,thread_pos");
while ($row = $sth->fetchRow()) {
// делаем отступ для сообщений с уровнем > 0
print str_repeat(’ ’,4 * $row->level);
// выводим информацию о сообщении со ссылкой для его чтения
print<<<_HTML_
$row->subject by
$row->author @ $row->posted_on ($row->body_length bytes)


_HTML_;
}
// предоставляем возможность послать сообщение, не являющееся ответом
printf('


Start a New Thread',
$_SERVER['PHP_SELF']);
}
// функция pc_message_read() выводит отдельное сообщение
function pc_message_read() {
global $dbh;
/* проверяем, что идентификатор переданного нами сообщения является
целым числом и действительно представляет сообщение */
$id = intval($_REQUEST['id']) or die("Bad message id");
if (! ($msg = $dbh->getRow(
"SELECT author,subject,body,posted_on FROM pc_message
WHERE id = $id"))) {
die("Bad message id");
}/* не выводим введенный пользователем HTML-текст, но отображаем
символ новой строки как HTML-ограничитель строки */
$body = nl2br(strip_tags($msg->body));
// выводим сообщение со ссылками на ответ и возвращаем список сообщений
print<<<_HTML_

$msg->subject


by $msg->author



$body



Reply


List Messages
_HTML_;
}
// функция pc_message_post() выводит форму для посылаемого сообщения
function pc_message_post() {
global $dbh,$form_errors;
foreach (array('author','subject','body') as $field) {
// преобразует символы значений полей по умолчанию
// в escape-последовательности
$$field = htmlspecialchars($_REQUEST[$field]);
// окрашивает сообщения об ошибках в красный цвет
if ($form_errors[$field]) {
$form_errors[$field] = '' .
$form_errors[$field] .


'

';
}
}
// если это сообщение является ответом
if ($parent_id = intval($_REQUEST['parent_id'])) {
// вместе с представлением формы посылаем parent_id
$parent_field =
sprintf('',
$parent_id);
// если тему сообщения не передали, используем родительскую тему
if (! $subject) {
$parent_subject = $dbh->getOne('SELECT subject FROM pc_message
WHERE id = ?',array($parent_id));
/* префикс 'Re: ' к родительской теме, если она существует,
но еще не имеет префикса 'Re:' */
$subject = htmlspecialchars($parent_subject);
if ($parent_subject && (! preg_match(’/^re:/i’,$parent_subject)))
{
$subject = "Re: $subject";
}
}
}// выводим форму отправки сообщения с ошибками и значениями по умолчанию
print<<<_HTML_




Your Name: $form_errors[author]
Subject: $form_errors[subject]
Message: $form_errors[body]

$parent_field

_HTML_;
}
// функция pc_message_validate() обеспечивает
// наличие какого-либо ввода в каждом поле
function pc_message_validate() {
global $form_errors;
$form_errors = array();
if (! $_REQUEST['author']) {
$form_errors['author'] = 'Please enter your name.';
}
if (! $_REQUEST['subject']) {
$form_errors['subject'] = 'Please enter a message subject.';
}
if (! $_REQUEST['body']) {
$form_errors['body'] = 'Please enter a message body.';
}
if (count($form_errors)) {
return false;
} else {
return true;
}
}

Для корректной реализации совместного использования функции pc_message_save() необходим монопольный доступ к таблице msg в промежутке времени между началом вычисления значения поля thread_ posнового сообщения и моментом действительной записи нового сообщения в базу данных.


Чтобы обеспечить это, мы воспользовались командами MySQL’s LOCK TABLE и UNLOCK TABLES. В других базах данных синтаксис может отличаться, а может понадобиться стартовать транзакцию в начале функции и фиксировать ее в конце.
Во время вывода сообщений можно использовать поле level для ограничения извлекаемой из базы данных информации. Значительное увеличение глубины вложенности потоков обсуждения может предотвратить чрезмерное разрастание страниц. Например, ниже показано, как отобразить только первое сообщение каждого потока и все ответы на
это первое сообщение:

$sth = $dbh->query(
"SELECT * FROM msg WHERE level <= 1 ORDER BY thread_id,thread_pos");
while ($row = $sth->fetchRow()) {
// выводим каждое собщение
}

Для создания группы обсуждения на веб-сайте можно воспользоваться существующими PHP-пакетами для форумов. Наиболее популярным является Phorum (http://www.phorum.org/), а список множества других пакетов находится на http://www.zend.com/apps.php?CID=261.

Оцените статью: (0 голосов)
0 5 0

Статьи из раздела PHP на эту тему:
Защита от многократной отправки одной и той же формы
Использование элементов формы с несколькими вариантами значений
Кэширование запросов и результатов
Обработка внешних переменных с точками в именах
Обработка загруженных файлов

Вернуться в раздел: PHP / 9. Формы