Самоучитель PHP 4 - Котеров Д. В.
.pdf
398 |
|
Часть V. Приемы программирования на PHP |
|
|
|
|
|
|
Возможно, этот разговор о каталогах выглядит с первого взгляда несколько надуманно, однако люди, уже столкнувшиеся когда-либо с рассматриваемой проблемой, по достоинству оценят затраченные усилия, особенно если их сце- нарии состоят из десятков файлов и библиотек.
Помните, что при помощи include или require нельзя один и тот же файл загружать дважды (как это часто бывает, если один модуль вызывает другой, но программа об этом "не знает" и еще раз подключает первый — опять же, стандартный случай). В самом деле, если в этом файле находится, к примеру, описание какой-нибудь функции, то при следующем его включении PHP выдаст ошибку: повторное объявление функции. Конечно, последняя проблема полностью решается подстановкой include_once вместо include, что работает, кстати, только в PHP версии 4.
Отсюда мы можем сформулировать главные два требования.
rМеханизм загрузки модуля должен сам решать, в каком каталоге располагается модуль, независимо от того, где выполняется сценарий. В любой программе возможность загрузить указанный по имени модуль должна быть легко осуществима. Мы хотели бы, чтобы это было так же просто, как мы делаем это с обычными файлами из текущего каталога при помощи include.
rОдин и тот же модуль не должен загружаться дважды, даже если программа попытается это выполнить.
К слову сказать, оба требования реализованы, например, в языке Perl.
Как я уже говорил, мы можем написать нужную нам "инструкцию", которая будет загружать модуль с применением указанных принципов прямо на PHP. Назовем ее Uses() и оформим в виде функции.
Далее для краткости модулем на PHP я буду называть файл (например, с рас- ширением phl), содержащий некоторые общеупотребительные функции, кон- станты и переменные, а также исполняемую часть, которая запускается при первой (и только первой) загрузке модуля.
Библиотекарь
Ту часть кода, которая будет содержать функцию Uses() (а мы реализуем ее именно в виде функции) и другие функции, нужные для загрузки модулей, назовем библио- текарем. Этот библиотекарь, очевидно, сценарию придется загружать первым, а каким именно образом, мы поговорим чуть позже.
Глава 29. Модульность программы. Написание "библиотекаря" |
399 |
Теперь немного о том, как мы будем реализовывать Uses(). Это довольно несложно. Помните, я подчеркивал, что поскольку PHP является интерпретатором, то на нем осуществимы такие приемы, как описание функций внутри функций и многое другое. Так мы и сделаем: функция Uses() вначале будет проверять, не загружался ли уже модуль с таким именем, затем искать затребованный модуль в специальных "каталогах для модулей", фиксировать во внутреннем массиве факт, что указанный файл загружен, и, наконец, вызывать include_once для файла с модулем. Кроме того, на время загрузки текущий каталог будет сменяться на тот, в котором находится модуль, чтобы стартовые части всех модулей запускались в "своих" каталогах. Это как раз та возможность, которая отсутствует в Perl, и которая оказывается довольно удобной на практике.
Раз библиотекарь всегда подключается к программе в первую очередь, разумно доверить ему выполнение еще некоторых действий.
rПоместим в файл библиотекаря функции, чаще всего необходимые почти каждому сценарию. Таким образом, мы как бы "расширим" набор встроенных в PHP функций. Однако помните, что встроенные функции переопределять все же нельзя, можно лишь создавать новые с уникальными именами.
rБиблиотекарь, как никто другой, должен приложить максимум усилий, чтобы сделать сценарии переносимыми с одной платформы на другую. Для нас это будет заключаться в корректировке некоторых переменных, которые PHP создает перед выполнением программы. Первым кандидатом на такую правку будет $SCRIPT_NAME (а также одноименная переменная окружения), которая, как мы знаем, в Windows-версии PHP содержит не то значение, которое мы ожидаем.
rИ еще нам хочется, чтобы на момент загрузки модуля текущий каталог сменялся на тот, в котором расположен файл модуля. Таким образом, стартовая часть библиотеки всегда сможет определить, где она находится, — например, при помощи вызова getcwd().
Вот что у нас получится в результате:
Листинг 29.1. Библиотекарь: librarian.phl
<?if(!defined("LIBRARIAN_LOADED")) { define("LIBRARIAN_LOADED",1);
//Расширение библиотечных файлов по умолчанию define("LibExt","phl");
//Пути поиска библиотек. Если начинаются с точки, то поиск
//ВСЕГДА ведется относительно текущего каталога, даже если его
//сменят, в противном случае при следующем вызова Uses() будет
//выполнен перевод пути в абсолютный.
$INC=array(".","./lib");
400 |
Часть V. Приемы программирования на PHP |
//Функция преобразует указанный относительный путь в абсолютный.
//Если путь уже является абсолютным (т. е. отсчитывается от корневого
//каталога системы), то с ним ничего не происходит, в противном случае
//используется имя текущего каталога (или заданного в $cur) с
//необходимыми преобразованиями. Существование файла с полученным полным
//именем не проверяется. Функция лишена некоторых недостатков
//встроенной в PHP realpath() и имеет по сравнению с ней несколько
//большие возможности, работая, правда, чуть медленнее.
function GetAbsPath($name,$cur="") { return abs_path($name,$cur); } function abs_path($name,$cur="")
{// Преобразуем обратные слэши в прямые $name=strtr(trim($name),"\\","/");
//Сначала разбиваем путь по знакам "/" $Parts=explode("/",$name);
$Path=($cur===""?getcwd():$cur); // начальный каталог поиска foreach($Parts as $i=>$s) if($s!=".") {
//Признак корневого каталога?
if(!$i && (strlen($s)>1&&$s[1]==":"||$s=="")) $Path=$s; // Ссылка на родительский каталог?
elseif($s=="..") {
//Если это уже корневой каталог, то куда спускаться?.. if(strlen($Path)>1 && $Path[1]==":") continue;
//Иначе используем dirname()
$p=dirname($Path);
if($p=="/"||$p=="\\"||$p==".") $Path=""; else $Path=$p;
}
// Иначе просто имя очередного каталога elseif($s!=="") $Path.="/$s";
}
return ($Path!==""?$Path:"/");
}
//Преобразует URL в абсолютный файловый путь.
//Т. е. если адрес начинается со слэша, то результат рассматривается
//по отношению к каталогу DOCUMENT_ROOT, а если нет — то относительно
//dirname($SCRIPT_NAME). Конечно, функция не безупречна (например, она
//не умеет обрабатывать URL, заданные Alias-директивами Apache, но в
//большинстве случаев это и не нужно.
Глава 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, когда мы хотим связать определенные документы с нашим сценарием.
