Программа: Хранение сообщений форума, разбитых на темыСохранение и извлечение сообщений, относящихся к различным темам (разделенных на потоки), требует особой осторожности при отображении тем в определенном порядке. Определение потомка каждого сообщения и построение дерева отношений сообщений может привести к рекурсии запросов. Пользователи в основном просматривают список сообщений и читают отдельные сообщения значительно чаще, чем помещают свои собственные сообщения. Потратив небольшие дополнительные усилия при записи нового сообщения в базу данных, можно упростить запрос, извлекающий список показываемых сообщений, и сделать его значительно более эффективным.Сохраним сообщения в таблице, имеющей, например, такую структуру: 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_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.4. // полезная функция для отладки базы данных 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() {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'; 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->subjectby $msg->author
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_ _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. Статьи из раздела PHP на эту тему: ![]() ![]() ![]() ![]() ![]() Вернуться в раздел: PHP / 10. Доступ к базам данных
|