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

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

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

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.

ПРИЛОЖЕНИЯ