САМОУЧИТЕЛЬ PHP 4

Библиотекарь


Ту часть кода, которая будет содержать функцию 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. Однако изменять имеющиеся переменные напрямую он все же способен.


Содержание раздела