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

Самоучитель PHP 4 - Котеров Д. В.

.pdf
Скачиваний:
99
Добавлен:
24.05.2014
Размер:
4 Мб
Скачать
☆

438

Часть V. Приемы программирования на PHP

Обратите внимание на то, что в приведенном файле конфигурации задаются также и некоторые директивы Apache, которые заставляют сервер запускать программу шаблонизатора каждый раз, когда пользователь обращается к HTML-документу. Мы уже знакомы с этими директивами: в главе 29 они использовались для того, чтобы обеспечить подключение библиотекаря к каждому сценарию сервера.

Наверное, вы уже заметили, что блочный файл, который обрабатывается шаб- лонизатором, представляет собой ни что иное, как код на PHP с вызовами управляющих функций типа Block(). Этим мы достигаем множества преиму- ществ, самое главное из которых — значительное ускорение работы шаблони- затора по сравнению со способом "ручного" разбора файлов. Кроме того, от- ладочные качества сценария при таком подходе ничего не теряют: файлы блоков загружаются с помощью include, а значит, случись там ошибка, PHP исправно покажет имя файла и номер строки, где это произошло. Правда, остается единственный недостаток: несколько некрасивый синтаксис определения блоков, естественный лишь для программиста, но не для дизайнера. Что же, всегда приходится идти на какие-то жертвы…

Внимательно взгляните на определение блока Template. Как уже упоминалось, этот блок содержит имя шаблона, который будет задействоваться при отображении страницы. То, что блоки из родительских каталогов наследуются файлами, позволяет нам задать Template в одном-единственном месте, автоматически распространив его действие на все файлы в каталоге. Не правда ли, это как раз то, чего мы так долго добивались?

Шаблонизатор также обрабатывает специальным образом еще один блок. Его название — Output. Тело именно этого блока выводится в браузер пользователя, когда вся страница уже обработана. Обычно блок Output вставляют только в шаблон страницы, потому что использование его в любом другом месте оказывается бессмысленным (все равно он переопределится в шаблоне).

Автоматическая генерация названий

Если пользователь находится на сайте "Книжный магазин" в разделе "Философия" на заинтересовавшей его странице "Современность", то, конечно, в заголовке окна браузера ему бы хотелось видеть что-то вроде "Книжный магазин | Философия | Современность", а не просто "Современность". Мы уже договорились хранить название страницы в блоке Title. Но, конечно, мы бы не хотели записывать в каждой странице название полностью, потому что:

rв будущем мы можем перенести страницу в другой раздел;

rмы, возможно, захотим сменить разделитель | на /;

rэто нарушает концепцию минимальной избыточности данных, что, как мы уже неоднократно убеждались, не приводит ни к чему хорошему.

Глава 30. Код и шаблон страницы

439

Специально для решения такого рода задач в нашем шаблонизаторе предусмотрим механизм, который я далее буду называть автоматической склейкой тел блоков. Вот как он работает. Если при обработке очередного блока шаблонизатор видит, что его тело начинается с подстроки [Клей], он определяет, что текст должен быть "пристыкован" к предыдущему телу одноименного блока, но не должен заменить его. В качестве "строки-клея" выступает значение блока с именем Клей. Это позволяет нам в будущем изменить символ "склейки" лишь в одном месте, чтобы это затронуло весь сайт. В случае, если указана пустая пара квадратных скобок [] (то есть имя блока было опущено), используется тело блока с именем DefaultGlue (см. листинг 30.12), а если и он отсутствует — то |.

Теперь при загрузке страницы /phil/index.html из листинга 30.11, пользователь увидит ее полное название, составленное из блоков Title всех "надкаталогов" и самого файла страницы. Мы добиваемся этого, определив блок Title следующим образом:

<?Block("Title","[]Философия")?>

Поддержка механизма поиска включаемых файлов

В шаблонизаторе есть одна полезная функция. Называется она Load() и занимается тем, что загружает указанный в параметрах файл, который как предполагается, также имеет блочную структуру. Имя этого файла можно задавать относительно текущего каталога (в котором расположен код, вызвавший Load()), либо же в абсолютном формате (относительно корневого каталога сервера).

С помощью данной функции мы можем разбивать сложные шаблоны на части. Например, так можно было бы поступить с блоком, занимающимся формированием карты текущего раздела, особенно если существует несколько шаблонов, отображающих эту карту. Функцию Load() можно вызывать в любом месте страницы или даже из файла .htaccess. Блоки, генерируемые ей, будут вставлены непосредственно перед тем блоком, в котором она была вызвана.

На примере использования библиотекаря мы уже убедились, насколько утомительным может быть указание абсолютных путей к файлам. Поэтому функция Load() умеет сама искать включаемые файлы по серверу. Она делает это всякий раз, когда ей задан относительный путь к файлу. Поиск ведется на основе списка так называемых каталогов для поиска шаблонизатора. Этот список можно пополнять с помощью вызова Inc(), как это сделано, например, в листинге 30.12. Функция Inc() довольно интеллектуальна: даже если ей передан относительный путь к каталогу, она переводит его в абсолютный. Так что при использовании Load() из файла, расположенного в другом каталоге, не происходит никаких недоразумений.

440

Часть V. Приемы программирования на PHP

Фильтры блоков

После того, как тело блока вычислено, шаблонизатор производит его дополнительную обработку. Делается это с помощью специальных функций-фильтров. Например, "склеивание" названий осуществляется именно таким фильтром. Система устроена таким образом, что позволяет добавлять и удалять фильтры прямо в процессе работы. Для этого программисту достаточно лишь написать код функции-фильтра, а затем добавить имя этой функции в специальную таблицу фильтров (см. исходный код шаблонизатора).

В той версии шаблонизатора, которую мы сейчас рассматриваем, имеется и еще один "стандартный" фильтр. Его задача — удалить из тел блоков все начальные символы табуляции, сколько бы их ни было. Это позволяет программистам и дизайнерам свободно делать отступы в HTML-коде документов, не думая о том, что символы табуляции будут увидены пользователем при просмотре кода страницы. Впрочем, возможно, это и излишество (в конце концов, кому какое дело, как выглядит исходный код страницы).

Ради интереса вы можете написать фильтр, который превращает все символы перевода строки в пробелы. Таким образом, исходный код страницы, которую получит пользователь, будет представлять собой одну длинную строку. Код этого фильтра занимает буквально одну строку на PHP!

Поддержка трехуровневой схемы разработки сценариев

Несомненно, наш шаблонизатор будет поддерживать трехуровневую схему разработки сценариев. Иначе и быть не могло: мы не должны удалять из системы то, что прекрасно работает. Наверное, вы уже заметили, что в телах блоков мы можем свободно применять операторы PHP, а это требование является главным для любой схемы.

Чтобы не "засорять" каталоги сайта сценариями — интерфейсами и генераторами данных — предлагается разместить все, что не относится к HTML-файлам и блокам, в отдельном (недоступном извне) каталоге. Им может быть, например, тот самый каталог, где располагаются различные модули. Ведь что такое ядро сценария, как не обычная библиотека, предоставляющая функции для всеобщего использования?! Взятие на вооружение такой техники также снимает с нас заботу об указании полного пути к файлам ядра, поскольку они находятся в общедоступном каталоге модулей, а значит, могут быть включены при помощи Uses().

С загрузкой интерфейсов посредством Uses() все обстоит несколько сложнее. Вполне может возникнуть ситуация, когда один и тот же интерфейс требуется в разных местах шаблона страницы для выполнения различных действий. Функ-

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

}