
web - tec / PHP 5 для начинающи
.pdf
652 Глава 17
ошибок, подключается класс Exceptions. Затем класс объявляется как абстрактный, поскольку сам по себе он ничего не должен делать ++++++ он лишь содержит методы, не+ обходимые другим классам, поэтому создавать объекты этого класса не нужно:
<?
require_once ("class.exceptions.php");
abstract class LogUtils
{
Код класса начинается с объявления нескольких полезных методов, которые ис+ пользуются другими классами. Например, следующий метод возвращает стандартное значение, в случае если источник отсутствует :
public static function getDef (&$source, $default)
{
if (!empty ($source)) return $source; return $default;
}
Этот метод, в частности, используется в конструкторе класса UserLog для при+ своения значения user_log_id по умолчанию:
$this->id = LogUtils::getDef ($initdict["user_log_id"], 0);
Следующая полезная функция работает так же, как PHP+функция implode, в отли+ чие от последней она заключает переданные ей строки в кавычки, а числа оставляет без кавычек:
public static function implodeQuoted (&$values, $delimiter)
{
$sql = ""; $flagIsFirst = true;
foreach ($values as $value) { if ($flagIsFirst) {
$flagIsFirst = false; } else {
$sql .= $delimiter;
}
if (gettype ($value) == "string") { $sql .= "'".$value."'";
} else {
$sql .= $value;
}
}
return $sql;
}
Эта функция впоследствии используется функцией generateSQLInsert() для форматирования запросов.
Затем объявляется две функции для работы с датами и временем:
public static function formattedDate ()
{
return substr(date("c"), 0, 10);
}
public static function formattedTime ()
{
return substr(date("c"), 11, 8);
}

Учебный пример: диспетчер протоколирования на PHP 653
Другая полезная функция необходима для преобразования N+мерных массивов в одномерные:
public static function arrayNth ($a, $n)
{
$out = array(); foreach ($a as $item) {
$out[$item[$n]]=$item[$n];
}
return $out;
}
Так как данные внутри приложения желательно представлять в виде массивов, следующая функция является особенно полезной. Она принимает запрос и помещает его результат в массив:
public static function queryToMultidimArray ($db, $sql)
{
$out = array();
$query = LogUtils::executeQuery ($db, $sql);
if ( !$query ) {
throw new MultiLogDatabaseQueryException ($sql);
}
while ($row = LogUtils::getQueryArray($query)) { array_push ($out, $row);
}
return $out;
}
В файл для удобства включены две функции. Первая из них возвращает массив всех имеющихся в базе данных сайтов, а вторая возвращает все уникальные разделы (или demo_ids):
public static function gatherSites ($db)
{
return LogUtils::queryToMultidimArray(
$db, "select distinct site_id from user_log order by site_id");
}
public static function gatherSections ($db)
{
return LogUtils::queryToMultidimArray(
$db, "select distinct demo_id from user_log order by demo_id");
}
Одной из главных функций, которыми необходимо обеспечить этот класс, являет+ ся функция, открывающая SQLite+базы данных. Приведенный ниже код предназначен именно для этого. (Функция возвращает одно из нестандартных исключений MultilogOpenDatabaseException(), если во время доступа к базе данных возникла какая+ либо проблема):
public static function openDatabase ()
{
$db = sqlite_popen($GLOBALS['dbpath'].$GLOBALS['dbname'], 0666, $err); if ( !$db ) {
throw new MultiLogOpenDatabaseException ();
}
return $db;
}

654 Глава 17
Обычно если открывается подключение к базе данных, то предоставляется функ+ ция для его закрытия. Однако так как эта функция создает постоянное соединение, его закрытие может негативно отразиться на других частях приложения, использую+ щих это же соединение. Если решено создавать отдельные соединения каждый раз, когда необходим доступ к данным, то закрывать эти соединения можно с помощью примерно такого кода:
public static function closeDatabase ($db)
{
// вот как это может выглядеть: if ( $db ) {
sqlite_close ($db);
}
}
В следующей функции используется созданная ранее функция implodeQuoted. Она возвращает сгенерированный SQL+оператор:
public static function generateSqlInsert ($tableName, &$metas, &$values)
{
return "insert into ".$tableName.
"(".implode ($metas, ", ")." ) ".
"values(".LogUtils::implodeQuoted($values, ", ")")";
}
Следует позаботиться об ошибках, которые могут возникать при работе с базой данных. Можно создать функцию, которая возвращала бы последнюю сгенерирован+ ную базой данных ошибку. Такая функция используется в классе Exceptions для вы+ вода на экран сообщения об ошибке, в случае если SQL+запрос сформирован неверно.
Такой подход может показаться неверным, поскольку выводить в браузер сообще+ ния об ошибках, которые раскрывают устройство приложения, не следует даже в са+ мом крайнем случае. В реальной среде это действительно недопустимо, однако здесь это делается лишь в качестве упражнения.
public static function databaseError ($db)
{
$err = "";
if ( $db ) {
$err = "error #".sqlite_last_error($db).
" : ".sqlite_error_string (sqlite_last_error($db));
}
return $err;
}
Код класса завершается определениями нескольких функций, поддерживающий абстракцию баз данных:
public static function executeQuery ($db, $sql)
{
return sqlite_query($sql, $db);
}
public static function getLastInsertedRowId ($db)
{
return sqlite_last_insert_rowid($db);
}
public static function getQueryArray ($query)
{return sqlite_fetch_array ($query);


656 Глава 17
function setProperty ($key, $value, $metaDb, $metaHuman)
{
$this->contentBase[$key] = $value; $this->contentMetaHuman[$key] = $metaHuman; $this->contentMetaDb[$key] = $metaDb;
}
function getProperties () { return $this->contentBase;
}
function getPropertiesMeta () {
return $this->contentMetaHuman;
}
Теперь с помощью следующей функции формируется SQL+оператор (обратите внимание на использование LogUtils+функции generateSqlInsert()):
function toSQL ($tableName = "user_log")
{
return LogUtils::generateSqlInsert (
$tableName, $this->contentMetaDb, $this->contentBase);
}
Далее начинается бизнес+часть класса. Функция persist() отвечает за вставку информации в базу данных. В случае неудачи она изящно завершает свою работу.
function persist ()
{
$rowid = 0;
// если объект некорректен, то нет смысла продолжать. if (!$this->isValid()) {
throw new MultiLogInvalidDataException ($this);
}
Так как после сохранения данных следует закрыть соединение, сначала необходи+ мо проверить, активно ли оно. Если нет, то:
if ($this->db == null) {
throw new MultiLogInvalidDatabaseException($this);
}
Если все идет как следует, то генерируется необходимый SQL+оператор, после чего соединение закрывается:
if (LogUtils::executeQuery($this->db, $this->toSQL())) { $rowid = LogUtils::getLastInsertedRowId ($this->db); $this->exitGracefully();
} else {
throw new MultiLogDatabaseQueryException ($this->toSQL(), $this->db);
}
Применение первичного ключа часто оказывается очень удобным, поскольку, по сути, первичный ключ представляет собой уникальный (для базы данных) идентифи+ катор. Если он есть, то возвращаем его:
return $rowid;
}


