PHP5_nachinayushim
.pdfУчебный пример: диспетчер протоколирования на PHP 663
Следует отметить, что этот класс расширяет класс PersistableLog, поэтому в нем можно использовать некоторые функции PersistableLog. Ниже приведен код конструктора:
function __construct ($initdict) // передается по значению
{
$key = "";
$this->db = LogUtils::openDatabase (); $this->setId(LogUtils::getDef ($initdict["id"],0));
$this->setId(LogUtils::getDef ($initdict["user_log_id"], $this->id)); $this->setProperty ("demo",
LogUtils::getDef ($initdict["demo"], 0), "answer",
LogUtils::getDef ($initdict["question"], "Demographic Answer")); $this->setProperty ("demo",
LogUtils::getDef ($initdict["answer"], $this->demo), "answer",
LogUtils::getDef ($initdict["question"], "Demographic Answer")); $this->question = LogUtils::getDef($initdict["question"], "");
}
Затем определяется функция setId(), используемая в конструкторе:
function setId ($id)
{
$this->setProperty ("id", $id, "user_log_id", "User ID");
}
Порядок следования ответов очень важен:
function setSequence ($seq)
{
$this->setProperty ("seq", $seq, "seq", "Sequence");
}
Следует проверить корректность UserLog+объекта, поэтому далее определяется функция getInvalidDate(). Демографические данные проверять не следует, пото+ му что отсутствие ответа вполне может означать корректный ответ:
function getInvalidData ()
{
$badDataEntries = array();
if ($this->id == null || $this->id <= 0) { array_push ($badDataEntries, "'id' is zero");
}
return $badDataEntries;
}
Наконец, переопределяем функцию toSQL следующим образом:
function toSQL ($tableName = "user_demographics")
{
return LogUtils::generateSqlInsert ($tableName, $this->contentMetaDb, $this->contentBase);
}
664 Глава 17
Сценарии проверки данных и обработки ошибок
С самого начала очень важно продумать, как будет осуществляться проверка кор+ ректности данных и обработка возможных ошибок. В предыдущем разделе уже было показано, как можно с помощью интерфейса реализовать проверку корректности данных, а также как обеспечить класс для базовой обработки ошибок, которым впо+ следствии смогут воспользоваться другие классы. Это еще один пример того, как хо+ рошие методики программирования позволяют выделить функции приложения с тем, чтобы максимально увеличить повторное использование кода и обеспечить единст+ венную точку модификации определенной функциональности.
В этом разделе представлено два сценария. Один из них ++++++ интерфейс для провер+ ки корректности данных, а другой ++++++ класс обработки исключений, методы и свойства которого наследуют все классы обработки данных. Очевидно, вы уже видели эти сце+ нарии в действии, потому что в сценариях обработки данных многократно использу+ ются вызовы функций MultiLogException() и getInvalidData().
Прежде чем углубляться в код, рассмотрим один из способов использования ин+ терфейса DataValidation в диспетчере протоколирования.
Упомянутый интерфейс реализуется классом class.exceptions.php, который представляет собой базовый класс для всех исключений и обеспечивает несколько различных классов для обработки всевозможных ошибок. Согласно UML+диаграммам, представленным в этой главе ранее, файл class.exceptions.php содержит опреде+ ления следующих классов:
MultiLogException
MultiLogOpenDatabaseException
MultiLogDatabaseQueryException
MultiLogInvalidDatabaseException
MultiLogInvalidDataException
UserLogException
LogContainerException
LogContainerInvalidDataException
Некоторые из этих классов заставляют класс, генерирующий исключение, реализо+ вать для них функцию getInvalidData(). Например, в классе MultiLogInvalidDataException объявлена следующая функция:
function suggestedSolutions ()
{
return $this->validatee->getInvalidData();
}
В классе UserDemographic, который уже рассматривался, функция getInvalidData() определена следующим образом:
function getInvalidData ()
{
$badDataEntries = array();
if ($this->id == null || $this->id <= 0) { array_push ($badDataEntries, "'id' is zero");
}
666 Глава 17
Первым объявляется класс MultiLogException, который используется многими другими классами обработки исключений. Его необходимо объявить как абстрактный, поскольку в нем реализована одна абстрактная функция. Абстрактная функция ис+ пользуется для того, чтобы заставить подклассы реализовать их собственные функции suggestedSolutions:
abstract class MultiLogException extends Exception
{
protected $message = ""; // каждая исключительная
// ситуация имеет свою причину
/** * Инициализация экземпляра с сообщением об ошибке
*/
function __construct ($msg)
{
$this->message = $msg;
}
abstract function suggestedSolutions ();
Кроме того, создается функция, возвращающая сообщение об ошибке. Очевидно, что она для реализации функции suggestedSolutions() опирается на вызывающую функцию, о чем уже говорилось ранее:
function getErrorMessage()
{
$message = "<h3>Ошибка</h3>".$this->message."<br>"; $message .= "<ul>";
foreach ($this->suggestedSolutions() as $solution) { $message .= "<li>".$solution;
}
$message .= "</ul>"; return ($message);
}
}
Затем определяется класс MultiLogOpenDatabaseException, расширяющий только что определенный класс MultiLogException. Конструктор этого класса про+ сто передает сообщение родительскому конструктору (в MultiLogException):
class MultiLogOpenDatabaseException extends MultiLogException
{
function __construct ()
{
parent::__construct ("Ошибка при открытии базы данных");
}
В данном случае функция suggestedSolutions() реализуется непосредственно, потому что вызывающий класс этого не делает ++++++ функция вызывается, только когда возникает ошибка открытия базы данных, поэтому необходимо просто вывести стан+ дартное сообщение. (Не стоит забывать, что в реальной среде сообщения о таких ошибках не следует выводить в браузер ++++++ это связано с большим риском для безопас+ ности системы.)
function suggestedSolutions ()
{
return array ("Запускался ли сценарий инициализации?", "Доступен ли каталог (".$GLOBALS['dbpath'].")
Учебный пример: диспетчер протоколирования на PHP 667
для чтения и записи Web-сервером?", "Доступен ли файл (".$GLOBALS['dbname'].") для чтения и записи Web-сервером?",
"Существует ли база данных и была ли она создана той же версией SQLite, что и в PHP?"
);
}
}
Теперь необходимо объявить класс MultiLogDatabaseQueryException для обра+ ботки исключений при выполнении запросов к базе данных. Этот класс тоже является наследником класса MultiLogException и передает сообщение родительскому конст+ руктору. В классе реализуется функция suggestedSolutions(), поэтому конструктор должен принимать два аргумента, которые можно использовать в этой функции:
class MultiLogDatabaseQueryException extends MultiLogException
{
protected $sql = ""; protected $db = null;
function __construct ($sql = "(no sql supplied)", $db = null)
{
parent::__construct ("Исключение при запросе к базе данных"); $this->sql = $sql;
$this->db = $db;
}
Затем реализуется метод suggestedSolutions(), который использует значения переменных $db и $sql (соединение с базой данных и SQL+запрос соответственно), если они переданы, для вывода некоторых полезных сообщений:
function suggestedSolutions ()
{
$err = array();
if ($this->db) { array_push (
$err, "Ошибка базы данных: ".LogUtils::databaseError ($this->db));
}
array_push ($err, "Ошибка базы данных, ожидалось SQL = ".$this->sql); array_push ($err, "Проверьте права на запись");
array_push ($err, "Проверьте, достаточно ли дискового пространства для работы базы данных");
return $err;
}
}
Следующий класс возвращает ‘‘подсказки’’, если выясняется, что используется не+ корректное соединение с базой данных:
class MultiLogInvalidDatabaseException extends MultiLogException
{
function __construct ()
{
parent::__construct ("Неправильно указанная база данных");
}
function suggestedSolutions ()
{
return array ("Корректно ли открылась база данных?",
668 Глава 17
"Была ли база данных закрыта дважды или сценарий пытается сохранить одни и те же данные дважды?");
}
}
Следующий класс весьма важен, потому что он сообщает причину появления некорректных данных. В этот класс передается объект, реализующий интерфейс DataValidation ++++++ это обязательно, потому что функция getInvalidData() в рас+ сматриваемом классе не реализована. Исключение генерируется до сохранения объекта в базе данных. Как уже было сказано, прежде чем класс PersistableLog запишет информацию в базу данных, выполняется проверка корректности объекта. Если дан+ ные объекта некорректны, то объект передается функции, которая выясняет причину и сообщает о ней:
class MultiLogInvalidDataException extends MultiLogException { private $validatee = null;
function __construct ($v)
{
parent::__construct ("Некорректные данные"); $this->validatee = $v;
}
function suggestedSolutions ()
{ return $this->validatee->getInvalidData();
}
}
Затем определяется несколько специальных классов для обработки UserLog+ исключений. После отображения короткого сообщения об ошибке вызывается метод exitGracefully(), который закрывает соединение с базой данных ++++++ помните, что этот метод определен в PersistableLog, родительском по отношению к UserLog классе:
abstract class UserLogException extends MultiLogException
{
protected $userLog = null;
function __construct ($userLog, $message)
{
parent::__construct ($message);
$this->userLog = $userLog;
$this->userLog->exitGracefully(); // метод определен в PersistableLog
}
}
В классе LogContainer также должна быть реализована обработка исключений, поэтому далее определяется класс, который обрабатывает все UserLog+объекты, соз+ данные в контейнере, и извлекает из них некорректные данные:
abstract class LogContainerException extends MultiLogException
{
protected $logs = null;
function __construct ($logs, $message) { parent::__construct ($message); $this->logs = $logs;
}
function suggestedSolutions ()
{
Учебный пример: диспетчер протоколирования на PHP 669
$solutions = array();
foreach ($this->logs as $log) { $solutions = array_merge (
$solutions, $log.DataValitation::getInvalidData());
}
return $solutions;
}
}
На самом деле генерируемое исключение не сообщает о том, в каком журнале со+ держатся некорректные данные. Для того чтобы это сделать, определяется класс+ наследник LogContainerException:
class LogContainerInvalidDataException extends LogContainerException
{
function __construct ($logs)
{
parent::__construct ($logs, "Объекты UserLog содержат некорректные данные.");
}
}
?>
Таким образом, с помощью множества разнообразных объектно+ориентированных методов (начиная от простого вызова класса и заканчивая более сложным использо+ ванием наследования и интерфейсов) реализуется обработка исключений в различ+ ных классах обработки данных. Очевидно, что реализованную здесь обработку ис+ ключений можно расширить ++++++ описанные классы представляют собой хорошую базу для дальнейшего развития.
Наличие класса исключений значительно облегчает определение нового исклю+ чения (для обработки какой+либо проблемы) и его вызов из генерирующего исключе+ ние класса. Это наглядная демонстрация эффективности инкапсуляции, краеугольно+ го камня объектно+ориентированной разработки программного обеспечения.
Сценарии уровня представления и шаблоны
После того как большая часть диспетчера протоколирования готова, следует рас+ смотреть пользовательский интерфейс приложения. Индексная страница позволяет инициализировать базу данных, запустить все тесты (их создание рассматривается в следующем разделе), отправить информацию диспетчеру протоколирования и про+ смотреть результаты. В этом разделе основное внимание уделено Smarty+шаблонам. Кроме этого, рассматривается также индексная страница ++++++ главная точка контакта пользователя с приложением.
index.php
Файл index.php служит входной точкой для всего приложения. Как видно из сле+ дующего кода, сценарий формирует набор гиперссылок, связанных с основными тес+ товыми файлами, а также со сценарием userlog.php, который использует классы обработки данных:
<H3>Настройки</H3>
<UL>
<LI><A HREF="setup.php">Инициализация базы данных</A> (Внимание! Данные будут удалены!)
<LI><A HREF="lib/test.UserLog.php">Запуск комплекта тестирования класса UserLog</A>
670 Глава 17
<LI><A HREF="lib/test.LogContainer.php">Запуск комплекта тестирования класса
LogContainer</A>
<LI><A HREF="lib/test.UserDemographic.php">Запуск комплекта тестирования
класса UserDemographic</A>
<LI><A HREF="post.php">Отправка журналов</A> <LI><A HREF="report.php">Просмотр отчетов</A>
</UL>
Последняя ссылка в коде связана со страницей report.php, которая отвечает за отображение пользовательского интерфейса, где пользователь может выбрать инте+ ресующий его журнал для просмотра.
report.php
В этом сценарии (report.php) используются Smarty+шаблоны, поэтому сначала вместе с остальными необходимыми классами нужно подключить класс Smarty. Если вы используете собственный дистрибутив Smarty, следует скорректировать путь к классу Smarty:
<?
require_once ("settings.php"); require_once ("lib/common.php");
require_once ($GLOBALS["smarty-path"].'Smarty.class.php');
Затем следует создать Smarty+объект и присвоить Smarty+переменной title заго+ ловок страницы:
$smarty = new Smarty;
$smarty->assign ('title', "MultiLog Report");
Затем требуется объявить функцию, которая будет форматировать элементы ‘‘сайт’’ и ‘‘раздел’’ для отображения в выпадающем списке. Для этого создается мас+ сив, а затем вставляется пустая строка для показываемого по умолчанию раздела. Функция arrayNth() применяется для преобразования ассоциативного массива в простой одномерный массив:
function optionMassage (&$a) {
$a = array_pad ($a, count($a)*(-1)-1,""); // увеличиваем массив $a[0][0] = ""; // вставляем пустую строку
$a = LogUtils::arrayNth($a,0); // преобразовываем массив в одномерный, включающий только значения
return $a;
}
Теперь необходимо решить, какой отчет показывать. Сначала отображается report.tpl, поскольку этот шаблон позволяет задать критерии для отбора журналов:
if ($_REQUEST['action'] <> "html") {
// отобразить входные данные $db = LogUtils::openDatabase();
$smarty->assign ('sites', optionMassage(LogUtils::gatherSites($db))); $smarty->assign ('sections', optionMassage(LogUtils::gatherSections($db)));
LogUtils::closeDatabase($db);
$smarty->display ("report.tpl");
Учебный пример: диспетчер протоколирования на PHP 671
После того как пользователь сделал выбор, для отображения результатов запроса используется шаблон report-html.tpl. Для этого сначала создается новый LogContainer+объект, который наполняется значениями, полученными в результате запро+ са, сделанного с помощью шаблона report.tpl. Затем для отображения результатов необходимо передать значения следующему шаблону:
}else {
//отображение результатов
require_once ("lib/class.UserLog.php"); require_once ("lib/class.LogContainer.php");
$logs = new LogContainer ($_REQUEST["start_Year"]. "-".$_REQUEST["start_Month"]."-".$_REQUEST["start_Day"], $_REQUEST["stop_Year"]."-".$_REQUEST["stop_Month"]."-". $_REQUEST["stop_Day"], $_REQUEST["site"], $_REQUEST["section"]);
$smarty->assign ('logs', $logs); $smarty->display ("report-html.tpl");
}
?>
Результаты работы этих сценариев показаны в конце главы, здесь рассмотрены непосредственно файлы шаблонов.
report.tpl
Шаблон report.tpl отображает несколько выпадающих списков, которые мож+ но использовать для создания запроса на выбор и отображение необходимых журна+ лов. Следует отметить, что форма возвращает значение html для параметра action. Это позволяет сценарию report.php определить, какой отчет отображать:
<html><title>MultiLog Report</title> <body>
<h3>{$title}</h3>
<form action="report.php" method="post">
<table>
<tr><td align="right">Начальная дата:</td><td>{html_select_date prefix="start_"
start_year="-2"}</td></tr>
<tr><td align="right">Конечная дата:</td> <td>{html_select_date prefix="stop_"
start_year="-2"}</td></tr> |
|
<tr><td align="right">Сайт:</td> |
<td><select name=site> {html_options |
options=$sites} </select> </td></tr> |
|
<tr><td align="right">Раздел:</td> |
<td><select name=section> {html_options |
options=$sections} </select></td></tr> |
|
<tr><td align="right"></td> |
<td><input type="submit" value="Показать |
отчет"></td></tr> |
|
</table>
<input type="hidden" name="action" value="html"> </form>
</body>
</html>
672 Глава 17
report-html.tpl
Если в базе имеются журналы для отображения, то для вывода их на экран исполь+ зуется шаблон report-html.tpl. Это самый простой шаблон для вывода, но его можно при необходимости расширить. Просто предположим, что шаблон отобража+ ет результаты в таблице и выводит количество показанных журналов. По диаграмме по+ следовательности, которая была представлена выше, можно сделать вывод, что на са+ мом деле запрос выполняется Smarty+шаблоном ++++++ в данном случае к методам toHTML()
и getCount() класса UserDemographic:
<html><title>MultiLog Report</title> <body>
<h3>{$title}</h3>
<table width="2048"><tr><td> {$logs->toHTML ()} </td></tr></table>
<h3>Количество журналов: {$logs->getCount()}</h3>
</body>
</html>
В основном приложение можно считать законченным. В нем имеется вся бизнес+ логика, а также реализована проверка корректности данных и обработка исключе+ ний. Однако это еще не все...
Тестирование приложения
Как узнать, что все действительно работает? Конечно, можно запускать сценарий несколько раз и проанализировать результаты всех попыток. Однако на самом деле не+ обходим более серьезный подход, позволяющий проверить приложение на всех этапах ее работы. Здесь оказывается полезным пакет PHPUnit, который уже обсуждался в этой главе. Ниже рассматривается создание комплекта тестов, которые позволяют прове+ рить, устойчиво ли работает приложение и работает ли оно так, как ожидалось.
Для проверки работы классов UserLog, UserLogContainer и UserDemographic
служат три набора блочных тестов. Для каждого класса формируется специфический контекст, в котором запускаются тесты, а затем различными способами используются функции, предоставленные PHPUnit.
Для проверки работы класса UserLog следует разработать серию из 11 тестов. В этом разделе также рассматривается новая PHPUnit+функция assertEquals(), ко+ торая позволяет проверить несколько условий.
Объявление класса начинается как обычно. Как уже было сказано, любой тестовый класс, для того чтобы сформировать контекст и выполнять другие операции, должен быть подклассом PHPUnit+класса TestCase. Две объявленные в классе TestUserLog переменные служат в качестве контейнеров для тестируемых объектов.
Важно отметить, что тесты получают свои параметры из settings-test.php (отдельный файл настроек), потому что разные тесты для полной сквозной проверки приложения очищают и заполняют разные таблицы базы данных. Кроме того, в чис+ ло подключаемых файлов включается PHPUnit: