Самоучитель PHP 4 - Котеров Д. В.
.pdf
498 Часть V. Приемы программирования на PHP
$Prg=array(
"Todo" |
=> array($Root), |
// для накопления путей необработанных файлов |
"Res" |
=> array() |
// результат обработки всех файлов |
); |
|
|
//Пытаемся загрузить текущее состояние. Если не получается,
//значит, обход только что начался.
if($f=@fopen($cache,"rb")) { if(@flock($f,LOCK_SH)) {
$Prg=Unserialize(fread($f,filesize($cache))); fclose($f);
}
}
//Обходим сайт — по одной итерации цикла на каждый файл или
//каталог. Найденные файлы добавляются в конец массива
//$Prg['Res'], а подвергающиеся обработке — извлекаются из его
//начала. Таким образом, мы продолжаем процесс до тех пор,
//пока не будут "пройдены" все файлы на сервере.
do {
// очередное полное имя файла $fname=array_shift($Prg['Todo']);
//если это не файл и не каталог, пропускаем if(!@is_file($fname) && !@is_dir($fname)) continue;
//если это каталог, добавляем все его содержимое if(@is_dir($fname)) {
$Files=array();
for($d=openDir($fname); $e=readDir($d); ) { if($e=="."||$e=="..") continue; $Files[]="$fname/$e";
}
closeDir($d);
//вставляем в начало массива, чтобы на следующей итерации
//цикла обрабатывались именно эти файлы
$Prg['Todo']=array_merge($Files,$Prg['Todo']);
}
//вызываем функцию для обработки очередного файла или каталога $Func($fname,$Prg['Res']);
//выходим, если время истекло, или же необработанных
//файлов не осталось.
Глава 33. Разные советы |
499 |
} while(time()-$Start<$time && count($Prg['Todo']));
//Вернуть текущий результат в $Result. $Result=$Prg['Res'];
//Если еще есть файлы для обработки, сохранить состояние. if(count($Prg['Todo'])) {
//Сохраняем текущее состояние. В следующий раз мы начнем с него. $f=fopen($cache,"a+b");
flock($f,LOCK_EX); ftruncate($f,0); fwrite($f,Serialize($Prg)); fflush($f); fclose($f);
return true; // процесс продолжается
}
// Иначе процесс закончился. Удалить файл состояния. @unlink($cache);
return false;
}
?>
Я не буду приводить здесь реальный сценарий для построения карты сервера, потому что он слишком велик и, к тому же, довольно однообразен и неинтересен. Вся "изюминка" заключена именно в функции WalkSite(). Листинг 33.2 содержит небольшую "демонстрацию" ее возможностей. Сценарий собирает сведения о размере каждого файла сайта, печатая на каждом этапе имена обработанных объектов, а затем выводит сводную информацию.
Листинг 33.2. Демонстрация возможностей функции WalkSite(): demo.php
<?
//Подключаем библиотекаря "прямым" способом. include "$DOCUMENT_ROOT/php/Librarian.phl";
//Подключаем модуль с функцией WalkSite(). Uses("SiteWalker");
//Эта функция будет вызываться для каждого файла на сервере.
//Ее задача — добавить обработанные данные из этого файла
//в массив $Result (формат определяется назначением этих данных). function Walk($fname,&$Result)
{ // для диагностики выводим имя файла
500 |
Часть V. Приемы программирования на PHP |
print ">$fname<br>";
// в качестве примера — просто добавляем имя файла в массив $Result[]="$fname: <b>".filesize($fname)."</b>";
}
// Если WalkSite() вернула false, значит, процесс закончился. if(!WalkSite($DOCUMENT_ROOT,"Walk","map",0,$Result)) {
//В качестве примера просто выводим содержимое массива,
//сформированного вызовами функции Walk(). Реальный код
//должен был бы вырабатывать HTML-представление карты,
//данные которой накоплены в $Result.
print "<hr>";
print join("<br>\n",$Result);
}else {
//для примера заставляем страницу обновить саму себя,
//имитируя многократные посещения пользователей.
print "<meta http-equiv=refresh content='0; url=$SCRIPT_NAME'>";
}
?>
В этом сценарии функции WalkSite() передается 0 как значение размера кванта времени, в течение которого можно собирать данные о сайте. Это означает, что файлы будут обрабатываться по одному при каждом запросе. В реальном коде карты сервера, конечно, это не так — нужно указывать приемлемый промежуток времени, чтобы в него "уложилась" обработка сразу нескольких страниц. Чем меньше будет этот промежуток, тем менее заметным для пользователя станет замедление, связанное с работой сценария, но тем значительнее будут "накладные расходы", вызванные работой функций сериализации. Так что тут нужно выбирать некоторый "средний" вариант. Проще всего это сделать опытным путем — например, так, чтобы примерно за час при известной посещаемости успевала перестроиться вся карта сервера.
Функция WalkSite() из листинга 33.2 работает с файлами, устанавливая на них рекомендательные блокировки. Этот процесс хоть и позволяет обойти про- блемы с разделением доступа к файлам, немного сложен для понимания. Он подробно описан в главе 15 части IV.
Использование самопереадресации
Термин самопереадресация (или, в английском варианте, self-redirect) означает свойство сценария подавать в браузер клиента запрос, заставляющий его (браузер) заново
Глава 33. Разные советы |
501 |
выполнить и загрузить этот сценарий с сервера. Звучит, как языческое заклинание, не правда ли? Пожалуй, с первого взгляда не совсем ясно, зачем же может понадобиться эта хваленая самопереадресация в Web-программировании.
Рассмотрим пример. Предположим, у нас имеется сценарий — гостевая книга наподобие той, эскиз которой мы рассматривали в главе 30. С точки зрения пользователя сценарий представляет собой страницу с адресом http://www.ourserver.ru/book/index.html. Если набрать этот адрес в браузе-
ре, появится, во-первых, форма с предложением добавить новое сообщение в книгу, а во-вторых, список ранее добавленных "посланий". В атрибуте action тэга <form> указан адрес той же самой страницы index.html (это вписывается в трехуровневую схему разработки сценариев), поэтому после набора сообщения и нажатия на кнопку отправки фактически снова загружается та же самая страница. Только перед ее загрузкой генератор данных гостевой книги определяет, что необходимо добавить новую запись, и делает это.
В общем-то, довольно стандартная схема. Пусть пользователь набрал свое послание и отправил его на сервер. Перед ним появится список сообщений, первым из которых будет его собственное. Пока вроде бы все верно. И теперь пользователь, ничего не подозревая, нажимает на кнопку Обновить в браузере, заставляя последний, как он думает, перезагрузить страницу гостевой книги.
Но в действительности происходит совсем не то, что он ожидает. Если данные формы были посланы методом POST, браузер выведет на экран диалоговое окно запроса примерно такого содержания: "Вы пытаетесь обновить данные страницы, которая была сгенерирована с применением метода POST. Повторить отправку данных (да или нет)?" Если пользователь нажмет кнопку Нет, то гостевая книга не перезагрузится, а появится совершенно бесполезная стандартная страница с сообщением о том, что "данные устарели". Если же он подтвердит вторичную отправку данных, его сообщение будет добавлено в книгу еще раз, а потому "размножится". Довольно нетрудно понять, почему так происходит: ведь браузер "не знает", что в действительности пользователь хочет лишь вторично "зайти" на адрес страницы книги, а не повторить отправку всех данных формы.
Однако ситуация становится еще плачевнее, если мы применяем в нашей гостевой книге метод GET. В этом случае при нажатии на кнопку Обновить браузер "без лишних разговоров" пошлет данные формы на сервер повторно, так что сообщение будет лишний раз добавлено в гостевую книгу без предупреждений. И это тоже понятно: ведь метод GET — не что иное, как простое изменение URL страницы, а именно, добавление в его конец символа ?, после которого следуют параметры (в том числе текст записи).
Впрочем, метод GET практически никогда не применяется в интерактивных сце- нариях, таких как гостевые книги, форумы и т. д. Мы уже говорили в первой части книги на эту тему, но она настолько важна, что я повторюсь. Если для
502 |
Часть V. Приемы программирования на PHP |
одних и тех же данных формы при их многократной отправке страница все- гда выглядит одинаково, значит, эти данные логично передавать методом GET. В противном случае необходимо применять метод POST. Такое поло-
жение вещей связано также и с тем, что некоторые proxy-серверы могут кэши- ровать страницы, полученные методом GET, но они никогда не кэшируют их при использовании POST.
Самопереадресация — это как раз то средство, которое позволяет разрешить рассмотренный конфликт в сторону пользователя. В самом деле, предположим, что при получении уведомления о новом сообщении генератор данных вставляет их в базу данных, а затем посылает браузеру заголовок, заставляющий его перезагрузить страницу гостевой книги. В этом случае страница уже не будет представлять собой результат работы метода POST, это будет обычный HTML-документ, загруженный с сервера, как будто бы пользователь считал файл только что самостоятельно и "вручную". Неудивительно, что кнопка браузера Обновить будет работать так, как ей и положено.
Впрочем, при использовании самопереадресации очень легко наткнуться на один неприятный "подводный камень". Это — ошибка некоторых версий браузера Netscape, заключающаяся в том, что любые страницы, полученные им в результате самопереадресации, он ошибочно принимает за пустые (и соответственно отображает). И все же выход есть: достаточно немного модифицировать URL страницы, чтобы браузер "подумал", что это уже другой документ, а не тот же самый. Листинг 33.3 показывает, как это можно сделать. В целях экономии места я разместил шаблон страницы и генератор данных в одном файле.
Листинг 33.3. Самопереадресация
<?
// Считываем содержимое базы данных. $Book=@Unserialize(join("",File("book.dat"))); if(!$Book) $Book=array();
// Проверяем, не нужно ли добавить запись...
if(@$Go) { array_unshift($Book,$Text); $f=fopen("book.dat","w"); fwrite($f,Serialize($Book)); fclose($f);
//Внимание! Самопереадресация. Обратите внимание на то,
//какой заголовок мы посылаем.
Header("Location: http://$HTTP_HOST$REQUEST_URI?".time()); exit; // Завершить сценарий.
}
Глава 33. Разные советы |
503 |
?>
<form action=sr.php method=post>
Введите текст:<br>
<input type=text name=Text><br>
<input type=submit name=Go value="Go!"> </form>
<?foreach($Book as $k=>$v) {?> <?=$v?>
<hr>
<?}?>
Мы обеспечиваем "уникальность" URL страницы гостевой книги за счет добавления в его конец текущего времени в секундах, прошедших с 1 января 1970 года (так называемый Unix timestamp). Вряд ли пользователь будет обновлять страницу чаще, чем раз в секунду, поэтому такой способ прекрасно подходит для наших целей.
Обратите внимание на то, что в заголовке Location мы передаем полный URL страницы, включая имя хоста. Большинство браузеров умеют "понимать" и сокращенные пути (например, без указания имени сервера), но некоторые — нет, так что лучше не искушать судьбу.
Запрет кэширования страниц
Изрядное количество сценариев генерируют страницы, которые постоянно изменяются во времени, поэтому кэширование таких документов, которое иногда пытаются провести "слишком умные" браузеры и proxy-серверы, следует отключить. В противном случае пользователь может увидеть устаревшие данные и не заметить, что ваша страница изменилась.
Вообще говоря, если браузер "захочет" сохранять страницу в кэше и затем постоянно выдавать пользователю одно и то же, никакая сила не сможет запретить ему делать это. К счастью, большинство браузеров более "послушны" — они адекватно реагируют на специальные заголовки запрета кэширования, которые могут присутствовать в странице, полученной с сервера. То же самое делают и proxy-серверы — правда, они используют уже другие заголовки.
В листинге 33.4 приведены четыре заголовка, которые необходимо послать вместе с телом страницы, чтобы браузеры и proxy-серверы не пытались ее кэшировать. Опыт подтверждает, что эти 4 заголовка — минимум. Если убрать хотя бы один из них, некоторые proxy-серверы (или браузеры) могут "не понять", что от них требуется.
Листинг 33.4. Заголовки для запрета кэширования
Header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); |
// Дата в прошлом |
504 Часть V. Приемы программирования на PHP
Header("Last-Modified: ".gmdate("D, d M Y H:i:s")."GMT"); // Изменилась
Header("Cache-Control: no-cache, must-revalidate"); |
// |
для |
HTTP/1.1 |
Header("Pragma: no-cache"); |
// |
для |
HTTP/1.0 |
Излишне напоминать, что все заголовки должны быть отправлены до первой команды вывода в сценарии.
При использовании шаблонизатора наподобие того, который был описан в главе 30, это требование является необязательным. В таком случае весь ре-
зультат работы сценария и шаблона буферизируется и не отправляется в браузер до самого последнего момента.
Несколько слов о флажках checkbox
Переключатель с независимым выбором (checkbox или более коротко — флажок) имеет одну довольно неприятную особенность, которая иногда может помешать Webпрограммисту. Вы, наверное, помните, что когда перед отправкой формы пользователь установил его в выбранное состояние, то сценарию в числе других параметров приходит пара имя_флажка=значение.
В то же время, если флажок не был установлен пользователем, указанная пара не посылается. Часто это бывает не совсем то, что нужно. Мы бы хотели, чтобы в невыбранном состоянии флажок также присылал данные, но только значение было равно какой-нибудь специальной величине — например, нулю или пустой строке.
К нашей радости, добиться этого эффекта в PHP довольно несложно. Достаточно воспользоваться одноименным скрытым полем (hidden) со значением, равным, например, нулю, разместив его перед нужным флажком. Вот пример:
Листинг 33.5. Гарантированная установка значений флажков
<? if(@$Go) {
foreach($Known as $k=>$v)
if($v) echo "Вы знаете язык $k!<br>"; else echo "Вы не знаете языка $k. <br>";
}
?>
<form action=lang.php method=post>
Какие языки программирования вы знаете?<br> <input type=hidden name=Known[PHP] value=0>
<input type=checkbox name= Known[PHP] value=1>PHP<br>
Глава 33. Разные советы |
505 |
<input type=hidden name=Known[Perl] value=0>
<input type=checkbox name= Known[Perl] value=1>PHP<br> <input type=submit name=Go value="Go!">
</form>
Теперь в случае, если пользователь не выберет какой-нибудь из флажков, браузер отправит сценарию пару Known[язык]=0, сгенерированную соответствующим скрытым полем, и в массиве $Known создастся соответствующий элемент. Если пользователь выбрал флажок, эта пара также будет послана, но сразу же после нее последует пара Known[язык]=1, которая "перекроет" предыдущее значение.
Не включи мы скрытые поля в форму из листинга 33.5, сценарий печатал бы только сообщения о тех языках, которые "знает пользователь", пропуская языки, ему "неизвестные". В нашем же случае сценарий реагирует и на неустановленные флажки.
Такой способ немного увеличивает объем данных, передаваемых методом POST, за счет тех самых пар, которые генерируются скрытыми полями. Впро- чем, в реальной жизни это "увеличение" практически незаметно (особенно для
POST-форм).
ЧАСТЬ VI.
ПРИЛОЖЕНИЯ
