Библиотекарь
Ту часть кода, которая будет содержать функцию Uses() (а мы реализуем ее именно в виде функции) и другие функции, нужные для загрузки модулей, назовем библиотекарем. Этот библиотекарь, очевидно, сценарию придется загружать первым, а каким именно образом, мы поговорим чуть позже.
Теперь немного о том, как мы будем реализовывать 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");
// Функция преобразует указанный относительный путь в абсолютный.
// Если путь уже является абсолютным (т. е. отсчитывается от корневого
// каталога системы), то с ним ничего не происходит, в противном случае
// используется имя текущего каталога (или заданного в $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, но в
// большинстве случаев это и не нужно.
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();
// Теперь просматриваем пути, начиная с того, по которому была
// найдена какая-нибудь предыдущая загруженная библиотека. Скорее
// всего, там окажется загружаемый сейчас модуль. Если нет —
// что же, просмотрим весь список...
$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"))
);
// На всякий случай включаем максимальный контроль ошибок
Error_reporting(1+2+4+8);
// ВНИМАНИЕ! После следующего закрывающего тэга
// не должно быть НИКАКИХ ПРОБЕЛОВ! В противном случае
// сценарий, подключающий библиотекаря, будет выводить в самом
// начале своей работы этот пробел, что недопустимо при
// работе с Cookies.
}?>
Обратите внимание на то, что весь код библиотекаря помещен в блок оператора if. Это сделано специально, чтобы при возможной (ошибочной) повторной загрузке библиотекаря по include все работало корректно.
Возможно, вы скажете, что то же самое можно было бы сделать и в модулях, и обойтись вообще без библиотекаря. Однако это приведет к заметной потере производительности, потому что интерпретатору каждый раз придется загружать и разбирать весь файл модуля, а это — основное время при запуске программы.
Пожалуй, в приведенном коде есть и еще одно интересное место. Я имею в виду инструкции, помеченные комментарием: "Делаем доступными для модуля все глобальные переменные". Зачем это нужно? Разве глобальные переменные по определению не доступны подключаемому модулю? К сожалению, это так, и вот почему. Мы вызываем include_once в теле функции Uses(), а не в глобальном контексте. Неудивительно, что подключенный файл работает не в нем, а в области видимости тела функции. Указанный цикл перебора всех глобальных переменных и их "глобализация"
с помощью global решает проблему.
Здесь есть еще одна тонкость. Если модуль "захочет" определить какую-либо новую глобальную переменную, он не сможет сделать это никак иначе, чем через массив $GLOBALS. Однако изменять имеющиеся переменные напрямую он все же способен.