Самоучитель по PHP 4
.pdfГлава 30. Код и шаблон страницы |
441 |
ция же Uses() всегда загружает файл лишь однажды, следя за тем, чтобы в следующий раз ее вызов был просто проигнорирован. Так что она нам не со- всем подходит. В качестве альтернативы предлагается добавить в код библио- текаря еще одну функцию (назвав ее, например, UsesMulti()), которая могла бы загружать указанный файл несколько раз. Единственное отличие ее кода от кода Uses() состоит в том, что она использует инструкцию include, а не include_once. Написание этой функции предоставляю читателю.
Вот и подошло к концу описание нашего шаблонизатора. Надеюсь, я ничего не упустил. Впрочем, если вдруг в приведенном ниже коде вы обнаружите еще какую-нибудь возможность, которую я здесь забыл описать, ничего страшного, наверное, не случится….
Обработчик Apache для шаблонизатора
Так как шаблонизатор должен запускаться при обращении к любой странице на сервере, для него придется написать обработчик. Я привожу здесь его код без дополнительных пояснений, поскольку он практически полностью аналогичен тому коду, который мы рассматривали в главе 29.
Листинг 30.13. Обработчик шаблонизатора: TemplateHandler.php
<?
//Проверяем, не пытается ли пользователь запустить обработчик
//напрямую, минуя Apache.
$FileName=strtr(__FILE__,"\\","/"); $ReqName=ereg_Replace("\\?.*","",strtr(getenv("REQUEST_URI"),"\\","/")); if(eregi(quotemeta($ReqName),$FileName)) {
//Выводим сообщение об ошибке. include "TemplateHandler.err";
//Записываем в журнал данные о пользователе. $f=fopen("TemplateHandler.log","a+");
fputs($f,date("d.m.Y H:i.s")." $REMOTE_ADDR — Access denied\n"); fclose($f);
//Завершаем работу.
exit;
}
//Все в порядке — корректируем переменные окружения в соответствии
//с запрошенным пользователем адресом.
@putenv("REQUEST_URI=". $GLOBALS["HTTP_ENV_VARS"]["REQUEST_URI"]=
442 |
Часть V. Приемы программирования на PHP |
$GLOBALS["REQUEST_URI"]= getenv("QUERY_STRING")
); @putenv("QUERY_STRING=".
$GLOBALS["HTTP_ENV_VARS"]["QUERY_STRING"]= $GLOBALS["QUERY_STRING"]= ereg_Replace("^[^?]*\\?","",getenv("QUERY_STRING"))
);
//Подключаем библиотекаря. $INC[]=getcwd();
include "Librarian.phl";
//Переходим в каталог со страницей. chdir(dirname($SCRIPT_FILENAME));
//Загружаем шаблонизатор. Uses("Template");
//Выводим содержимое главного блока страницы. echo RunUrl($SCRIPT_NAME);
?>
Главный модуль шаблонизатора
Основной код шаблонизатора, который и выполняет всю работу, помещен в библиотеку Template.phl. Она содержит все функции, которые могут потребоваться в шаблонах и блочных страницах. Главная функция модуля — RunUrl() — "запускает" страницу, путь к которой (относительно корневого каталога сервера) передается в параметрах. Результат работы этой функции — содержимое блока Output, порожденного страницей.
В листинге 30.14 приводится полный код шаблонизатора с комментариями.
Листинг 30.14. Модуль шаблонизатора: Template.phl
<?
// Константы, задающие некоторые значения по умолчанию define("DefGlue"," | "); // символ склейки по умолчанию define("Htaccess_Name",".htaccess"); // имя .htaccess-файла
// Имена "стандартных" блоков define("BlkTemplate","template"); // шаблон страницы
Глава 30. Код и шаблон страницы |
443 |
define("BlkOutput","output"); |
// этот блок выводится в браузер |
define("BlkDefGlue","defaultglue"); // символ для "склейки" по умолчанию
// Рабочие переменные |
|
$GLOBALS["BLOCK"]=array(); |
// массив тел всех блоков |
$GLOBALS["BLOCK_INC"]=array(); |
// аналог $INC библиотекаря |
$GLOBALS["CURBLOCK_URL"]=false; |
// URL текущего обрабатываемого файла |
$GLOBALS["bSingleLine"]=0; |
// обрабатываемый файл — .htaccess? |
//В следующем массиве перечислены имена функций-фильтров,
//которые будут вызваны для каждого блока, когда получено его
//содержимое. Вы, возможно, захотите добавить сюда и другие
//фильтры (например, исполняющие роль простейшего макропроцессора,
//заменяющего одни тэги на другие). Формат функций:
//void FilterFunc(string $BlkName, string &$Value, string $BlkUrl) $GLOBALS["BLOCKFILTERS"]=array(
"_FBlkTabs", "_FBlkGlue"
//*** Здесь могут располагаться имена ваших функций-фильтров. );
//Возвращает тело блока по его имени. Регистр символов не учитывается. function Blk($name)
{ return @$GLOBALS["BLOCK"][strtolower($name)];
}
//Добавляет указанный URL в список путей поиска. При этом путь
//автоматически преобразуется в абсолютный URL (за текущий каталог
//принимается каталог текущего обрабатываемого файла).
function Inc($url)
{global $CURBLOCK_URL,$SCRIPT_NAME; $CurUrl=$CURBLOCK_URL; if(!$CurUrl) $CurUrl=$SCRIPT_NAME; if($url[0]!="/") $url=abs_path($url,dirname($CurUrl)); $GLOBALS["BLOCK_INC"][]=$url;
}
//Устанавливает имя текущего блока и, возможно, его значение.
//Все данные, выведенные после вызова этой функции, будут принадлежать
//телу блока $name. Если задан параметр $value, тело сразу
//устанавливается равным $value, а весь вывод просто проигноруется.
444 |
Часть V. Приемы программирования на PHP |
//Это удобно для коротких однострочных блоков, особенно расположенных
//в файлах .htaccess. Из того, что было выведено программой в
//стандартный поток, будут удалены начальные и концевые пробелы,
//а также вызовутся все функции-фильтры. Окончанием вывода,
//принадлежащего указанному блоку, считается конец файла либо начало
//другого блока (то есть еще один вызов Block()).
function Block($name=false, $value=false)
{global $BLOCK,$bSingleLine,$CURBLOCK_URL; // Объявляем некоторые флаги состояния
static $Handled=false; // в прошлый раз вывод был перехвачен static $CurBlock=false; // имя текущего обрабатываемого блока
//Если имя блока задано, перевести его в нижний регистр if($name!==false) $name=strtolower(trim($name));
//Вывод был перехвачен. Значит, что до этого вызова уже
//была запущена функция Block(). Теперь блок, который
//она обрабатывала, закончился, и его надо добавить в массив
//блоков (или же проигнорировать этот вывод).
if($Handled) {
// Имя предыдущего блока было задано? if($CurBlock!==false) {
// Добавляем в массив блоков. $BLOCK[$CurBlock]=trim(ob_get_contents());
// Если блок однострочный (из файла .htaccess), то
//удаляем все строки, кроме первой. if(@$bSingleLine)
$BLOCK[$CurBlock]=ereg_Replace("[\r\n].*","",$BLOCK[$CurBlock]);
//Запускаем фильтры
_ProcessContent($CurBlock,$BLOCK[$CurBlock],$CURBLOCK_URL);
}
// Завершаем перехват потока вывода ob_end_clean(); $Handled=0;
}
//Если имя блока задано (а это происходит практически всегда),
//значит, функция была вызвана нормальным образом, а не только для
//того, чтобы завершить вывод последнего блока (см. функцию Load()). if($name!==false) {
//Перехватываем поток вывода
ob_start(); $Handled=1;
// Тело явно не задано, значит, нужно его получить путем
Глава 30. Код и шаблон страницы |
445 |
//перехвата выходного потока. Фактически, сейчас мы просто
//говорим системе, что текущий блок — $name, и что как только
//она встретит другой блок или конец файла, следует принять
//выведенные данные и записать их в массив.
if($value===false) { $CurBlock=$name;
}else {
//Тело задано явно. Записать блок в массив, но все равно
//перехватить выходной поток (чтобы потом его проигнорировать). _ProcessContent($name,$value,$CURBLOCK_URL); $BLOCK[$name]=$value;
$CurBlock=false;
}
}
}
//Загружает файл с URL $name и добавляет блоки, которые в нем
//находились, к списку существующих блоков. Параметр $name может
//задавать относительный URL, в этом случае производится его
//поиск в глобальном массиве $INC (том же самом, который использует
//библиотекарь). Если в качестве $name задано не имя файла, а имя
//каталога, то анализируется файл .htaccess, расположенный
//в этом каталоге. На момент загрузки файла текущий каталог
//изменяется на тот, в котором расположен файл.
function Load($name)
{ global $BLOCK,$bSingleLine,$CURBLOCK_URL,$BLOCK_INC;
//Перевести все пути в $INC в абсолютные
AbsolutizeINC();
//Если путь относительный, ищем по $BLOCK_INC $fname=false;
if($name[0]!='/') {
//Перебираем все каталоги включения foreach($BLOCK_INC as $v) {
$fname=Url2Path("$v/$name"); // Определяем имя файла if(file_exists($fname)) { $name="$v/$name"; break; }
}
//Если не нашли, $fname остается равной false
}else {
// Абсолютный URL — перевести его в имя файла
446 |
Часть V. Приемы программирования на PHP |
$fname=Url2Path($name);
}
// Обрабатываем файл, имя которого вычислено по URL.
//Сначала проверяем, существует ли такой файл. if($fname===false || !file_exists($fname))
die("Couldn't open \"$name\"!");
//Это каталог — значит, используем .htaccess $Single=false;
if(@is_dir($fname)) { $name.="/".Htaccess_Name; $fname.="/".Htaccess_Name; $Single=1;
}
//Если файла до сих пор не существует (это может случиться, когда
//мы предписали использовать .htaccess, а его в каталоге нет),
//"мирно" выходим. Ничего страшного, если в каталоге нет .htaccess'а. if(!file_exists($fname)) return;
//Запускаем файл. Для этого сначала запоминаем текущее состояние
//и каталог, затем загружаем блоки файла (просто выполняем файл),
//а в конце восстанавливаем состояние.
$PrevSingle=$bSingleLine; $bSingleLine=@$Single; $SaveDir=getcwd(); chdir(dirname($fname)); $SaveCBU=$CURBLOCK_URL; $CURBLOCK_URL=$name;
//Возможно, в файле присутствуют начальные пробелы или другие
//нежелательные символы (например, в .htaccess это может
//быть знак комментария). Все они включатся в блок с
//именем _PreBlockText (его вряд ли целесообразно использовать). Block("_PreBlockText");
//Делаем доступными все глобальные переменные.
foreach($GLOBALS as $k=>$v) if(!@Isset($$k)) global $$k;
//Запускаем файл. include $fname;
//Сигнализируем, что блоки закончились (достигнут конец файла).
//При этом чаще всего будет осуществлена запись данных последнего
//блока файла в массив.
Block(); chdir($SaveDir); $CURBLOCK_URL=$SaveCBU; $bSingleLine=$PrevSingle;
Глава 30. Код и шаблон страницы |
447 |
}
//Главная функция шаблонизатора. Обрабатывает указанный файл $url
//и возвращает тело блока Output. В выходной поток ничего не печатается
//(за исключением предупреждений, если они возникли).
function RunUrl($url)
{global $BLOCK;
//Собираем все блоки. _CollectBlocks($url);
//Находим и запускаем главный шаблон. Мы делаем это в последнюю
//очередь, чтобы ему были доступны все блоки, из которых состоит
//страница. Шаблон — обычный блочный файл. В нем обязательно должен
//присутствовать блок Output.
$tmpl=@$BLOCK[BlkTemplate]; if(!$tmpl) {
die("Cannot find the template for <b>$url</b> ".
"(have you defined <tt>".BlkTemplate."</tt> block?)");
}
Load($tmpl);
// Возвращаем блок Output. if(!isSet($BLOCK[BlkOutput])) {
die("No output from template <b>$tmpl</b> ".
"(have you defined <tt>".BlkOutput."</tt> block?)");
}
return $BLOCK[BlkOutput];
}
//Эта функция предназначена для внутреннего использования. Она собирает
//блоки из файла, соответствующего указанному $url, в том числе и блоки
//из всех .htaccess-файлов "надкаталогов".
function _CollectBlocks($url)
{global $BLOCK; $url=abs_path($url,dirname($GLOBALS["SCRIPT_NAME"]));
//Если путь — не /, то обратиться к "надкаталогу". if(strlen($url)>1) _CollectBlocks(dirname($url));
//Загрузить блоки самого файла.
Load($url);
}
448 |
Часть V. Приемы программирования на PHP |
//Запускает все фильтры для блока. function _ProcessContent($name,&$cont,$url) { foreach($GLOBALS["BLOCKFILTERS"] as $F)
$F($name,$cont,$url);
}
//"Склеивание" блоков.
//Если тело блока начинается с [name], то оно не просто
//записывается в массив блоков, а "пристыковывается" к значению,
//уже там находящемуся, причем в качестве символа-соединителя
//выступает тело блока с именем name. Если строка name не задана
//(то есть указаны []), используется блок с именем DefaultGlue,
//а если этого блока нет, то соединитель по умолчанию — " | ". function _FBlkGlue($name,&$cont,$url)
{ global $BLOCK; if(ereg("^\\[([^]])*]",$cont,$P)) {
$c=substr($cont,strlen($P[0])); // тело блока после [name]
$n=$P[1]; |
// имя соединителя |
//Есть с чем "склеивать"? if(!empty($BLOCK[$name])) {
$glue=@$BLOCK[$n];
if(!Isset($glue)) $glue=@$BLOCK[BlkDefGlue]; if(!Isset($glue)) $glue=DefGlue; $cont=$BLOCK[$name].$glue.$c;
}
//"Склеивать" нечего — просто присваиваем. else $cont=$c;
}
}
//Удаление начальных символов табуляции из тела блока.
//Теперь можно выравнивать HTML-код в документах с помощью табуляции.
//Это оказывается чрезвычайно удобным, если мы используем тэги,
//например, в таком контексте:
//< ?foreach($Book as $k=>$v) {? >
//<tr>
//<td>< ?=$Book['name']? ></td>
//<td>< ?=$Book['text']? ></td>
Глава 30. Код и шаблон страницы |
449 |
//</tr>
//< ?}? >
function _FBlkTabs($name,&$cont,$url)
{// используем регулярное выражение в формате PCRE, т. к. это — // единственный приемлемый способ решения задачи
$cont=preg_replace("/^\t+/m","",$cont);
}
?>
"Перехват" выходного потока
В коде листинга 30.14 есть всего лишь несколько вызовов стандартных функций, которые мы еще не рассматривали в этой книге. Я имею в виду функции с префиксами ob_ (от Output Buffering — Буферизация вывода). Их задача — "перехватить" тот текст, который выводится операторами echo, а также участками, расположенными вне PHP-тэгов <? и ?>, и направить его в строковую переменную для дальнейшей обработки.
Эти чрезвычайно полезные функции впервые введены в PHP версии 4. Нужно за- метить, что без них вряд ли можно написать более-менее удобный шаблонизатор.
Я привожу здесь их описания в том виде, который принят в этой книге.
void ob_start()
Вызов данной функции говорит PHP, что необходимо начать "перехват" стандартного выходного потока программы. Иными словами, весь текст, который выводится операторами echo или расположен вне участков кода PHP, будет накапливаться в специальном буфере, а не отправится в браузер. В любой момент времени мы можем получить все содержимое этого буфера, вызвав функцию ob_get_contents(). В шаблонизаторе мы вызываем ob_start() каждый раз, когда встречается начало нового блока.
string ob_get_contents()
Функция возвращает текущее содержимое буфера, который заполняется операторами вывода при включенном режиме буферизации. Именно ob_get_contents() обеспечивает в нашем шаблонизаторе возможность накопления текста блоков. Она вызывается (а возвращенные данные записываются в массив) каждый раз, когда заканчивается очередной блок (вернее, перед началом следующего блока), а также при достижении конца файла.
450 |
|
Часть V. Приемы программирования на PHP |
|
|
|
|
|
|
В случае, если буферизация выходного потока не была включена, функция возвращает false. Это свойство можно использовать для проверки того, ус- тановлен ли буфер вывода, или же данные сразу направляются в браузер.
void ob_end_clean()
Вызов данной функции завершает буферизацию выходного потока. При этом все содержимое буфера, которое было накоплено с момента последнего вызова ob_start(), теряется (не попадает в браузер). Конечно, если текст вывода нужен, необходимо сначала получить его при помощи ob_get_content(). Именно так и происходит в шаблонизаторе. Вызов функции ob_end_clean() с последующим ob_start() — единственный способ очистить внутренний буфер PHP.
void ob_end_flush()
Эта функция практически полностью эквивалентна ob_end_clean(), за исключением того, что данные, накопленные в буфере, немедленно выводятся в браузер пользователя. Ее применение оправдано, если мы хотим отправлять данные страницы клиенту, параллельно записывая их в переменную для дальнейшей обработки.
Стек буферов
Необходимо сделать несколько замечаний насчет функций "перехвата" выходного потока программы. Что получится, если больше одного раза подряд вызвать ob_start()? Хотя об этом не написано ни слова в официальной документации, рискну взять на себя ответственность и заявить, что, в общем-то, ничего нежелательного не произойдет. Последующие операторы вывода будут работать с тем буфером, который был установлен самым последним вызовом. При этом функция ob_end_clean() не завершит буферизацию, а просто установит в активное состояние "предыдущий" буфер (разумеется, сохранив его предыдущее содержимое). Легче всего понять этот механизм на примере:
Листинг 30.15. Пример "перехвата" выходного потока
<?
ob_start(); // устанавливаем перехват в буфер 1
echo "1"// попадет в 1-й буфер |
|
|
ob_start(); // откладываем на время буфер 1 |
и активизируем второй |
|
echo "2"; |
// текст попадет в |
буфер 2 |
$A[2]=ob_get_contents(); |
// текст во втором |
буфере |
ob_end_clean(); |
// отключает буфер |
2 и активизируем первый |
echo "1"; |
// попадет опять в буфер 1 |
|
$A[1]=ob_get_contents(); |
// текст в первом буфере |