658Глава 17
Спомощью функции setProperty(), содержащейся в классе PersistableLog, ключ связывается с соответствующим значением, а также с удобочитаемым описани+ ем (которое, например, можно выводить на Web+странице). Функция setProperty() имеет следующую форму:
function setProperty ($key, $value, $metaDb, $metaHuman)
Зная об этом, можно понять, что $value в данном случае задается следующим вы+ ражением:
LogUtils::getDef ($initdict[$key], LogUtils::formattedDate())
Известно, что метод getDef() возвращает второй аргумент в случае отсутствия первого. В результате, чтобы обеспечить внутреннее представление данных со значе+ ниями по умолчанию (если реальные значения не были получены с сайта), использу+ ется следующий оператор:
$this->setProperty ($key,
LogUtils::getDef ($initdict[$key], LogUtils::formattedDate()), $key, "Date of visit");
Установка остальных свойств выполняется точно так же. Необходимо лишь обра+ ботать каждое значение и вызвать функцию setProperty():
$key="visit_time"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], LogUtils::formattedTime()), $key, "Time of visit");
$key="site"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], 0), "site_id", "Site ID");
$key="site_id"; $this->setProperty ("site",
LogUtils::getDef ($initdict[$key], $this->site), $key, "Site ID");
$key="section"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], 0), "demo_id", "Section ID");
$key="section_id"; $this->setProperty ("section",
LogUtils::getDef ($initdict["demo_id"], $this->section), "demo_id", "Section ID");
$key="login"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), "login_id","Login ID");
$key="login_id"; $this->setProperty ("login",
LogUtils::getDef ($initdict[$key], $this->login), $key, "Login ID");
$key="session"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "Session");

