Самоучитель по PHP 4
.pdfГлава 29. Модульность программы. Написание "библиотекаря" |
401 |
function Url2Path($name)
{$curUrl=dirname($GLOBALS["SCRIPT_NAME"]);
$url=abs_path(trim($name),$curUrl); return getenv("DOCUMENT_ROOT").$url;
}
//Превращает все пути в списке $INC в абсолютные, однако делает это
//не каждый раз, а только если массив изменился с момента последнего
//вызова.
function AbsolutizeINC()
{global $INC;
static $PrevINC=""; // значение $INC при предыдущем входе
//Сначала проверяем — изменился ли $INC. Если да, то преобразуем
//все пути в массиве в относительные, иначе ничего не делаем.
//Нам это нужно только из соображений повышения производительности
//функции.
if($PrevINC!==$INC) {
//Мы не можем использовать foreach, т. к. нам надо
//модифицировать массив
for($i=0; $i<count($INC); $i++) { $v=&$INC[$i];
if($v[0]=="." && (strlen($v)==1 || $v[1]=='\\' || $v[1]=='/')) continue;
$v=abs_path($v);
}
// Запоминаем текущее состояние массива $PrevINC=$INC;
}
}
//Загружает указанную библиотеку функций. Для поиска файла
//просматривает каталоги в массиве $INC.
function Uses($libname)
{global $INC;
static $PrevINC=""; // значение $INC при предыдущем входе static $LastFound=0; // для ускорения работы
//Переводим все пути в $INC в относительные — вдруг вызывающая
//программа добавила что-нибудь в массив?..
AbsolutizeINC();
402 |
Часть V. Приемы программирования на PHP |
//Теперь просматриваем пути, начиная с того, по которому была
//найдена какая-нибудь предыдущая загруженная библиотека. Скорее
//всего, там окажется загружаемый сейчас модуль. Если нет —
//что же, просмотрим весь список...
$l=$LastFound; do {
// В очередном каталоге есть файл модуля?.. $dir=$INC[$LastFound]; if(@is_file($file="$dir/$libname.".LibExt)) {
//Сменить каталог на тот, в котором расположен модуль $cwd=getcwd();
chdir(dirname($file));
//Делаем доступными для модуля все глобальные переменные foreach($GLOBALS as $k=>$v) global $$k;
//Включаем файл
$ret=include_once($file);
//Пока не вернулись в предыдущий каталог, перевести
//добавленные (возможно?) пути в $INC в абсолютные
AbsolutizeINC();
//Вернуться
chdir($cwd); return $ret;
}
$LastFound=($LastFound+1)%count($INC); } while($LastFound!=$l);
// Ничего не вышло — "умираем"...
die("Couldn't find library \"$libname\" at ".join(", ",$INC)."!");
}
//Корректируем некоторые переменные окружения, которые могут иметь
//неверные значение, если PHP установлен не как модуль Apache @putenv("SCRIPT_NAME=".
$GLOBALS["HTTP_ENV_VARS"]["SCRIPT_NAME"]= $GLOBALS["SCRIPT_NAME"]= ereg_Replace("\\?.*","",getenv("REQUEST_URI"))
); @putenv("SCRIPT_FILENAME".
$GLOBALS["HTTP_ENV_VARS"]["SCRIPT_FILENAME"]= $GLOBALS["SCRIPT_FILENAME"]= Url2Path(getenv("SCRIPT_NAME"))
Глава 29. Модульность программы. Написание "библиотекаря" |
403 |
);
//На всякий случай включаем максимальный контроль ошибок
Error_reporting(1+2+4+8);
//ВНИМАНИЕ! После следующего закрывающего тэга
//не должно быть НИКАКИХ ПРОБЕЛОВ! В противном случае
//сценарий, подключающий библиотекаря, будет выводить в самом
//начале своей работы этот пробел, что недопустимо при
//работе с Cookies.
}?>
Обратите внимание на то, что весь код библиотекаря помещен в блок оператора if. Это сделано специально, чтобы при возможной (ошибочной) повторной загрузке библиотекаря по include все работало корректно.
Возможно, вы скажете, что то же самое можно было бы сделать и в модулях, и обойтись вообще без библиотекаря. Однако это приведет к заметной потере производительности, потому что интерпретатору каждый раз придется загру- жать и разбирать весь файл модуля, а это — основное время при запуске про- граммы.
Пожалуй, в приведенном коде есть и еще одно интересное место. Я имею в виду инструкции, помеченные комментарием: "Делаем доступными для модуля все глобальные переменные". Зачем это нужно? Разве глобальные переменные по определению не доступны подключаемому модулю? К сожалению, это так, и вот почему. Мы вызываем include_once в теле функции Uses(), а не в глобальном контексте. Неудивительно, что подключенный файл работает не в нем, а в области видимости тела функции. Указанный цикл перебора всех глобальных переменных и их "глобализация" с помощью global решает проблему.
Здесь есть еще одна тонкость. Если модуль "захочет" определить какую-либо новую глобальную переменную, он не сможет сделать это никак иначе, чем через массив $GLOBALS. Однако изменять имеющиеся переменные напрямую он все же способен.
Работа с библиотекарем
Рассмотрим пример сценария, использующего библиотекарь в своей работе. Мы будем предполагать, что все модули размещены в подкаталоге /lib основного каталога
404 |
Часть V. Приемы программирования на PHP |
с Web-документами (если вы заметили, такой каталог уже есть в путях поиска модулей по умолчанию, "зашитых" в библиотекаре).
Пока мы будем подключать библиотекаря явно — инструкцией include. Ко- нечно, это не очень удобно. Очень скоро мы узнаем, как избавиться от указан- ного недостатка.
Пусть сценарию требуется библиотека files.phl, которую мы написали (или где-то достали, хотя модули для PHP все еще большая редкость), и которая содержит некоторые функции для работы с файлами.
Кстати, модулю files.phl самому могут понадобиться некоторые модули. Если это так, нет проблем: достаточно лишь поставить вызов Uses() внутрь кода библиотеки.
Листинг 29.2. Тестовый сценарий
<?
include "$DOCUMENT_ROOT/lib/librarian.phl"; // подключаем библиотекарь Uses("files"); // подключаем модуль files.phl
// Все — теперь можно использовать модуль $Content=ReadAllFile("myfile.txt"); // читаем весь файл myfile.txt $Hash=ReadKeyValFile("keyval.txt"); // читаем файл формата key=value // ... и другие функции, которые, возможно, присутствуют в модуле
?>
Как видите, ничего сложного. Давайте теперь посмотрим, как выглядит модуль files.phl.
Листинг 29.3. Пример модуля files.phl
<?
//Внимание! Так указывается дополнительный каталог для поиска модулей.
//Запись означает, что библиотекарь должен искать модули также и в
//подкаталоге OtherModules/dk текущего каталога
$INC[]="OtherModules/dk";
// Подключение каких-то других модулей, в которых нуждается files.phl Uses("SomeOtherModule");
Uses("AndOtherModuleToo");
Глава 29. Модульность программы. Написание "библиотекаря" |
405 |
//Константа: символы перевода строки define("CRLF",getenv("COMSPEC")?"\r\n":"\n");
//Читает все содержимое файла $fname и возвращает его function ReadAllFile($fname)
{ $f=fopen($fname,"r"); if(!$f) return ""; $Cont=fread($f,1000000); fclose($f); return $Cont;
}
//Читает файл $fname, строки которого имеют формат
//ключ1=значение1
//Возвращает ассоциативный массив с указанными в файле ключами function ReadKeyValFile($fname)
{ $Cont=@File($fname); if(!@is_array($Cont)) return array(); $Hash=array();
foreach($Cont as $i=>$st) { if(!ereg("^([^=]+)=(.*)",$st,$regs)) continue; $Hash[trim($regs[1])]=trim($regs[2]);
}
return $Hash;
}
?>
Автоматическое подключение библиотекаря
Из листинга 29.2 можно видеть, что пока нам не удалось полностью избавиться от указания абсолютного пути к библиотекам. Вот строка, которая мне не нравится:
include "$DOCUMENT_ROOT/lib/librarian.phl"; // подключаем библиотекарь
Действуя привычным способом, нам придется вставлять ее в каждый сценарий, который планирует использовать библиотекаря. Этих сценариев может быть довольно много, так что если мы вдруг захотим изменить lib на, скажем, ../libraries, то придется править все программы. По закону Мэрфи где-нибудь да ошибетесь — обязательно. А значит, такое решение нам, как дотошным программистам, не подходит. К счастью, существует еще по крайней мере два способа решить проблему с абсолютными путями, и который из них выбрать — зависит от ситуации.
406 |
Часть V. Приемы программирования на PHP |
Здесь я хочу оговориться: разумеется, где-то все равно придется задать путь к библиотекарю, но такое место будет только одно, поэтому в случае нужды его легко модифицировать.
Способ первый: использование auto_prepend_file
Как следует из Приложения 2, PHP опирается при выполнении сценариев на специальный файл конфигурации под названием php.ini, в котором хранится большинство его настроек, заданных в виде директив. Кроме того, если PHP установлен как модуль Apache (а именно так обстоит дело у большинства хостинг-провайдеров), некоторые директивы можно также включать прямо в файлы .htaccess, управляющие работой сервера. Последние могут быть помещены в любой каталог, содержащий сценарии на PHP. Таким образом, для заданного каталога и всех его подкаталогов указанные настройки всегда будут действовать.
Помните, что для помещения директивы PHP с каким-нибудь именем NAME в файл .htaccess ее нужно назвать php_NAME, а значение отделить от имени не знаком =, как в php.ini, а пробелом. В противном случае Apache будет со- общать о неизвестной директиве в файле конфигурации.
Среди обрабатываемых интерпретатором директив есть две особенных. Называются они auto_prepend_file и auto_append_file. В первой задается абсолютный путь к файлу, содержащему код на PHP, который будет автоматически выполняться перед запуском любого сценария. Не правда ли, это то, что нам нужно?
Конечно, вставлять директиву auto_prepend_file в глобальный php.ini нет никакого смысла. Ведь у подавляющего большинства хостинг-провайдеров одни и те же Apache и PHP обслуживают сразу несколько виртуальных хостов, принадлежащих разным владельцам. А значит, никто не разрешит вам изменять глобальные настройки интерпретатора. В этом случае модификация файлов .htaccess оказывается единственно правильным и возможным решением. Правда, для этого нам нужно знать, какой физический каталог соответствует на нашем сервере корневому для документов. Выяснить это можно, например, с помощью такого простого сценария:
Листинг 29.4. Определение физического корневого каталога сервера
<?
echo $DOCUMENT_ROOT; ?>
Глава 29. Модульность программы. Написание "библиотекаря" |
407 |
Пусть, к примеру, у нашего хостинг-провайдера используется каталог /home/dk/www. Тогда для автоматического подключения библиотекаря ко всем сценариям на PHP нужно добавить в файл .htaccess примерно такую строку:
php_auto_prepend_file /home/dk/www/lib/librarian.phl
Вообще говоря, лучше всего сделать это в файле .htaccess, который нахо- дится в корневом каталоге сервера, для того чтобы подключение библиотекаря происходило ко всем сценариям во всех каталогах. Если этого файла не суще- ствует, необходимо его создать.
Как уже упоминалось, данный способ не подходит для того виртуального сервера для Windows, установка которого описана в части II настоящей книги. Изменение php.ini — тоже не очень удачная идея в силу вышеизложенных рассуждений. Тут нам на помощь придет второй способ, который мы сейчас и рассмотрим.
Способ второй: установка обработчика Apache
Установка своего обработчика сопряжена с несколько большими сложностями, чем использование директив auto_prepend_file и auto_append_file. Тем не менее,
он позволяет нам получить чуть больший контроль над сервером, поскольку перекладывает задачу выбора и запуска нужного сценария на плечи программиста. Это — установка нового обработчика Apache. Тема настолько важна, что мы, пожалуй, отложим на время нашего библиотекаря (к нему мы еще обязательно вернемся) и займемся непосредственно обработчиками.
Обработчики Apache
Итак, что же такое обработчик Apache? На самом деле мы постоянно сталкиваемся с одним из классических примеров обработчика. Да-да, вы уже догадались: это сам PHP. Если чуть углубиться в теорию, то обработчиком называется сценарий (возможно, встроенный в сам сервер, как это происходит с PHP), который запускается сервером при попытке пользователя открыть ту или иную страницу определенного типа.
Каждый обработчик должен иметь уникальный идентификатор — имя обработчика, который я для краткости буду называть просто именем. Оно может состоять только из алфавитно-цифровых символов и знаков подчеркивания. Заметьте, что это имя — не то же самое, что имя файла сценария, в котором хранится код обработчика. Имя обработчика и является тем, которое нужно указывать серверу в директиве AddHandler, когда мы хотим связать определенные документы с нашим сценарием.
408 |
Часть V. Приемы программирования на PHP |
Но как же сопоставить идентификатор обработчика тому сценарию, который содержит его код? У сервера Apache для этого есть специальная директива под названием Action. Где задается эта директива? В любом файле конфигурации Apache. Конечно, удобнее всего это делать в файле .htaccess, расположенном в корневом каталоге виртуального хоста, чтобы изменения распространились сразу на весь сервер. Мы уже рассматривали такую стратегию выше, только теперь все будет чуточку сложнее.
Вот требуемые директивы. Поместим их, как водится, в главный .htaccess-файл хоста.
#Сначала связываем имя обработчика с конкретным файлом.
#Знак "?" говорит серверу, что исходный URL запроса следует
#передать сценарию методом GET, т. е. через QUERY_STRING. Action libhandler "/lib/libhandler.php?"
#Теперь уведомляем сервер, документы какого типа мы желаем
#"пропускать" через наш обработчик.
AddHandler libhandler .html .htm
В этом фрагменте есть два тонких места.
rПуть к сценарию обработчика всегда указывается как абсолютный URL без указания имени хоста и порта. Мы не можем задать здесь ни путь к файлу, ни даже относительный URL. По той причине, чтобы позволить одному обработчику "передавать эстафету" другому. В самом деле, ведь это и происходит в нашем примере: Apache сначала определяет, что документ нужно "пропустить" через обработчик libhandler, а т. к. он представляет собой ни что иное, как сценарий на PHP, то и через интерпретатор PHP. В деталях затронутый процесс чуть сложнее, но мы не будем в него углубляться.
rПосле URL сценария в директиве Action следует знак ?. Зачем он? Это связано с механизмом, который использует Apache для того, чтобы определить конечный обработчик для того или иного документа. Когда пользователь посылает серверу URL, который, как Apache "знает", подходит под одну из команд Action, к этому URL слева просто присоединяется второй параметр директивы, и все начинается сначала — до тех пор, пока не будет найден последний обработчик. Например, если пользователь ввел /dir/file.html, то благодаря директиве Action указан-
ный адрес преобразуется в /lib/libhandler.php?/dir/file.html. Это — ни что иное, как адрес PHP-сценария с параметром, который будет передан программе, как обычно, через переменную окружения QUERY_STRING.
Теперь сервер знает, что все документы с расширением html и htm нужно обрабатывать при помощи сценария, расположенного по адресу /lib/libhandler.php. Точнее, при каждой попытке открыть страницы с указанными расширениями Apache будет запускать наш сценарий и в числе обычных переменных окружения отправлять ему несколько специальных, содержащих первичную информацию о запросе, переданном пользователем. Мы сейчас рассмотрим эти переменные на практике. Если вас интересует их полный список, попробуйте распечатать массив $GLOBALS или вос-
Глава 29. Модульность программы. Написание "библиотекаря" |
409 |
пользоваться функцией phpinfo(), вставив ее первой и единственной командой об-
работчика libhandler.php.
Вы, возможно, спросите, почему же мы не добавили в список расширений, на которые будет "реагировать" сценарий, еще одно — php? Давайте посмотрим, что бы произошло, поступи мы так. Предположим, пользователь хочет загру- зить страницу /a.php. Apache "видит", что расширение у нее — php, и запус- кает обработчик с именем /lib/libhandler.php. Точнее, он "сваливает" всю работу на сценарий libhandler.php. Еще точнее — перенаправляет
сервер по адресу /lib/libhandler.php?a.php! И тут же зацикливается,
потому что у этого сценария расширение — также php. Итак, сервер начинает вызывать сценарий снова и снова, все удлиняя его URL — до тех пор, пока не "поймет": что-то неверно, и пора аварийно завершаться, о чем и сообщает в файлах журнала. О том, как решить эту проблему, рассказано в самом конце главы.
Ну вот, у нас уже почти все готово. Осталось только написать сам код обработчика. Это не так уж и сложно. Но прежде давайте вспомним, зачем мы вообще связались с обработчиками. Для автоматической загрузки библиотекаря перед выполнением того или иного сценария, помните? Что же, вот пример (листинг 29.5).
Мы подразумеваем, что обработчик libhandler.php находится в том же са- мом каталоге, что и библиотекарь с большинством модулей. Это довольно удобно, поскольку позволяет нам задавать путь к каталогу с модулями лишь в единственном месте — в директиве Action файла .htaccess, да и то в виде относительного URL. Оцените, насколько это проще для будущих модифика- ций сайта.
Листинг 29.5. Обработчик /lib/libhandler.php с подключением библиоте- каря
<?
//Прежде всего, устанавливаем свои каталоги поиска модулей.
//Это, по нашей договоренности, — текущий в данный момент каталог. $INC[]=getcwd();
//Проверяем, не пытается ли пользователь запустить обработчик напрямую,
//минуя Apache — например, путем набора в браузере адреса
///lib/libhandler.php. Так как адрес, введенный пользователем,
//всегда передается в переменной окружения REQUEST_URI, то нужно
//"бить тревогу", если переданная строка адреса встречается
410 |
Часть V. Приемы программирования на PHP |
//в имени файла обработчика (причем в любом регистре символов).
//Мы не забыли отрезать в этой строке часть после ?, потому что
//она будет мешать при сравнении с именем файла.
//К сожалению, похоже, это единственный переносимый между операционными
//системами способ проверки легальности запуска обработчика.
$FileName=strtr(__FILE__,"\\","/"); $ReqName=ereg_Replace("\\?.*","",strtr(getenv("REQUEST_URI"),"\\","/")); if(eregi(quotemeta($ReqName),$FileName)) {
//Выводим сообщение об ошибке include "libhandler.err";
//Записываем в журнал данные о пользователе $f=fopen("libhandler.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"]= $GLOBALS["REQUEST_URI"]= getenv("QUERY_STRING")
); @putenv("QUERY_STRING=".
$GLOBALS["HTTP_ENV_VARS"]["QUERY_STRING"]= $GLOBALS["QUERY_STRING"]= ereg_Replace("^[^?]*\\?","",getenv("QUERY_STRING"))
);
//Подключаем библиотекарь include "librarian.phl";
//Здесь можно выполнить еще какие-нибудь действия...
//. . .
//Запускаем тот сценарий, который был запрошен пользователем chdir(dirname($SCRIPT_FILENAME));
include $SCRIPT_FILENAME; ?>