Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Самоучитель по PHP 4

.pdf
Скачиваний:
61
Добавлен:
02.05.2014
Размер:
4.36 Mб
Скачать

Глава 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();

// текст в первом буфере