Учебный пример: диспетчер протоколирования на PHP 659
$key="firstname"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "First Name");
$key="lastname"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "Last/Sur Name");
$key="address1"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "Address Line 1");
$key="address2"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "Address Line 2");
$key="city"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "City");
$key="state"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "State/Province");
$key="zip"; $this->setProperty ($key,
LogUtils::getDef ($initdict[$key], ""), $key, "ZIP/Postal Code");
Прежде чем закончить с конструктором, необходимо обработать ответы (демогра+ фические данные) на вопросы каждого сайта, которые также отправляются в запросе вместе с UserLog+информацией. Для хранения демографических данных имеется от+ дельная таблица, поэтому для создания объектов, которые можно отправлять в эту таблицу, вызывается класс UserDemographics. Количество ответов в запросе зара+ нее не известно, поэтому необходимо использовать цикл до достижения границы массива. Этот цикл реализован в классе class.UserDemographic.php, который подключается в начале class.UserLog.php.
Для каждого ответа (отправленного в запросе в форме demo1=>value1, demo2=>value2 и т.д.) создается новый UserDemographic+объект, а его внутренним свойствам присваиваются значения, отправленные в запросе. (Класс UserDemographic будет рассматриваться далее.)
for ($i=0; $i <= $GLOBALS['maxdemo']; $i++) { $key = 'demo'.$i;
if (array_key_exists($key, $initdict)) { $d = new UserDemographic (
array("id"=>$this->id, "demo"=>$initdict[$key])); array_push ($this->demographics, $d);
} else { break;
}
}
}

660 Глава 17
Кроме этого, нужна функция для связывания уникальных ответов с предопреде+ ленными вопросами. Для этого необходим довольно громоздкий SQL+оператор, ко+ торый помещается в функцию gatherDemographics():
function gatherDemographics ()
{
if (!$this->db) $this->db = LogUtils::openDatabase (); $sql = "SELECT ud.user_log_id AS id, ud.answer AS demo, dd.question AS question FROM user_demographics ud LEFT JOIN user_log ul ON
ud.user_log_id = ul.user_log_id
LEFT OUTER JOIN demographic_description dd ON ( ul.demo_id = dd.demo_id and ud.seq = dd.seq ) WHERE ul.user_log_id =
'".$this->id."' AND ul.site_id = '". $this->site."' AND ul.demo_id =
".$this->section." ORDER BY ud.seq";
$query = LogUtils::executeQuery ($this->db, $sql); if ( !$query ) {
throw new MultiLogDatabaseQueryException ($sql);
}
while ($row = LogUtils::getQueryArray($query)) { $demo = new UserDemographic ($row);
if ($demo->isValid()) {
array_push ($this->demographics, $demo);
}
}
LogUtils::closeDatabase ($this->db);
}
Также следует организовать некоторую проверку корректности данных. На самом деле эту проверку при желании можно расширить, однако здесь представлено только три теста. Фактически этот класс используется из функции PersistableLog::isValid() и клас+ сов обработки исключений (которые обсуждаются далее):
function getInvalidData ()
{
$badDataEntries = array(); if ($this->site == 0) {
array_push ($badDataEntries, "'site' is zero");
}
if ($this->section == 0) {
array_push ($badDataEntries, "'section' is zero");
}
if ($this->login == "") {
array_push ($badDataEntries, "'login' is missing");
}
return $badDataEntries;
}
Наконец, определяется собственная функция persist, которая позволяет сохра+ нить UserDemographic+данные в таблице Demographics. Необходимо также связать ее с UserLog+объектами, поэтому требуется доступ к значению поля user_log_id, которое можно получить только после сохранения UserLog+объекта:

Учебный пример: диспетчер протоколирования на PHP 661
function persist()
{
// сохранить свои данные
$this->id = parent::persist();
//сохранить демографические данные, порядок имеет значение $i=0;
foreach ($this->demographics as $demo) { $demo->setId($this->id); $demo->setSequence($i++); $demo->persist();
}
}
}
?>
class.LogContainer.php
Имя этого класса ясно дает понять, что он выполняет роль контейнера для UserLog+ объектов. Как обычно, в начале файла подключаются другие необходимые файлы, а затем объявляется класс:
<?php
require_once ("common.php"); require_once ("class.UserLog.php");
class LogContainer
{
private $sql;
private $logs = array(); protected $db = null;
Конструктор инициализирует SQL+запрос, используемый для получения UserLog+ информации, которую необходимо возвратить. (Этот класс вызывается для создания отчета по журналам, которые содержатся в базе данных.)
function __construct ($start = "", $stop = "", $site = "", $section = "")
{
$initsql = "SELECT |
* FROM user_log WHERE 1=1 and"; |
if($site <> "") |
$initsql .= " site_id = '".$site."' and"; |
if($section <> "") |
$initsql .= " demo_id = '".$section."' and"; |
if($start <> "") |
$initsql .= " visit_date >= '".$start."' and"; |
if($stop <> "") |
$initsql .= " visit_date <= '".$stop."'"; |
Далее проверяется, не заканчивается ли SQL+оператор ключевым словом ‘‘and’’, после чего он используется для опроса базы данных:
// удалить последнее слово "and"?
if (substr($initsql, strlen($initsql)-4, 4) == " and") { $initsql = substr($initsql, 0, strlen($initsql) - 4);
}
// запрос к базе данных
$this->db = LogUtils::openDatabase();
$query = LogUtils::executeQuery ($this->db, $initsql);
if ( !$query ) {
throw new MultiLogDatabaseQueryException ($initsql);
}