Очень часто создание и вывод сообщений разнесены по разным 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
непосредственно в клиентском коде. Но, пожалуй, это тема для отдельной статьи.
Автор: Н.Кашапов