Вывод сообщений пользователю в веб-приложениях
Автор: Наиль Кашапов
Вывод сообщений
пользователю — довольно распространенное действие,
которое должно выполнять веб-приложение. Оно может происходить
при обработке форм, это могут быть сообщения об ошибках, а
также сообщения, которые говорят, что надо зарегистрироваться,
когда пользователь пытается получить доступ к ограниченной
части сайта, и во многих других случаях.
Вывод сообщений пользователю — довольно
распространенное действие, которое должно выполнять
веб-приложение. Оно может происходить при обработке форм, это
могут быть сообщения об ошибках, а также сообщения, которые
говорят, что надо зарегистрироваться, когда пользователь
пытается получить доступ к ограниченной части сайта, и во
многих других случаях.
Очень часто создание и вывод сообщений
разнесены по разным HTTP-запросам. Как правило, удобно
бывает использовать редирект после обработки форм (чтобы
избежать проблем с кнопками Back и Refresh), но в то же время
естественный момент для создания сообщения — это именно
момент обработки форм и совершения действий, ему
сопутствующих. Почему? Представьте, что текст сообщения должен
выглядеть примерно так: "Количество заказываемых единиц
товара 'Коврик для мыши' успешно изменено с 7 до 12".
После редиректа, возможно, на совершенно другую с точки зрения
функциональности страницу, это будет лишняя головная —
определить, что же было совершено до этого.
Чаще всего сообщения выводят именно в POST-запросе, который
занимается обработкой формы — это нехорошо, надписи "эта
страница устарела" портят жизнь (когда пользователю вздумается
попробовать кнопку Back).
Кто-то использует редирект, махнув рукой на дружелюбные
сообщения.
В то же время имеется простой и очевидный способ сделать
жизнь лучше. Несмотря на очевидность, мне почему-то не
приходилось видеть, чтобы кто-то его использовал — по
крайней мере, когда я смотрел чужие исходники.
Итак, имеем проблему — сообщение должно "жить" в
разных запросах. Нам нужен механизм передачи текста сообщения
на страницу, которая должна его выводить. Вы уже, наверное,
вспомнили про сессии.
Да, вобщем-то вы правы. Прочие способы, например через
глобальную переменную, не позволяют сохранить данные в случае,
когда используется редирект (замечание Максима Науменко). Плюс
еще я обычно делаю так, чтобы каждый экран в приложении имел
возможность, наряду с прочей информацией, выводить сообщения,
которые были сформированы на предыдущих экранах. Это удобно,
потому что не потребуется готовить отдельные экраны для вывода
сообщений, а пользователю не придется лишний раз щелкать
мышью. Но, правда, здесь надо подумать дизайнеру —
выделить область, в которой бы появлялись сообщения.
Идея очень простая, и ее можно реализовать с помощью пары
классов.
Первое, что приходит в голову — создать класс Message
, который бы, собственно, и представлял
собой сообщение на нашей нехитрой схеме классов.
Сообщение должно уметь сохранять себя в сессии, а также
выводить себя на экран.
Message.php
class Message
{
/**
* Содержание сообщения.
*/
var $content;
/**
* Конструктор для инициализации текста сообщения.
*
* @param content содержание сообщения
*/
function Message($content)
{
$this->content = $content;
}
/**
* Запись сообщения в сессию.
*/
function send()
{
$_SESSION['session_messages'][] = $this->content;
}
/**
* Вывод сообщения на страницу.
*/
function toPage()
{
echo ' - ' . $this->content . '<br>';
}
}
Для доступа к сессии используется переменная $_SESSION
.
Замечу, что $_SESSION
— это массив, мы
используем всего лишь один элемент этого массива с индексом 'session_message'
.
В данном случае имеем дело с "массивом массивов" — то,
что мы храним в элементе 'session_message'
,
представляет собой массив, это и есть список передаваемых
сообщений (их, конечно, может быть несколько).
Если вы не смогли нащупать нить, самое время освежить в
памяти разделы мануала, посвященные сессиям и массивам.
У вас может возникнуть вопрос. А зачем здесь нужны классы?
Вполне можно было бы обойтись двумя функциями. Но давайте
заглянем дальше. Нам может понадобиться создавать сообщения с
различными типами (info, error, warning), определять адресатов
сообщений.
Заметьте, что в данный момент в сессию кладется не сам
объект, а только текст сообщения. ООП позволяет нам в
дальнейшем поменять поведение метода send()
, не
меняя клиенский код, который обращается к этому методу
(например, в будущем в сессию можно записывать полностью
объект Message
, если в нем будет много полей).
Представим, что мы бы это делали с помощью функций.
Наверное, у нас была бы функция message_send($txt)
, еще была бы функция message_to_page($txt)
. Теперь надо добавить
возможность различного поведения для различных видов
сообщений. Вызовы функций меняются: message_send($txt,
$kind)
, message_to_page($txt, $kind)
.
Придется прочесать весь код приложения в поисках таких
функций, делая исправления.
Этого можно избежать, заранее предвидя ситуацию, представив
сообщение в виде ассоциативного массива: $msg['txt']
, $msg['kind']
, тогда в
вызовах функций будет только один параметр. Чувствуете, как
это стремится превратиться в класс?
Так вот, ООП дает возможность позволить себе роскошь не
продумывать все заранее.
Идем дальше. На каждой странице мы должны вывести все
поступившие сообщения, а также удалить их из сессии после
этого. Это очень похоже на чтение писем из почтового ящика.
Следующий класс — Inbox
— как раз
для этого и предназначен.
Inbox.php
class Inbox
{
/**
* Массив поступивших сообщений.
*/
var $messages = array();
/**
* В конструкторе получаем все поступившие сообщения
* и удаляем их из сессии.
*/
function Inbox()
{
if (is_array($_SESSION['session_messages']))
{
$messages = $_SESSION['session_messages'];
$co = sizeof($messages);
for ($i = 0; $i < $co; $i++)
{
$this->messages[] = new Message($messages[$i]);
}
}
/* очищаем массив сообщений */
$_SESSION['session_messages'] = array();
}
/**
* Выводим на страницу содержимое Inbox.
*/
function toPage()
{
$co = sizeof($this->messages);
if ($co > 0)
{
echo 'Сообщение от системы: <br>';
}
for ($i = 0; $i < $co; $i++)
{
$this->messages[$i]->ToPage();
}
}
}
Давайте испытаем нашу систему сообщений.
Создадим очень простой пример, который в ответ на отправку
формы будет сообщать количество секунд в текущей минуте.
index.php
<?php
include('Inbox.php');
include('Message.php');
session_start();
if ('POST' == $_SERVER['REQUEST_METHOD'])
{
$msg = new Message('msg: ' . date('s'));
$msg->send();
/* перенаправление на себя же */
header('location:');
}
else
{
$inbox = new Inbox();
$inbox->toPage();
}
?>
<form method=post action=index.php><input type=submit></form>
Всю работу с массивами и сессиями мы спрятали внутри
классов, и конечный код выглядит просто и красиво.
Создайте каталог на веб-сервере, затем создайте в нем эти
три файла и попробуйте скрипт в работе. Заметьте, проблем с
кнопками Back и Refresh не возникает.
А теперь представьте, что вы создаете сложный портал, где,
как правило, на страницах есть несколько блоков, и каждый
может содержать внутри себя отдельное приложение.
Здесь мы встречаем два затруднения:
- Хотелось бы, чтобы список сообщений появлялся в
определенной части страницы, и вы уже подобрали хорошее
местечко для этого.
Проблема в том, что надо запустить
команду $inbox->toPage()
именно в тот
момент, который бы соответствовал положению списка сообщений
на странице. Если мы захотим поменять положение этого
списка, придется лезть в код, но нехорошо постоянно для
этого изменять каркас портала. Наилучшим решением было бы
сделать вывод сообщений в виде отдельного модуля, о котором
известно лишь только, что его надо подключить к каркасу.
То есть освободиться от строгой последовательности
запуска модулей. Действительно, раз результат работы
вывода Inbox не зависит от работы системы (на данном
шаге — все данные у нас уже есть в сессии), то зачем
лишние сложности ?
- Чтобы поддерживать внешний вид (дизайн) списка сообщений
надо заботиться об HTML-коде, который у нас зашит в методах
toPage()
классов Message
и Inbox
. Как правило, придется изменять
PHP-код для того, чтобы изменить дизайн.
Чтобы попытаться решить первую проблему, можно создать
буфер, в котором бы хранился результат работы вывода Inbox.
Возможно, у нас еще будет несколько похожих (на Inbox)
вещей, и надо создать систему буферов. Для того, чтобы
не перепутать где чей вывод, мы, наверное придем к именованию буферов. У нас будет где-то храниться
последовательность, в соответствии с которой должен
происходить вывод буферов — желательно во внешнем файле,
чтобы легче было вносить изменения.
Уже эта попытка решения дает нам идею использовать XML как
средство хранения промежуточных данных. А использование стилей
XSLT поможет справиться и со втором проблемой.
Я не буду останавливаться на том, что такое XML, и что
такое XSLT. Если вы не знакомы с этими вещами, zvon.org станет
хорошей отправной точкой для изучения.
Идея в том, чтобы в методах toPage()
формировать не HTML-код, а XML структуру. Документ страницы
будет создаваться в виде стринга с XML-кодом (он будет служить
в качестве "буфера"), а на последней стадии работы скрипта мы
будем использовать XSL-трансформацию.
Для начала представим себе, что должно являться результатом
работы основной части кода.
<messages>
<message>minute 57</message>
<message>second: 45</message>
</messages>
<refresh_form/>
Что это такое — догадаться довольно просто — два
сообщения и форма. Заметьте, PHP-скрипт должен подготовить
только такой стринг — он очень простой. Причем порядок
следования основных тегов неважен — <refresh_form/>
можно поставить вначале,
например, как будет удобно программисту. Как это реализовать.
Можно, почти ничего не меняя, использовать output buffering,
вместо HTML-кода выводить XML, а в конце просто захватить
вывод в стринг. Но тогда мы потеряем в гибкости —
например, хочется иногда выводить отладочную информацию прямо
на страницу (с помощью echo
). В то же время,
разработчики PHP работают над DOM-модулем, который предлагает
более продвинутый способ создания и передачи древовидных
документов. Если мы захотим внедрить DOM, то придется
перекраивать все приложение, изменяя вывод стрингов на
создание DOM-элементов. Поэтому я предпочитаю хранить
XML-представление объектов внутри самих объектов,
последовательно собирая общий XML-документ. Это не так сложно,
нужна всего лишь небольшая модификация. Вы увидите, что такой
прием не привязан жестко к конкретному способу хранения
XML-данных, и это позволит совершить переход к использованию
DOM "малой кровью". Прежде всего заметим, что у каждого нашего
объекта есть метод toPage()
. Эта похожесть должна
нас заставить задуматься о том, чтобы ввести новый общий
родительский класс. Пусть каждый класс, который способен
создавать кусочки XML-документа для страницы, будет
наследоваться от класса, который будет заботиться об
XML-представлении объекта. Назовем его Outputable
.
Outputable.php
class Outputable
{
/**
* XML контейнер (стринг).
*/
var $output = '';
/**
* Отдать содержимое контейнера и очистить контейнер.
*
* @return стринг с XML-данными
*/
function getOutput()
{
$out = $this->output;
$this->output = '';
return $out;
}
/**
* Добавить порцию к содержимому контейнера.
*
* @param string добавляемый стринг
*/
function appendOutput($string)
{
$this->output .= $string . "\n";
}
/**
* "Абстрактный" метод.
*/
function toPage()
{
}
}
Метод toPage()
сделан пустым — в данном
случае он нужен как индикатор того, как должны внешние
"матрешки"-классы общаться с внутренним классом. Впрочем,
здесь можно было бы предложить реализацию по умолчанию, если
бы мы заметили, что есть много объектов, которые одинаково
выводят себя на страницу.
Классы Message
и Inbox
несколько
изменятся — теперь оба они должны наследоваться от Outputable
, а также изменятся и методы toPage()
Message.php
class Message extends Outputable
{
/**
* Содержание сообщения.
*/
var $content;
/**
* Конструктор для инициализации текста сообщения.
*
* @param content содержание сообщения
*/
function Message($content)
{
$this->content = $content;
}
/**
* Запись сообщения в сессию.
*/
function send()
{
$_SESSION['session_messages'][] = $this->content;
}
/**
* Вывод сообщения на страницу.
*/
function toPage()
{
$this->appendOutput('<message>'.$this->content.'</message>');
}
}
Inbox.php
class Inbox extends Outputable
{
/**
* Массив поступивших сообщений.
*/
var $messages = array();
/**
* В конструкторе получаем все поступившие сообщения
* и удаляем их из сессии.
*/
function Inbox()
{
if (is_array($_SESSION['session_messages']))
{
$messages = $_SESSION['session_messages'];
$co = sizeof($messages);
for ($i = 0; $i < $co; $i++)
{
$this->messages[] = new Message($messages[$i]);
}
}
/* очищаем массив сообщений */
$_SESSION['session_messages'] = array();
}
/**
* Выводим на страницу содержимое Inbox.
*/
function toPage()
{
$co = sizeof($this->messages);
$this->appendOutput('<messages>');
for ($i = 0; $i < $co; $i++)
{
$this->messages[$i]->toPage();
$this->appendOutput($this->messages[$i]->getOutput());
}
$this->appendOutput('</messages>');
}
}
Изменился способ вывода — теперь вместо
непосредственного вывода на страницу внешнее представление до
поры до времени хранится в Outputable
, который
"сидит" в каждом из объектов. Метод appendOutput()
служит некоторой заменой
конструкции echo()
. Чтобы забрать вывод объекта,
используется метод getOutput()
.
Теперь посмотрим, что собой представляет клиентская часть
кода, которая будет решать ту же задачу, что и
раньше.
index.php
<?php
include('Outputable.php');
include('Inbox.php');
include('Message.php');
session_start();
/* здесь будет скапливаться XML-код */
$global_content = new Outputable;
if ('POST' == $_SERVER['REQUEST_METHOD'])
{
/* текущая минута */
$msg_min = new Message('minute ' . date('i'));
$msg_min->send();
/* текущая секунда */
$msg_sec = new Message('second: ' . date('s'));
$msg_sec->send();
/* перенаправление на себя же */
header('location:');
exit;
}
else
{
/* подготавливаем список сообщений в виде XML */
$inbox = new Inbox();
$inbox->toPage();
$global_content->appendOutput($inbox->getOutput());
}
$global_content->appendOutput('<refresh_form/>');
$xml_string = $global_content->getOutput();
$xh = xslt_create();
$xarg = array();
/* заголовок XML-документа */
$xarg['xml'] = '<?xml version="1.0" encoding="KOI8-R"?>'."\n";
/* тело XML-документа */
$xarg['xml'] .= '<page>' . $xml_string . '</page>';
/* XSL-шаблон */
$xarg['xsl'] = implode('', file('style.xsl'));
/* выводим HTML-код - результат XSL-трансформации */
echo xslt_process($xh, 'arg:xml', 'arg:xsl', NULL, $xarg);
/* выводим XML-исходник (debug) */
echo '<hr><pre>' . htmlspecialchars($xml_string) . '</pre>';
?>
Главное новшество — в объекте $global_content
, название которого говорит само
за себя. В данном случае он принадлежит классу Outputable
, в реальных задачах вы, наверное,
создадите отдельный класс для контента страницы.
Если внимательно присмотреться, то содержательная часть
скрипта практически не изменилась — тот же inbox
, тот же toPage()
. Добавлена
инструкция, которая содержимое списка сообщений выводит в
контент страницы. Для разнообразия теперь генерируется два
сообщения.
Для того, чтобы посмотреть на результат, осталось только
подготовить XSL-шаблон.
style.xsl
<?xml version="1.0" encoding="KOI8-R"?>
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" version = "1.0" >
<!-- главный шаблон, который соответствует корневому элементу -->
<xsl:template match = "/page" >
<html>
<head>
<title>XSLT Example</title>
</head>
<body bgcolor="#eeeeee">
<!-- выводим список сообщений -->
<xsl:apply-templates select="/page/messages" />
<!-- выводим форму -->
<xsl:apply-templates select="/page/refresh_form" />
</body>
</html>
</xsl:template>
<!-- шаблон для декорации сообщений -->
<xsl:template match="/page/messages/message">
<table border="1" cellspacing="0" cellpadding="0">
<tr><th>message</th></tr>
<tr><td class="message"><xsl:value-of select="."/></td></tr>
</table>
</xsl:template>
<!-- шаблон выводит форму с кнопкой -->
<xsl:template match="/page/refresh_form">
<form method="POST" action="index.php">
<input type="submit"/>
</form>
</xsl:template>
</xsl:stylesheet>
Чего же мы добились?
Прежде всего, можно смелее браться за сложные
проекты — обеспечена реальная независимость модулей.
Порядок укладки результатов на страницу теперь контролируется
с помощью внешнего XSL-шаблона и не зависит от порядка запуска
модулей.
Любой модуль, который генерирует XML-данные в
качестве результата своей работы, может быть использован в
проекте. Кстати, это одно из преимуществ перед
template-движками, в которых создание данных заключается в
последовательности вызова методов (assign и т.п.) конкретного
движка, на которых нет общего стандарта.
Еще одно преимущество — легкость отладки. Если вы
запустите скрипт, то заметите, что на каждой странице
присутствует debug-вывод — XML-прообраз, который здорово
упрощает отладку приложений.
Над чем надо еще подумать — как создавать
объекты-сообщения. Не всегда удобно использовать new
непосредственно в клиентском коде. Но,
пожалуй, это тема для отдельной статьи.
Напоследок, галопом о перспективах:
- всплывающие окна для списка важных сообщений
- "страницы-отправители" и "страницы-адресаты" в
сообщениях
- ведение лога сообщений в базе данных
- кнопка "показать историю моих действий"
- статистический анализ действий пользователей в пределах
сессий
- "интеллектуальные помощники" в веб-приложениях