Главный модуль шаблонизатора
Основной код шаблонизатора, который и выполняет всю работу, помещен в библиотеку Template.phl. Она содержит все функции, которые могут потребоваться в шаблонах и блочных страницах. Главная функция модуля— RunUrl() — "запускает" страницу, путь к которой (относительно корневого каталога сервера) передается в параметрах. Результат работы этой функции — содержимое блока Output, порожденного страницей.
В листинге 30.14 приводится полный код шаблонизатора с комментариями.
Листинг 30.14. Модуль шаблонизатора: Template.phl
<?
// Константы, задающие некоторые значения по умолчанию
define("DefGlue"," | "); // символ склейки по умолчанию
define("Htaccess_Name",".htaccess"); // имя .htaccess-файла
// Имена "стандартных" блоков
define("BlkTemplate","template"); // шаблон страницы
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, а весь вывод просто проигноруется.
// Это удобно для коротких однострочных блоков, особенно расположенных
// в файлах .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;
// Тело явно не задано, значит, нужно его получить путем
// перехвата выходного потока. Фактически, сейчас мы просто
// говорим системе, что текущий блок — $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 — перевести его в имя файла
$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;
}
// Главная функция шаблонизатора. Обрабатывает указанный файл $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);
}
// Запускает все фильтры для блока.
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>
// </tr>
// < ?}? >
function _FBlkTabs($name,&$cont,$url)
{ // используем регулярное выражение в формате PCRE, т. к. это —
// единственный приемлемый способ решения задачи
$cont=preg_replace("/^\t+/m","",$cont);
}
?>