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

Самоучитель PHP 4 - Котеров Д. В.

.pdf
Скачиваний:
99
Добавлен:
24.05.2014
Размер:
4 Мб
Скачать
☆

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, когда мы хотим связать определенные документы с нашим сценарием.