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

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

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

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