Проблема с кодировками
Думаю, не надо напоминать, что русских кодировок существует великое множество. Поэтому от того, насколько умело вы перекодируете письмо перед его отсылкой, зависит, прочтет ли его получатель или, махнув рукой, отправит в корзину, даже не попытавшись установить в своем почтовом клиенте нужную кодировку. Эта глава как раз и призвана раз и навсегда решить проблему кодировок. Если вы воспользуетесь советами, изложенными в ней, ваши письма всегда будут читаемыми.
Проблема с заголовками
Есть одна проблема, возникающая при подобном использовании заголовка Content-type. Дело в том, что существуют почтовые программы, которые понимают заголовок Content-type, но не понимают русский текст в поле Subject, если это поле стоит до Content-type. В то же время, другие почтовые клиенты обязывают нас задавать Content-type последним заголовком в списке. Чтобы обойти этот заколдованный круг, проще всего разместить поле Content-type сразу в двух местах— перед первым и после последнего заголовка:
$message=
"Content-type: text/plain; charset=koi8-r
From: Лист ðàññûëêè
To: Èâàíîâ Èâàí Èâàíîâè÷ <ivanov@ivan.ivanovich.ru>
Subject: Ïðîáíàÿ ðàññûëêà
Content-type: text/plain; charset=koi8-r
Óâàæàåìûé òîâàðèù! Ýòî ïèñüìî ïîñëàíî ïî÷òîâûì ðîáîòîì.
Âñåãî õîðîøåãî!";
$message=convert_cyr_string($message,"w","k");
Mail("ivanov@ivan.ivanovich.ru","",$message);
Да, это может показаться весьма искусственным приемом, но зато работает "на ура". Теперь вы можете быть уверены, что ваше письмо прочитает любой пользователь (особенно если оно послано в кодировке KOI8), даже если его почтовая программа вообще не настроена ни на какую кодировку. Можете похвастать этим достижением перед начальством, предложив ему поставить у себя в Outlook Express по умолчанию японскую кодировку, а затем попросив принять письмо, сгенерированное роботом по указанной схеме.
Проблемы с отладкой
В последней версии PHP на момент написания этих строк имелось небольшое неудобство, которое может превратить отладку программ, использующих буферизацию, в сущий ад. Дело в том, что при включенной буферизации все предупреждения, в нормальном состоянии генерируемые PHP, записываются в буфер, а потому (если программа не отправляет буфер в браузер) могут потеряться. К счастью, это касается лишь предупреждений, которые не завершают работу сценария немедленно. Фатальные ошибки отправляются в браузер почти всегда. Неприятность как раз и состоит в этом "почти". Даже фатальные ошибки останутся программистом незамеченными, если он вызывает функцию ob_start() вложенным образом, — т. е. более одного раза, как это было описано в предыдущем абзаце. Например, если в листинге 30.15 после присваивания текста элементу массива $A[2] вставить вызов несуществующей функции, программа сразу же выдаст пользователю текущее содержимое буфера номер 1, а затем, "не сказав ни слова", завершится. Это и понятно: ведь сообщение об ошибке попало во второй буфер, а значит, было проигнорировано! Почему разработчики PHP, вопреки общеизвестной практике, не разделили стандартный выходной поток и поток ошибок, остается неясным.
Если вы заметили, шаблонизатор всегда использует не более одного буфера "перехвата" в каждый момент времени. Это сделано именно из соображений упрощения отладки сценариев. И все же, если нефатальное предупреждение было сгенерировано в момент обработки блока, который по каким-то причинам не входит в шаблон страницы, оно останется незамеченным программистом. Впрочем, наверное, в этом нет ничего страшного: раз блок не выводится, значит, нам все равно, правильно он отработан или нет….
Проблемы со сложными именами
Но не все так восхитительно, как может показаться на первый взгляд. Беда в том, что описанный механизм работает замечательно, лишь когда мы задействуем элементы одномерных массивов в качестве имен полей формы. В случае же многомерных массивов дела обстоят несколько хуже. Правда, многомерные массивы используются при закачке значительно реже, но все равно, мой долг — предупредить вас и уберечь от возможного недоразумения.
Итак, напишем форму:
<form action="script.php" method=POST enctype=multipart/form-data>
<input type=file name="File[a][b]">
<input type=submit>
</form>
При приеме данных такой формы PHP "запутается"
и, хотя и создаст массив $File, но не поместит в него никаких полезных данных. А именно, в моей версии 4.0.3pl1 в элемент с ключом a вместо имени файла попадает какое-то комплексное (судя по его странному виду) число.) Надеюсь, в будущих версиях интерпретатора это досадное недоразумение будет исправлено.
Но все же существует метод, с помощью которого мы сможем обработать и такие "неправильные"
с точки зрения PHP формы. Я об этом еще не упоминал, но PHP, помимо установки вышеперечисленных переменных, создает также глобальный массив с именем $HTTP_POST_FILES. Как показывает практика, в этом массиве содержатся верные данные, какое бы имя не имело поле закачки в форме.
Массив $HTTP_POST_FILES создается не всегда, а только в том случае, если в настройках PHP задействован параметр track_vars. Так как, судя по документации, в PHP версии 4 он включен всегда (чего нельзя сказать о третьей версии), то беспокоиться не о чем.
Массив $HTTP_POST_FILES
используется довольно редко, так что я предоставляю читателю возможность самостоятельно разобраться, в каком формате хранятся в нем данные. Это несложно. Вам не потребуется ничего, кроме функции Dump(), которая уже упоминалась в этой главе, и, конечно, желания экспериментировать.
Простые имена полей закачки
Как я уже говорил, интерпретатору совершенно все равно, в каком формате приходят данные из формы. Он умеет их обрабатывать и "рассовывать" по переменным в любом формате. Однако данные одного специального поля формы— а именно, поля закачки — он интерпретирует особым образом. Пусть у нас есть multipart-форма, а в ней — поле закачки файла:
<form action="script.php" method=POST enctype=multipart/form-data>
<input type=file name="MyFile">
<input type=submit>
</form>
После выбора в этом поле нужного файла и отправки формы (и загрузки на сервер того файла, который был указан) PHP определит, что нужно принять файл, и сохранит его во временном каталоге на сервере. Кроме того, в программе создадутся несколько переменных.
r $MyFile — имя временного файла на машине сервера, который содержит данные, переданные пользователем. С этим файлом теперь можно вытворять все что угодно: удалять, копировать, переименовывать, ñíîâà óäàëÿòü...
r $MyFile_name — исходное имя файла, которое он имел до своей отправки на сервер.
r $MyFile_size — размер закачанного файла в байтах.
r $MyFile_type — тип загруженного файла, если браузер смог его определить. К примеру, image/gif, text/html
и т. д.
Как видим, префикс у всех созданных переменных один и тот же — MyFile_. Этот префикс состоит из имени элемента закачки в форме, к которому присоединен знак _.
Теперь мы можем, например, скопировать только что полученный файл на новое место, при помощи Copy($MyFile,"uploaded.dat") или других средств, проверив предварительно, не слишком ли он велик, основываясь на значении переменной $MyFile_size.
Настоятельно рекомендую использовать функцию копирования, а не переименования/перемещения. Дело в том, что в некоторых операционных системах временный каталог, в котором PHP хранит только что закачанные файлы, может находиться на другом носителе, и в результате операция переименования завершится с ошибкой. Хотя мы и оставили копию полученного файла во временном каталоге, можно не заботиться о его удалении в целях экономии места: PHP сделает это автоматически.
Если процесс окончится неуспешно, вы сможете определить это по отсутствию файла, имя которого задано в $MyFile, или же по отсутствию самой этой переменной в программе.
Простые символы
Класс простых символов, действительно, самый простой. А именно, любой символ в строке на RegEx обозначает сам себя, если он не является управляющим (к управляющим символам причисляются следующие: ".*?+[]{}|$^"). Например, регулярное выражение abcd будет "реагировать"
на строки, в которых встречается последовательность abcd.
Протокол
Часть URL, предваряющая имя хоста и завершающаяся двумя косыми чертами (в нашем примере http://), указывает браузеру, какой высокоуровневый протокол нужно использовать для обмена данными с Web-сервером. Обычно это HTTP, но могут поддерживаться и другие протоколы. Например, протокол HTTPS позволяет передавать информацию в специальном зашифрованном виде, чтобы злоумышленники не могли ее перехватить, — конечно, если Web-сервер способен с ним работать. Нужно заметить, что все подобные протоколы базируются на сервисе, предоставляемом TCP, и по большей части представляют собой лишь набор текстовых команд. В следующей главе мы убедимся в этом утверждении, разбирая, как работает протокол HTTP.
Протоколы передачи данных
Как и любая компьютерная сеть, Интернет основан на множестве компьютеров, соединенных друг с другом проводами, через спутниковый канал связи ит. д. Однако, как известно, одних проводов для передачи информации недостаточно — передающей и принимающей сторонам необходимо также придерживаться ряда соглашений, позволяющих строго регламентировать передачу данных, а также гарантировать, что эта передача пройдет без искажений. Такой набор правил называется протоколом передачи. Грубо говоря, протокол — это набор правил, который позволяет системам, взаимодействующим в рамках Интернета, обмениваться данными в наиболее удобной для них форме. Следуя сложившейся в книгах подобного рода традиции, я вкратце расскажу, что же из себя представляют основные протоколы Интернета.
Иногда я буду называть Интернет Сетью с большой буквы, в отличие от "сети" с маленькой буквы, которой обозначается вообще любая сеть, локальная или глобальная. Тут ситуация сходна со словом "галактика": наша галактика называется Галактикой с прописной буквы, а "галактика" cо строчной буквы соответствует любой другой звездной системе подобных размеров. На самом деле, сходство Сети и Галактики идет несколько дальше орфографии, и, я думаю, вы скоро также проникнетесь этой мыслью.
Необходимость некоторой стандартизации возникла чуть ли не с самого момента возникновения компьютерных сетей. Действительно, подчас одной сетью объединены компьютеры, работающие под управлением не только различных операционных систем, но нередко имеющие и совершенно различную архитектуру процессора, организацию памяти и т. д. Именно для того, чтобы обеспечивать возможность передачи между такими компьютерами, и предназначены всевозможные протоколы. Давайте рассмотрим этот вопрос чуть подробнее.
Разумеется, для разных целей существуют различные протоколы. К счастью, нам не нужно иметь представление о каждом из них — достаточно знать только тот, который мы будем использовать в Web-программировании. Таковым для нас является протокол TCP (Transmission Control Protocol — Протокол управления передачей данных), а точнее, протокол HTTP (Hypertext Transfer Protocol — Протокол передачи гипертекста), базирующийся на TCP. Протокол HTTP как раз и задействуется браузерами и Web-серверами.
Заметьте, что уже в самом начале первой главы я упомянул о том, что один протокол может использовать в своей работе другой. В мире Интернета эта ситуация является совершенно обычной. Чаще всего каждый из протоколов, участвующих в передаче данных по сети, реализуется в виде отдельного и по возможности независимого программного обеспечения или драйвера. Среди них существует некоторая иерархия, когда один протокол является всего лишь "надстройкой"
над другим, тот, в свою очередь — над третьим, и т. д. до самого "низкоуровневого"
драйвера, работающего уже непосредственно на физическом уровне с сетевыми картами или модемами. На рис. 1.1 приведена примерная схема того, что происходит при отправке запроса браузером пользователя на некоторый Web-сервер в Интернете. Прямоугольниками обозначены программные компоненты: драйверы протоколов и программы-абоненты (последние выделены жирным шрифтом), направление передачи данных указано стрелками. Конечно, в действительности процесс гораздо более сложен, но нам сейчас нет необходимости на этом останавливаться.
Обратите внимание, что в пределах каждой системы протоколы на схеме расположены в виде "стопки", один над другим. Такая структура обуславливает то, что часто семейство протоколов обмена данными в сети Интернет называют стеком TCP/IP (стек в переводе с английского как раз и обозначает "стопку").
Рис. 1.1. Организация обмена данными в Интернете
Каждый из протоколов в идеале "ничего не знает" о том, какой протокол "стоит над ним". Например, протокол IP (который обеспечивает несколько более простой сервис по сравнению с TCP) не использует возможности протокола TCP, а TCP, в свою очередь, "не догадывается" о существовании протокола HTTP (именно его задействует браузер и понимает Web-сервер, на схеме протокол HTTP
не обозначен).
Применение такой организации позволяет заметно упростить ту часть операционной системы, которая отвечает за поддержку работы с сетью. А я тем временем прошу вас не пугаться. Нас будет интересовать в конечном итоге всего лишь протокол самого высокого уровня, "возвышающийся" над всеми остальными протоколами, т. е. HTTP и то, как он взаимодействует с протоколом TCP.
Провайдер
Провайдер — организация, имеющая несколько модемных входов, к которым могут подключаться пользователи для доступа в Интернет. Все это обычно происходит не бесплатно (для пользователей, разумеется).
Проверка CGI
В каталоге z:/home/localhost/cgi, предназначенном для хранения CGI-сценариев, создайте файл test.bat с таким содержанием:
Листинг 4.2. Файл test.bat
@echo off
echo Content-type: text/html
echo.
echo.
Dir
Далее в браузере наберите:
http://localhost/cgi-bin/test.bat
В окне отобразится результат команды DOS dir.
Нужно отметить, что последний пример работает не под всеми версиями Windows: иногда вместо того, чтобы выполнить файл test.bat, Apache выводит в браузер его содержимое. С чем это связано — не совсем ясно, однако, кажется, можно избавиться от указанной ошибки путем манипулирования с реестром Windows. Если у вас test.bat не запускается, не расстраивайтесь: вряд ли вы когда-нибудь будете писать сценарии в виде bat-файлов, тем более, что этот способ несовместим с Unix.
Если что-то пошло не так, либо окно Apache открывается и тут же закрывается, значит, где-то произошла ошибка — скорее всего, причины ее возникновения можно найти в httpd.conf. За детальным разъяснением этих причин можно обратиться к log-файлам, расположенным в каталоге C:\Program Files\Apache Group\Apache\logs.
Проверка html
В каталоге z:/home/localhost/www, содержащем HTML-документы Apache, создайте файл index.html с любым текстовым наполнением. Теперь запустите браузер и наберите:
http://localhost/index.html
или просто
http://localhost/
Должен загрузиться ваш файл.
Проверка корректности входных данных
До сих пор мы не заботились о том, корректные ли данные заносит посетитель. В нашей ситуации это и не нужно: в книгу кто угодно может добавлять любую информацию. В то же время в реальной жизни, конечно, приходится проверять правильность введенных пользователем данных.
Например, мы можем ввести в нашу гостевую книгу цензуру, которая будет запрещать пользователям употреблять в сообщениях ненормативную лексику. Конечно, при вводе недопустимого текста он не должен добавиться в гостевую книгу; вместо этого в браузер пользователя хотелось бы вывести предупреждение. Но как осуществить желаемую модерацию в соответствии с трехуровневой схемой? И какая часть программы должна за это отвечать?
На второй вопрос ответить довольно просто. Так как ядро не в состоянии "общаться" с шаблоном напрямую, а шаблон не может исполнять сложный код, остается единственный вариант — интерфейс. А что касается того, как выводить сообщение об ошибке, — вопрос довольно спорный. Мы рассмотрим лишь самое простое решение.
Интерфейс должен сгенерировать сообщение и передать его шаблону. Последний, "заметив" сообщение, может вывести текст контрастными буквами, например, вверху страницы. С этим никаких проблем быть не должно. Пусть интерфейс в случае ошибки создает переменную $Error и присваивает ей текст ошибки. Вот как может выглядеть шаблон:
. . .
<?if(Isset($Error)) {?>
<h3><font color=red>Произошла ошибка: <?=$Error?></font></h3>
<?}?>
. . .
Такой подход, хоть и прост, оказывается немного неудобным для пользователя. Действительно, ему сообщают, что произошла ошибка, но не говорят, например, какое именно поле формы он заполнил неправильно. Пользователь желает, чтобы сообщения об ошибках появлялись напротив неверно введенных данных. К сожалению, без дополнительного программирования в шаблоне на PHP этого добиться довольно сложно (если вообще возможно). Единственный имеющийся выход — использовать шаблонизатор и написать для него фильтр (функцию, занимающуюся финальной обработкой блока страницы перед ее отправкой), которая будет в автоматическом режиме рядом со всеми тэгами формы проставлять возможные сообщения об ошибках (а заодно и атрибуты value
в самих тэгах, чтобы поля формы сохраняли свои значения между вызовами сценария). Эта задача, пожалуй, потребует всей информации о PHP, заложенной в этой книге, и еще, вероятно, хорошего знания регулярных выражений Perl. Код, полностью решающий проблему, слишком объемен, чтобы уместиться на страницах данной книги.
Проверка на идентификатор
Задача: проверить, является ли строка $id идентификатором, т.е. состоит ли она исключительно из алфавитно-цифровых символов (чтобы сделать задачу более интересной, договоримся также, что первым символом строки не может быть цифра).
Решение:
if(eregi("[a-z_][[:alnum:]]*",$id)) echo "Это идентификатор!";
Проверка синтаксической корректности кода
С помощью create_function()
можно проверить, является ли некоторая строка верным PHP-кодом, не запуская при этом сам код. В самом деле, если создание функции с телом— заданной строкой — прошло успешно, значит, код синтаксически корректен. Вот пример:
$fname="file.php";
$code=join("",File($fname));
if(create_function("","?>$code<?"))
echo "Файл $fname является программой на PHP";
else
echo "Файл $fname — не PHP-сценарий";
Мы используем оператор @, чтобы подавить сообщение о том, что функцию создать не удалось, если файл не является верным PHP-сценарием. И, конечно, нам нужно перевести наш код в контекст восприятия документа, для чего, собственно,
и нужно обрамление строки тэгами ?>
и <?.
Представленный фрагмент, конечно, будет воспринимать любой текстовый файл и HTML-документ как "программу на PHP". И он будет прав, т. к., действительно, статический текст, в котором нет PHP-вставок, является верным PHP-сценарием.
Проверка SSI
В каталоге z:/home/localhost/www с HTML-документами Apache создайте файл test.shtml со следующим содержанием (внимательно следите за соблюдением пробелов в директиве include!):
Листинг 4.1. Файл test.shtml
SSI Test!<hr>
<!--#include virtual="/index.html" -->
<hr>
Затем наберите в браузере:
http://localhost/test.shtml
Должен открыться файл, который состоит из текста "SSI Test!", за которым следует содержимое файла index.html
между двумя горизонтальными чертами. Если этого не произошло, значит, вы неправильно сконфигурировали SSI.
Проверка существования
Можно проверить, существует ли (то есть, инициализирована ли) указанная переменная. Осуществляется это при помощи оператора IsSet(). Например:
if(IsSet($MyVar))
echo "Òàêàÿ ïåðåìåííàÿ åñòü. Åå çíà÷åíèå $MyVar";
Если переменной в данный момент не существует (то есть нигде ранее ей не присваивалось значение, либо же она была вручную удалена при помощи Unset()), то IsSet() возвращает ложь, в противном случае— истину.
Важно помнить, что мы не можем использовать неинициализированную переменную в программе — иначе это породит предупреждение со стороны интерпретатора (что, скорее всего, свидетельствует о наличии логической ошибки в сценарии). Конечно, предупреждения можно выключить, тогда все неинициализированные переменные будут полагаться равными пустой строке. Однако я категорически не советую вам этого делать — уж лучше лишняя проверка присутствия в коде, чем дополнительная возня с "отлавливанием" возможной[В. О.21] ошибки в будущем. Если вы все же захотите отключить это злополучное предупреждение (а заодно и все остальные), лучше использовать оператор отключения ошибок @, который действует локально (о нем мы тоже вскоре поговорим).
Проверка существования константы
В PHP существует также функция, которая проверяет, существует ли (была ли определена ранее) константа с указанным именем. Вот она.
bool defined(string $name)
Возвращает true, если константа с именем $name была ранее определена.
Впрочем, я ни разу не видел программы, которая задействовала бы эту возможность. Но для полноты картины я эту функцию все-таки здесь привел.
Путь к странице
Наконец, мы дошли до последней части адресной строки— пути к файлу страницы (в нашем примере это /path/to/document.html). Как уже упоминалось, совершенно не обязательно, чтобы эта страница действительно присутствовала, — вполне типична ситуация, когда страницы создаются "на лету" и не представлены отдельными файлами в файловой системе сервера. Например, сайт новостей может использовать виртуальные пути типа /Y/M/N.html
для отображения всех новостей за число N
месяца M года Y, так что пользователь, набрав в браузере адрес наподобие http://новострой_сервер/2000/
10/20.html, сможет прочитать новости за 20 октября 2000 года. При этом файла с именем 20.html физически нет, существует только виртуальный путь к нему, а всю работу по генерации страницы берет на себя программное обеспечение сервера (в последней части этой книги я расскажу, как такое программное обеспечение можно написать).
Есть и другой механизм обработки виртуальных путей, когда запрошенные файлы представляют собой статические объекты, но располагаются где-то в другом месте. С точки зрения программного обеспечения путь к документу отсчитывается от некоторого корневого каталога, который указывает администратор сервера. Практически все серверные программы позволяют создавать псевдонимы для физических путей. Например, если мы вводим:
http://www.somehost.com/cgi-bin/something
отсюда не следует, что существует каталог cgi-bin, — это может быть лишь имя псевдонима, ссылающегося на какую-то другую каталог.
Расширение html (от HyperText Markup Language — Язык разметки гипертекста) принято давать документам со страницами Web. HTML представляет собой язык, на котором задается расположение текста, рисунков, гиперссылок и т. д. Кроме html часто встречаются и другие форматы данных: gif, jpg — для изображений, cgi, pl — для сценариев (программ, запускаемых на сервере) и т. д. Вообще говоря, сервер можно настроить таким образом, чтобы он корректно работал с любыми расширениями,
например, никто не запрещает нам сконфигурировать его так, чтобы файлы с расширением htm также рассматривались как HTML-документы (что часто и делается).
Браузеру совершенно все равно, какое расширение у запрошенного объекта — он ориентируется по другому признаку.
QUERY_STRING
Параметры, которые в URL указаны после вопросительного знака. Напомню, что они доступны как при методе GET, так и при методе POST (если в последнем случае они были определены в атрибуте action тэга <form>).
Работа с базой данных MySQL
База данных — совокупность связанных данных, сохраняемая в двумерных таблицах информационной системы. Программное обеспечение информационной системы, обеспечивающей создание, ведение и совместное использование баз данных, называется системой управления базами данных (СУБД). В этой главе мы рассмотрим функции PHP, предназначенные для работы с одной из самых популярных СУБД — MySQL. В PHP есть функции для "общения" и с другими системами управления базами данных (например, Sybase, Oracle и т. д.), но я остановился именно на MySQL в силу ее простоты и универсальности для большинства приложений. Конечно, прежде чем работать с MySQL, нужно установить соответствующее программное обеспечение — программу-сервер MySQL. Как это сделать в системе Windows, подробно описано во главе 2 настоящей книги.
Данная глава ни в коей мере не претендует на исчерпывающее описание языка SQL и системы управления базами данных MySQL. Здесь приведен только основной минимум материала. Имея его под рукой, можно начинать писать сценарии, использующие MySQL. Если вам понадобится подробная документация, вы сможете найти ее в любом дистрибутиве MySQL.
Итак, с точки зрения программы база данных[E131] MySQL представляет собой удачно организованный набор поименованных таблиц. Каждая таблица — массив (возможно, очень большой) из однородных элементов, которые я буду называть записями. В принципе, запись — неделимая единица информации в базе данных, хотя по запросу можно получать и не всю ее целиком, а только какую-то часть.
Запись может содержать в себе одно или несколько именованных полей. Число и имена полей задаются при создании таблицы. Каждое поле имеет определенный тип (например, целое число, строка текста, массив символов и т. д).
Если вы в замешательстве и так и не поняли до конца, что же такое таблица, просто представьте себе Excel, таблицу на раскрученном рулоне туалетной бумаги[E132] , прямоугольную матрицу, сильно вытянутую по вертикали, или, наконец, двумерный массив. Строки таблицы/матрицы/массива и будут записями, а столбцы в пределах каждой строки — полями.
В таблицу всегда можно добавить новую запись. Другая операция, которую часто производят с записью (точнее, с таблицей) — это поиск. Например, запрос поиска может быть таким: "Выдать все записи, в первом поле которых содержится число, меньшее 10, во втором — строка, включающая слово word, а в третьем — не должен быть ноль". Из найденных записей в программу можно извлекать какие-то части данных (или не извлекать), также записи таблицы можно удалить.
Следует еще раз заметить, что обычно все упомянутые операции осуществляются очень быстро. Например, Microsoft SQL Server может за 0,01 секунды из 10 миллионов записей выделить ту, у которой значение определенного поля совпадает с нужным числом или строкой. Высокое быстродействие в большей мере обусловлено тем, что данные не просто "свалены в кучу", а определенным образом упорядочены и все время поддерживаются в таком состоянии.
Работа с библиотекарем
Рассмотрим пример сценария, использующего библиотекарь в своей работе. Мы будем предполагать, что все модули размещены в подкаталоге /lib основного каталога с 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");
// Константа: символы перевода строки
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;
}
?>
Работа с блоками текста
Перечисленные ниже функции чаще всего оказываются полезны, если нужно проводить однотипные операции с многострочными блоками текста, заданными в строковой переменной.
string str_replace(string $from, string $to, string $str)
Заменяет в строке $str все вхождения подстроки $from (с учетом регистра) на $to и возвращает результат. Исходная строка, переданная третьим параметром, при этом не меняется. Эта функция работает значительно быстрее, чем ereg_replace(), которую мы рассмотрим в главе о регулярных выражениях PHP, и ее часто используют, если нет необходимости в каких-то экзотических правилах поиска подстроки. Например, вот так мы можем заместить все символы перевода строки на их HTML-эквивалент— тэг <br>:
$st=str_replace("\n","<br>\n",$st)
Как видим, то, что в строке <br>\n тоже есть символ перевода строки, никак не влияет на работу функции, т. е. функция производит лишь однократный проход по строке. Для решения описанной задачи также применима функция nl2br(), которая работает чуть быстрее.
string nl2br(string $string)
Заменяет в строке все символы новой строки \n на <br>\n и возвращает результат. Исходная строка не изменяется.
Обратите внимание на то, что символы \r, которые присутствуют в конце строки текстовых файлов Windows, этой функцией никак не учитываются, а потому остаются на старом месте.
string WordWrap(string $st, int $width=75, string $break="\n")
Эта функция, наконец-то появившаяся в PHP версии 4, оказывается невероятно полезной при форматировании текста письма перед автоматической отправкой его адресату при помощи mail(). Она разбивает блок текста $st на несколько строк, завершаемых символами $break, так, чтобы на одной строке было не более $width
букв. Разбиение происходит по границе слова, так что текст остается читаемым. Возвращается получившаяся строка с символами перевода строки, заданными в $break. Давайте рассмотрим пример, как мы можем отформатировать некоторый текст по ширине поля 60 символов, предварив каждую строку префиксом ">" (то есть, оформить его как цитирование, принятое в электронной переписке):
function Cite($OurText, $prefix="> ")
{ $st=WordWrap($OurText, 60-strlen($prefix), "\n");
$st=$prefix.str_replace("\n","\n$prefix",$st);
// можно было бы сделать это и одной операцией, но так,
// по-моему, несколько универсальнее.
return $st;
}
string strip_tags (string $str [, string $allowable_tags])
Эта функция удаляет из строки все тэги и возвращает результат. В параметре $allowable_tags можно передать тэги, которые не следует удалять из строки. Они должны перечисляться вплотную друг к другу. Вот пример:
$st="
<b>Жирный текст</b>
<tt>Моноширинный текст</tt>
<a href=http://www.dklab.ru>Ссылка</a>";
echo "Исходный текст: $st";
echo "<hr>После удаления тэгов: ".strip_tags($st,"<a><b>")."<hr>";
Запустив этот пример, мы сможем заметить, что тэги <a>
и <b> не были удалены (ровно как и их парные закрывающие), в то время как <tt>
исчез.
string str_repeat(string $st, string $number)
Функция "повторяет"
строку $st $number раз и возвращает объединенный результат. Вот пример:
echo str_repeat("test!",3); // выводит test!test!test!
Работа с цветом в формате RGB
Наверное, теперь вам захочется что-нибудь нарисовать на пустой или только что загруженной картинке. Но чтобы рисовать, нужно определиться, каким цветом это делать. Проще всего указать цвет заданием тройки RGB-значений (от red-green-blue)— это три цифры от 0 до 255, определяющие содержание красной, зеленой и синей составляющих в нужном нам цвете. Число 0 обозначает нулевую яркость соответствующей компоненты, а 255 — максимальную интенсивность. Например, (0,0,0) задает черный цвет, (255,255,255) — белый, (255,0,0) — ярко-красный, (255,255,0) — желтый и т. д.
Правда, GD не умеет работать с такими тройками напрямую. Она требует, чтобы перед использованием RGB-цвета был получен его специальный идентификатор. Дальше вся работа опять же осуществляется через этот идентификатор. Скоро станет ясно, зачем нужна такая техника.
Работа с данными формы
Дойдя до этого места, я столкнулся с проблемой непростого выбора: продолжать и дальше рассказывать о самом языке PHP или же чуть-чуть уйти в сторону и рассмотреть более прикладные задачи. Я остановился на последнем. Как-никак, Web-программирование в большей части (или хотя бы наполовину) представляет собой как раз обработку различных данных, введенных пользователем — т. е., обработку форм.
Пожалуй, нет другого такого языка, как PHP, который бы настолько облегчил нам задачу обработки и разбора форм, поступивших из браузера. Дело в том, что в язык на самом нижнем уровне встроены все необходимые возможности, так что нам не придется даже и задумываться над особенностями протокола HTTP и размышлять, как же происходит отправка и прием POST-форм или даже загрузка файлов. Разработчики PHP все предусмотрели.
В седьмой главе мы довольно подробно рассмотрели механизм работы протокола HTTP, который отвечает за доставку данных из браузера на сервер и обратно. Впрочем, там было довольно много теории, так что предлагаю повторить этот процесс еще раз — так сказать, с прикладных позиций, а также разобрать возможности, предоставляемые PHP.
Работа с датами
string date(string $format [,int $timestamp])
Эта функция крайне полезна и весьма универсальна. Она возвращает строку, отформатированную в соответствии с параметром $format и сформированную на основе параметра $timestamp (если последний не задан— то на основе текущей даты). Строка формата может содержать обычный текст, перемежаемый одним или несколькими символами форматирования:
r U — количество секунд, прошедших с полуночи 1 января 1970 года;
r z — номер дня от начала года;
r Y — год, 4 цифры;
r y — год, 2 цифры;
r F — название месяца, например, January;
r m — номер месяца;
r M — название месяца, трехсимвольная аббревиатура, например, Jan;
r d — номер дня в месяце, всегда 2 цифры (первая может быть 0);
r j — номер дня в месяце без предваряющего нуля;
r w — день недели, 0 соответствует воскресенью, 1 — понедельнику, и т. д.;
r l — день недели, текстовое полное название, например, Friday;
r D — день недели, английское трехсимвольное сокращение, например, Fri;
r a — am или pm;
r A — AM или PM;
r h — часы, 12-часовой формат;
r H — часы, 24-часовой формат;
r i — минуты;
r s — секунды;
r S — английский числовой суффикс (nd, th и т. д.).
Те символы, которые не были распознаны как форматирующие, подставляются в результирующую строку "как есть". Впрочем, не советую этим злоупотреблять, поскольку довольно мало английских слов не содержат ни одной из перечисленных выше букв.
Как видите, набор символов форматирования весьма и весьма богат. Вот пример применения функции date():
echo date("l dS of F Y h:i:s A");
echo date("Ñåãîäíÿ d.m.Y");
echo date("Ýòîò ôàéë äàòèðîâàí d.m.Y",filectime("myfile"));
Остается еще отметить, что формат выдачи для таких символов, как F (название месяца), зависит от текущих настроек локали (см. функцию setlocale()) и вполне может быть названием месяца на родном языке пользователя.
int checkdate(int $month, int $day, int $year)
Эта функция проверяет, существует ли дата, переданная ей в параметрах: вначале ищется месяц, затем — день, и, наконец, — год. Конкретнее, checkdate() проверяет следующее:
r год должен быть между 1900 и 32 767 включительно;
r месяц обязан принадлежать диапазону от 1 до 12;
r число должно быть допустимым для указанного месяца и года (если год високосный).
Функция очень полезна, например, при автоматическом формировании HTML-календарика для указанного месяца и года. В самом деле, мы можем определить, какие числа в месяце "не существуют", и для них вместо номера проставить пустое место.
array getdate(int $timestamp)
Возвращает ассоциативный массив, содержащий данные об указанном времени. В массив будут помещены следующие ключи и значения:
r seconds — секунды;
r minutes — минуты;
r hours — часы;
r mday — число;
r wday — день недели, число;
r mon — номер месяца;
r year — год;
r yday — номер дня с начала года;
r weekday — полное название дня недели, например, Friday;
r month — полное название месяца, например, January.
В общем-то, всю эту информацию можно получить и с помощью функции date(), но тут разработчики PHP предоставляют нам альтернативный способ.
Работа с датами и временем
В PHP присутствует полный набор средств, предназначенных для работы с датами и временем в различных форматах. Дополнительные модули (входящие в дистрибутив PHP) позволяют также работать с календарными функциями и календарями различных народов мира. Мы рассмотрим только самые популярные из этих функций.
Работа с файлами
Хорошие новости. Во-первых, вы можете наконец с облегчением вздохнуть и забыть о том, что в Windows (в отличие от Unix) для разделения полного пути файла используются не прямые, а обратные слэши. Интерпретатору PHP совершенно все равно, какие слэши вы будете использовать: прямые или обратные. Он в любом случае переведет их в ту форму, которая требуется вашей ОС, а функции по работе с полными именами файлов также не будут против "чужих"
слэшей[E64] .
Во-вторых, вы теперь можете работать с файлами на удаленных серверах Web в точности так же, как и со своими собственными (ну, разве что записывать в них можно не всегда). Если вы предваряете имя файла строкой http:// или ftp://, то PHP понимает, что нужно на самом деле установить сетевое соединение и работать именно с ним, а не с файлом. При этом в программе такой файл ничем не отличается от обычного (если у вас есть соответствующие права, что вы можете и записывать в подобный HTTP- или FTP-файл).
Работа с изображениями
Как мы знаем, одним из самых важных достижений WWW по сравнению со всеми остальными службами Интернета стала возможность представления в браузерах пользователей мультимедиа-информации, а не только "сухого"
текста. Основной объем этой информации приходится, конечно же, на изображения.
Разумеется, было бы довольно расточительно хранить и передавать все рисунки в обыкновенном растровом формате (наподобие BMP), тем более, что современные алгоритмы сжатия позволяют упаковывать такого рода данные в сотни и более раз эффективней. Чаще всего для хранения изображений используются три формата сжатия с перечисленными ниже свойствами.
r JPEG. Идеален для фотографий, но сжатие изображения происходит с потерями качества, так что этот формат совершенно не подходит для хранения различных диаграмм и графиков.
r GIF. Позволяет достичь довольно хорошего соотношения размер/качество, в то же время не искажая изображение; применяется в основном для хранения небольших точечных рисунков и диаграмм.
r PNG. Сочетает в себе хорошие стороны как JPEG, так и GIF, но в настоящий момент ему почему-то не выражают особого доверия — скорее, по историческим причинам, из-за нежелания отказываться от GIF и т. д.
В последнее время GIF все более вытесняется форматом PNG, что связано в первую очередь с окончанием действия бесплатной лицензии изобретателя на его использование. К сожалению, для небольших изображений GIF все еще остается самым оптимальным форматом, оставляя позади (иногда далеко позади) PNG.
Зачем может понадобиться в Web-программировании работа с изображениями? Разве это не работа дизайнера?
В большинстве случаев это действительно так. Однако есть и исключения, например, графические счетчики (автоматически создаваемые картинки с отображаемым поверх числом, которое увеличивается при каждом "заходе"
пользователя на страницу), или же графики, которые пользователь может строить в реальном времени — скажем, диаграммы сбыта продукции или снижения цен на комплектующие. Все эти приложения требуют как минимум умения генерировать изображения "на лету", причем с довольно большой скоростью. Чтобы этого добиться на PHP, можно применить два способа: задействовать какую-нибудь внешнюю утилиту для формирования изображения (например, известную программу fly), или же воспользоваться встроенными функциями PHP для работы с графикой. Оба способа имеют как достоинства, так и недостатки, но, пожалуй, недостатков меньше у второго метода, так что им-то мы и займемся в этой главе.
С недавнего времени все программные продукты, которые умели формировать изображения в формате GIF, переориентируются на PNG. В частности, не так давно[E90] [DK91] компания, поддерживающая библиотеку GD для работы с GIF-изображениями, переписала ее код с учетом формата PNG. Так как PHP использует эту библиотеку, то поддержка GIF автоматически исключилась и из него. К счастью, в Интернете все еще можно найти старые версии GD с поддержкой GIF и, таким образом, настроить PHP для работы с этим форматом, но задумайтесь: стоит ли теперь применять GIF, если весь мир вполне успешно переходит на PNG, тем более, что его поддерживают практически все [E92] [DK93] современные браузеры (четвертой версии) — а это 98% от используемого их числа...
Работа с изображениями и библиотека GD
Давайте теперь рассмотрим идею создания рисунков сценарием "на лету". Например, как мы уже замечали, это очень может пригодиться при создании сценариев-счетчиков, графиков, картинок-заголовков да и многого другого.
Для деятельности[E94] [DK95] такого рода существует специальная библиотека под названием GD. Она содержит в себе множество функций (такие как рисование линий, растяжение/сжатие изображения, заливка до границы, вывод текста и т. д.), которые могут использовать программы, поддерживающие работу с данной библиотекой. PHP (со включенной поддержкой GD) как раз и является такой программой.
Поддержка GD включается при компиляции и установке PHP. Возможно, некоторые хостинг-провайдеры ее не имеют. Выясните, работает ли PHP вашего хостера с библиотекой GD.
Работа с каталогами
С точки зрения операционной системы каталоги — это те же самые файлы, только со специальным именем. То есть директорию можно представить себе как файл, в котором хранятся имена и местоположения других файлов и каталогов. Этим обеспечивается традиционная древовидность организации файловой системы в различных ОС.
С каждым процессом (в частности, и с работающим сценарием) ассоциирован свой так называемый текущий каталог. Все действия по работе с файлами и каталогами осуществляются по умолчанию именно в ней. Например, если мы открываем файл, указав только его имя, PHP будет искать этот файл именно в текущем каталоге. Существуют также и функции, которые могут сделать текущим любой указанный каталог.
Работа с массивами
В части III книги мы уже рассматривали многие возможности, которые предоставляет PHP для работы с ассоциативными массивами. В их число входят различные механизмы перебора, получение числа элементов, оперирование ключами и значениями и т. ä.
Однако здесь перечислено далеко не все, что можно делать с массивами в PHP. Язык (особенно версии 4) содержит множество других, иногда крайне полезных, функций. В этой главе мы рассмотрим большинство из них.
Работа с пикселами
int imageSetPixel(int $im, int $x, int $y, int $col)
Эта функция практически не интересна, т. к. выводит всего один пиксел цвета $col в изображении $im, расположенный в точке ($x,$y). Не думаю, чтобы с помощью нее можно было закрасить хоть какую-нибудь сложную фигуру, потому что, как мы знаем, PHP довольно медленно работает с длинными циклами,[E104] [DK105] а значит, даже рисование обычной линии с использованием этой функции будет очень дорогим занятием.
int imageColorAt(int $im, int $x, int $y)
В противоположность своему антиподу — функции imageSetPixel() — функция imageColorAt() не рисует, а возвращает цвет точки, расположенной на координатах ($x,$y). Возвращается идентификатор цвета, а не его RGB-представление.
Функцию удобно использовать, опять же, для определения, какой цвет в картинке должен быть прозрачным. Например, все у той же птички на кислотно-зеленом фоне мы достоверно знаем, что прозрачный цвет точно приходится на точку с координатами (0,0). Таким образом, теперь мы сможем в любой момент сменить цвет фона на мертвенно-голубой (который тоже у реальной птицы вряд ли встретится), и программа все равно будет работать правильно.
Работа с WWW
Мы уже рассматривали основы протокола HTTP в части I книги. Оператор echo, предназначенный для вывода данных в браузер, нам также хорошо знаком. Теперь осталось лишь разобрать, какие средства предусмотрены в PHP для работы с заголовками HTTP.
Работа с записями
Дальше описываются функции, которые позволяют узнать, какие объекты находятся в указанном каталоге. Например, с их помощью можно вывести содержимое текущего каталога. Механизм работы этих функций базируется примерно на тех же принципах, что и применяемых для файловых операций: сначала интересующий каталог открывается, затем из него производится считывание записей, и под конец каталог нужно закрыть. Правила интуитивно понятны и, наверное, хорошо вам знакомы.
int opendir(string $path)
Открывает каталог $path для дальнейшего считывания из него информации о файлах и подкаталогах и возвращает его идентификатор. Дальнейшие вызовы readdir() с идентификатором в параметрах будут обращены именно к этому каталогу. Функция возвращает false, если произошла ошибка.
string readdir(int $handle)
Считывает очередное имя файла или подкаталога из открытого ранее каталога с идентификатором $handle и возвращает его в виде строки. Порядок следования файлов в каталоге зависит от операционной системы— скорее всего, он будет совпадать с тем порядком, в котором эти файлы создавались, но не всегда. Вместе с именами подкаталогов и файлов будут также получены два специальных элемента: это . (ссылка на текущий каталог) и .. (ссылка на родительский каталог). В подавляющем большинстве случаев нам нужно их игнорировать, что и сделано в примере из листинга 16.1 при помощи инструкции continue.
PHP версии 3 позволял опускать параметр $handle — в этом случае, кажется, подразумевался последний открытый каталог. Сценарий "собирался с силами", "вздыхал" и… кое-как работал. PHP версии 4 более строг: в нем вы обязательно должны указывать параметр $handle äëÿ ôóíêöèè readdir(), в противном случае вам гарантированы сюрпризы.
В случае, если в каталоге все файлы уже считаны, функция возвращает ложное значение. Но не позволяйте себе привыкнуть к конструкции такого вида:
$d=opendir("somewhere");
while($e=readdir($d)) { . . .}
Она заставит цикл прерваться в его середине в случае обнаружения файла с именем "0", чего нам бы, конечно, не хотелось. Вместо этого пользуйтесь следующим методом:
$d=opendir("somewhere");
while(($e=readdir($d))!==false) { . . .}
Оператор !== позволяет точно проверить, была ли возвращена величина false.
void closedir(int $handle)
Закрывает ранее открытый каталог с идентификатором $handle. Не возвращает ничего.
В принципе, можно и не закрывать каталоги, т. к. это делается автоматически при завершении программы, но лучше все-таки такой легкостью не обольщаться.
void rewinddir(int $handle)
"Перематывает"
внутренний указатель открытого каталога на начало. После этого можно воспользоваться readdir(), чтобы заново начать считывать содержимое каталога.
"Расщепление" шаблона
Второй недостаток более очевиден для дизайнера: файлы header.htm и footer.htm, хотя и представляют собой логически один шаблон, все же разделены. Все мы привыкли к тому, что многие тэги HTML (такие как <body>, <table> и т.д.) имеют парные закрывающие, причем расположенные в том же самом файле. Но в ситуации с разделением шаблона на footer и header мы, наоборот, должны хранить большинство открывающих тэгов в одном файле, а закрывающие — в другом. В листинге 30.8 приведен пример верхней части шаблона страницы.
Листинг 30.8. Файл header.htm
<html>
<body bgcolor=white>
<h1>Добрый день.</h1>
<table><tr>
<td width=20%>Карта раздела: . . .</td>
<td>
Видите, файл оборвался на открывающем тэге. Теперь взглянем на листинг 30.9:
Листинг 30.9. Файл footer.htm
</td>
</tr></table>
</body>
</html>
Он состоит исключительно из одних закрывающих тэгов. Потенциально, добавив в header.htm новый открывающий тэг, мы можем забыть закрыть его в footer.htm. Кроме того, такая конструкция несколько противоречит логике: две похожих по смыслу части шаблона содержатся в разных файлах. Мы уже обсуждали это выше и пришли к выводу, что данное построение оказывается довольно неудобным.
Расшифровка URL-кодированных данных
Если бы в предыдущем примере мы ввели параметры, содержащие, например, буквы кириллицы, то сценарию они бы поступили не в "нормальном" виде, а в URL-закодированном. Пожалуй, ни один сценарий не обходится без функции расшифровки URL-кодированных данных. И это совсем не удивительно. Радует только то, что такую функцию нужно написать один раз, а дальше можно пользоваться ей по мере необходимости.
Как уже упоминалось, кодирование заключается в том, что некоторые неалфавитно-цифровые символы (в том числе и "русские"
буквы, которые тоже считаются неалфавитными) преобразуются в форму %XX, где XX— код символа в шестнадцатеричной системе счисления. Далее представлена функция на Си, которая умеет декодировать подобные данные и приводить их к нормальному представлению.
Мы не можем сначала все данные (например, полученные из стандартного потока ввода) декодировать, а уж потом работать с ними (в частности, разбивать по месту вхождения символов & и =). Действительно, вдруг после перекодировки появятся символы & и =, которые могут быть введены пользователем? Как мы тогда узнаем, разделяют ли они параметры или просто набраны с клавиатуры? Очевидно, никак. Поэтому такой способ нам не подходит, и придется работать с каждым значением отдельно, уже после разделения строки на части.
Итак, приходим к следующему алгоритму: сначала разбиваем строку параметров на блоки (параметр=значение), затем из каждого блока выделяем имя параметра и его значение (обособленные символом =), а уж потом для них вызываем функцию перекодировки, приведенную ниже:
Листинг 3.5. Функция URL-декодирования
// Ôóíêöèÿ ïðåîáðàçóåò ñòðîêó äàííûõ st â íîðìàëüíîå ïðåäñòàâëåíèå.
// Ðåçóëüòàò ïîìåùàåòñÿ â òó æå ñòðîêó, ÷òî áûëà ïåðåäàíà â ïàðàìåòðàõ.
void UrlDecode(char *st) {
char *p=st; // óêàçûâàåò íà òåêóùèé ñèìâîë ñòðîêè
char hex[3]; // âðåìåííûé áóôåð äëÿ õðàíåíèÿ %XX
int code; // ïðåîáðàçîâàííûé êîä
// çàïóñêàåì öèêë, ïîêà íå êîí÷èòñÿ ñòðîêà (òî åñòü, ïîêà íå
// ïîÿâèòñÿ ñèìâîë ñ êîäîì 0, ñì. íèæå)
do {
// Åñëè ýòî %-êîä ...
if(*st == '%') { // òîãäà êîïèðóåì åãî âî âðåìåííûé áóôåð
hex[0]=*(++st); hex[1]=*(++st); hex[2]=0;
// ïåðåâîäèì åãî â ÷èñëî
sscanf(hex,"%X",&code);
// è çàïèñûâàåì îáðàòíî â ñòðîêó
*p++=(char)code;
// óêàçàòåëü p âñåãäà îòìå÷àåò òî ìåñòî â ñòðîêå, â êîòîðîå
// áóäåò ïîìåùåí î÷åðåäíîé äåêîäèðîâàííûé ñèìâîë
}
// èíà÷å, åñëè ýòî "+", òî çàìåíÿåì åãî íà " "
else if(*st=='+') *p++=' ';
// à åñëè íå òî, íè äðóãîå — îñòàâëÿåì êàê åñòü
else *p++=*st;
} while(*st++!=0); // ïîêà íå íàéäåì íóëåâîé êîä
}
Функция основана на том свойстве, что длина декодированных данных всегда меньше, чем кодированных, а значит, всегда можно поместить результат в ту же строку, не опасаясь ее переполнения. Конечно, примененный алгоритм далеко не оптимален, т. к. использует довольно медлительную функцию sscanf() для перекодирования каждого символа. Тем не менее, это самое простое, что можно придумать, вот почему я так и сделал.
Итак, теперь мы можем слегка модифицировать предыдущий пример сценария, заставив его перед выводом данных декодировать их. Попробуем написать это так:
Листинг 3.6. Получение POST-данных с URL-декодированием
#include <stdio.h>
#include <stdlib.h>
void main(void) {
// ïîëó÷àåì çíà÷åíèÿ ïåðåìåííûõ îêðóæåíèÿ
char *RemoteAddr = getenv("REMOTE_ADDR");
char *ContentLength = getenv("CONTENT_LENGTH");
// âûäåëÿåì ïàìÿòü äëÿ áóôåðà QUERY_STRING
char *QueryString = malloc(strlen(getenv("QUERY_STRING")) + 1);
// êîïèðóåì QUERY_STRING â ñîçäàííûé áóôåð
strcpy(QueryString, getenv("QUERY_STRING"));
// äåêîäèðóåì QUERY_STRING
UrlDecode(QueryString);
// âû÷èñëÿåì êîëè÷åñòâî áàéòîâ äàííûõ — ïåðåâîäèì ñòðîêó â ÷èñëî
int NumBytes = atoi(ContentLength);
// âûäåëÿåì â ñâîáîäíîé ïàìÿòè áóôåð íóæíîãî ðàçìåðà
char *Data = (char*)malloc(NumBytes + 1);
// ÷èòàåì äàííûå èç ñòàíäàðòíîãî ïîòîêà ââîäà
fread(Data, 1, NumBytes, stdin);
// äîáàâëÿåì íóëåâîé êîä â êîíåö ñòðîêè
// (â Ñè íóëåâîé êîä ñèãíàëèçèðóåò î êîíöå ñòðîêè)
Data[NumBytes] = 0;
// äåêîäèðóåì äàííûå (õîòü ýòî è íå ñîâñåì îñìûñëåííî, íî âûïîëíÿåì
// ñðàçó äëÿ âñåõ POST-äàííûõ, íå ðàçáèâàÿ èõ íà ïàðàìåòðû)
UrlDecode(Data);
// âûâîäèì çàãîëîâîê
printf("Content-type: text/html\n\n");
// âûâîäèì äîêóìåíò
printf("<html><body>");
printf("<h1>Çäðàâñòâóéòå. Ìû çíàåì î Вàñ âñå!</h1>");
printf("Âàø IP-àäðåñ: %s<br>",RemoteAddr);
printf("Êîëè÷åñòâî áàéòîâ äàííûõ: %d<br>", NumBytes);
printf("Âîò ïàðàìåòðû, êîòîðûå Âû óêàçàëè: %s<br>",Data);
printf("À âîò òî, ÷òî ìû ïîëó÷èëè ÷åðåç URL: %s",
QueryString);
printf("</body></html>");
}
Обратите внимание на строки, выделенные жирным шрифтом. Теперь мы используем промежуточный буфер для хранения QUERY_STRING. Зачем? Попробуем поставить все на место, т. е. не задействовать промежуточный буфер, а работать с переменной окружения напрямую, как это было в листинге 3.5. Тогда в одних операционных системах этот код будет работать прекрасно, а в других — генерировать ошибку общей защиты, что приведет к немедленному завершению работы сценария. В чем же дело? Очень просто: переменная QueryString ссылается на значение переменной окружения QUERY_STRING, которая расположена в системной области памяти, а значит, доступна только для чтения. В то же время функция UrlDecode(), как я уже замечал, помещает результат своей работы в ту же область памяти, где находится ее параметр, что и вызывает ошибку.
Чтобы избавиться от указанного недостатка, мы и копируем значение переменной окружения QUERY_STRING в область памяти, доступной сценарию для записи — например, в какой-нибудь буфер, а потом уже преобразовываем его. Что и было сделано в последнем сценарии.
Несколько однообразно и запутанно, не так ли? Да, пожалуй. Но, как говорится, "это даже хорошо, что пока нам плохо" — тем больше будет причин предпочитать PHP другим языкам программирования (так как в PHP эти проблемы изжиты как класс).
Разделенные вычисления
Большинство хостинг-провайдеров ставят ограничения на то время, в течение которого могут выполняться сценарии пользователя. Иными словами, если выполнение программы занимает более определенного времени (например, 10 секунд), она прерывается принудительным образом. Минимальный квант времени задается в файле конфигурации php.ini. Как правило, его хватает для большинства программ, но все же существуют Web-приложения, требующие длительной работы.
Одним из таких приложений является автоматически генерируемая карта сервера. Она может представлять собой обычный сценарий на PHP, который рекурсивно обходит все каталоги сервера и собирает информацию о файлах, которые в них находятся. Конечно, если сайт велик, кванта времени, отведенного хостинг-провайдером, может и не хватить. Кроме того, не очень вежливо заставлять пользователя ждать загрузки страницы карты сервера дольше нескольких секунд.
Как же быть, если описанный сценарий нужен для вашего сайта? Для этого следует формировать карту не при каждом запросе, а лишь изредка, — ведь новые страницы добавляются на сервер довольно редко. Гораздо реже, чем, например, их загружают пользователи. Кроме того, наверное, пользователь не будет особенно недоволен, если изменение на карте сервера проявится не сразу же, а спустя некоторое время — например, час. Главное для него, чтобы карта была всегда перед глазами, а значит, отображалась быстро.
Мы можем хранить уже "просчитанную" карту сервера в файле, быстро выдавая его пользователю при запросе. Но даже если мы собираемся обновлять этот файл всего лишь один раз в час (при очередном запросе карты пользователем), мы наталкиваемся на проблему нехватки кванта времени, выделенного хостинг-провайдером.
Чтобы решить и эту проблему, придется разбить построение большой карты на множество мелких этапов, каждый из которых занимает, скажем, не более 2-х секунд. Каждый такой этап должен запускаться при очередном обращении пользователя к карте сервера, но уже после того, как содержимое временного файла с "просчитанной" картой будет отправлено пользователю. Таким образом, мы постепенно будем накапливать сведения и, как только весь сайт обработан, перестроим карту во временном файле. В ближайший час будет отображаться именно она.
Напишем функцию WalkSite(), которая будет заниматься поиском и обработкой файлов на каждом этапе обхода сайта. Листинг 33.1 содержит код библиотеки, в которой описана эта функция. Чтобы не "привязываться" к специфике конкретной задачи, сделаем функцию универсальной. Будем передавать ей имя процедуры-обработчика, умеющего "вытаскивать" из указанного файла всю информацию, необходимую для построения карты (например, название страницы, ее размер и т. д.),
сама же WalkSite() будет просто вызывать этот обработчик в нужный момент времени, следя за тем, чтобы квант времени, отведенный на данный этап построения карты, не истек. Если это произойдет, текущее состояние обхода сервера (включая всю собранную информацию) будет сохранено в специальном файле, а при следующем запуске — восстановлено, с тем чтобы обход продолжился с того же места, где он завершился в прошлый раз.
Листинг 33.1. Библиотека для обхода дерева сайта: SiteWalker.phl
<?
// Функция выполняет один этап обхода всех каталогов и файлов сайта.
// Если обход нужно продолжить, загружается предыдущее состояние
// из файла $cache. Если этого файла не существует, значит,
// необходимо начать новый обход, начиная с каталога $Root.
// Этап будет длиться не более $time секунд (если 0, то за один
// раз обрабатывается ровно один файл или каталог).
// Для каждого обнаруженного файла или каталога вызывается функция,
// имя которой передано в $Func.
// Формат функции: function FWalker(string $fname, array &$Result)
// Эта функция должна обрабатывать найденный файл $fname
// соответствующим образом и добавлять данные в массив $Result
// (в любом формате). Состояние массива $Result будет автоматически
// сохранено сразу по истечении кванта времени и восстановлено
// перед началом нового этапа.
// Возвращает true, если процесс не был закончен на этом этапе,
// и false, если только что были обработаны последние файлы на сервере.
function WalkSite($Root,$Func,$cache,$time,&$Result)
{ $Start=time();
// Состояние в самом начале работы. Нужно обработать
// корневой каталог $Root.
$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']);
// выходим, если время истекло, или же необработанных
// файлов не осталось.
} 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)
{ // для диагностики выводим имя файла
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.
Разделяемая блокировка
Мы решили ровно половину нашей задачи. Действительно, теперь данные из нескольких процессов-писателей не будут перемешиваться, но как быть с читателями? А вдруг процесс-читатель захочет прочитать как раз из того места, куда пишет процесс-писатель? В этом случае он, очевидно, получит "половинчатые"
данные. То есть, данные неверные. Как же быть?
Существуют два метода обхода этой проблемы. Первый— это использовать все ту же исключительную блокировку. Действительно, кто сказал, что исключительную блокировку можно применять только в процессах, изменяющих файл? Ведь функция flock() не знает, что будет выполнено с файлом, для которого она вызвана. Однако этот метод довольно-таки неудачен, и вот по какой причине. Представьте, что процессов-читателей много, а писателей — мало, и к тому же писатели еще и вызываются, скажем, раз в пару минут, а не постоянно, как читатели. В случае использования исключительной блокировки для процессов-читателей, довольно интенсивно обращающихся к файлу, мы очень скоро получим целый их рой, висящий, недовольно гудя, в очереди, пока очередному процессу разрешат читать. Но ведь никакой "аварии" не случится, если один и тот же файл будут читать и сразу все процессы этого роя, правда? Ведь чтение из файла его не изменяет. Итак, предоставив исключительную блокировку для читателей, мы потенциально получаем проблемы с производительностью, перерастающие в катастрофу, когда процессов-читателей становится больше некоторого определенного порога.
Второй (и лучший) способ подразумевает использование разделяемой блокировки. Процесс, который устанавливает этот вид блокировки, будет приостановлен только в одном случае: когда активен другой процесс, установивший исключительную блокировку. В нашем примере процессы-читатели будут "поставлены в очередь"
только тогда, когда активизируется процесс-писатель. И это правильно. Посудите сами: зачем зажигать красный свет на перекрестке, если поперечного движения заведомо нет?
Теперь давайте посмотрим на разделяемую блокировку читателей с точки зрения процесса-писателя. Что он должен делать, если кто-то читает из файла, в который он как раз собирается записывать? Очевидно, он должен дождаться, пока читатель не закончит работу. Иными словами, вызов flock($f,LOCK_EX) обязан подождать, пока активна хотя бы одна разделяемая блокировка. Это и происходит в действительности.
Возможно, вам на ум пришла аналогия с перекрестком, по одной дороге которого движется почти непрерывный поток машин, и поперечное движение при этом блокируется навсегда, — так что у водителей нет никаких шансов пробиться через сплошной поток. В реальном мире это действительно иногда происходит (потому-то любой светофор всегда представляет собой исключительную блокировку), но только не в мире PHP. Дело в том, что, если почти всегда активна разделяемая блокировка, операционная система все равно так распределяет кванты времени, что в некоторые из них можно "включить" исключительную блокировку. То есть "поток машин" становится не сплошным, а с "пробелами" — ровно такого размера, чтобы в них могли "прошмыгнуть" машины, едущие в перпендикулярном направлении.
В листинге 15.3 представлена модель процесса, использующего разделяемую блокировку.
Листинг 15.3. Модель процесса с разделяемой блокировкой
<?
// инициализация
// . . .
$f=fopen($f,"r") or die("Не могу открыть файл на чтение!");
flock($f,LOCK_SH); // ждем, когда процессы-писатели угомонятся
// В этой точке мы можем быть уверены, что эта программа работает
// с файлом, когда ни одна другая программа в него не пишет
// . . .
flock($f,LOCK_UN); // говорим, что мы больше не будем работать с файлом
fclose($f);
// Завершение
// . . .
?>
[E72] Устанавливайте разделяемую блокировку, когда вы собираетесь только читать из файла, не изменяя его. Всегда используйте при этом режим открытия r, и никакой другой. Снимайте блокировку так рано, как только сможете.
Разные советы
В этой небольшой завершающей главе сведены воедино некоторые советы и приемы программирования сценариев, которым не было удалено достаточного внимания в остальных главах книги.
Разрешение[DK148] IP-адреса в доменное имя и наоборот
string gethostbyaddr(string $ip_address)
Функция возвращает доменное имя хоста, заданного своим IP-адресом.
В случае ошибки возвращается $ip_address.
Функция не гарантирует, что полученное имя будет на самом деле соответствовать действительности. Она лишь опрашивает хост по адресу $ip_address
и просит его сообщить свое имя. Владелец хоста, таким образом, может передать все, что ему заблагорассудится. Как обойти эту проблему, см. чуть ниже.
string gethostbyname(string $hostname)
Функция получает в параметрах доменное имя хоста и возвращает его IP-адрес. Если адрес определить не удалось, возвращает $hostname.
array gethostbynamel(string $hostname)
Эта функция очень похожа на предыдущую, но возвращает не один, а все IP-адреса хоста с именем $hostname. Как мы знаем, одному доменному имени может соответствовать сразу несколько IP-адресов, и в случае сильной загруженности серверов DNS-сервер сам выбирает, по какому IP-адресу перенаправить запрос. Он выбирает тот адрес, который использовался наиболее редко.
Обратите внимание на то, что в Интернете существует множество виртуальных хостов, которые имеют различные доменные имена, но один и тот же IP-адрес. Таким образом, если следующая последовательность команд для существующего хоста с IP-адресом $ip всегда печатает этот же адрес:
$host=gethostbyaddr($ip);
echo gethostbyname($host);
то аналогичная последовательность для домена с DNS-именем $host,
наоборот, может напечатать не то же имя, а другое:
$ip=gethostbyname($host);
echo gethostbyaddr($ip);
Referer
r
Формат: Referer: URL_адрес
r Переменная окружения: HTTP_REFERER
Как правило, этот заголовок формируется браузером и содержит URL страницы, с которой осуществился переход на текущую страницу по гиперссылке. Впрочем, если вы пишете сценарий, который в целях безопасности отслеживает значение данного заголовка (например, для его запуска только с определенной страницы), помните, что умелый хакер всегда сможет подделать заголовок Referer.
Вы, наверное, подумали, что слово referer пишется по-английски с двумя буквами "r". Да, вы правы. Однако те, кто придумывал стандарт HTTP, этого, видимо, не знали. Так что не позволяйте столь досадному факту ввести себя в заблуждение, когда будете в сценарии использовать переменную окружения HTTP_REFERER.
Регистрация обработчиков
Вы, наверное, обратили внимание, что при описании обработчиков я указывал их имена с префиксом handler. На самом деле, это совсем не является обязательным. Даже наоборот — вы можете давать такие имена своим обработчикам, какие только захотите.
Но возникает вопрос: как же тогда PHP их найдет? Вот для этого и существует функция регистрации обработчиков, которая говорит интерпретатору, какую функцию он должен вызывать при наступлении того или иного события.
void session_set_save_handler($open,$close,$read,$write,$destroy,$gc)
Эта функция регистрирует подпрограммы, имена которых переданы в ее параметрах, как обработчики текущей сессии. Параметр $open
содержит имя функции, которая будет вызвана при инициализации сессии, а $close — функции, вызываемой при ее закрытии. В $read и $write
нужно указать имена обработчиков, соответственно, для чтения и записи во временное хранилище. Функция с именем, заданным в $destroy, будет вызвана при уничтожении сессии. Наконец, обработчик, определяемый параметром $gc, используется как сборщик мусора.
Эту функцию можно вызывать только до инициализации сессии, в противном случае она просто игнорируется.
Регистрация переменных
bool session_register(mixed $name [, mixed $name1, ...])
PHP узнает о том, что ту или иную переменную нужно сохранить в сессии, если ее предварительно зарегистрировать. Для этого и предназначена функция session_register(). Она принимает в параметрах одно или несколько имен переменных (имена задаются в строках, без знака $ слева), регистрирует их в текущей запущенной сессии и возвращает значение "истина", если все прошло корректно.
Почему же тогда я описал типы параметров как mixed, а не как string? Да потому, что на самом деле в функцию можно передавать не одну строку в каждом параметре, а сразу список строк. Каждая такая строка будет регистрировать отдельную переменную с соответствующим именем. Более того — элементом списка может опять же быть список строк, и т. д.
Нет ничего страшного, если мы дважды зарегистрируем одну и ту же переменную в сессии. На самом деле, чаще всего как раз так и происходит — при повторном запуске сценария. Вот пример:
Листинг 25.1. Пример работы с сессиями
<?
session_start();
session_register("count");
$count=@$count+1;
?>
<body>
<h2>Счетчик</h2>
В текущей сессии работы с браузером Вы открыли эту страницу
<?=$count?> раз(а). Закройте браузер, чтобы обнулить счетчик.
</body>
Как видим, все предельно просто.
REMOTE_ADDR
Эта переменная окружения задает IP-адрес (или доменное имя) узла пользователя, на котором был запущен браузер.
REMOTE_PORT
Порт, который закрепляется за браузером пользователя для получения ответа сервера.
REQUEST_METHOD
Метод, который применяет пользователь при передаче данных (мы рассматриваем только GET и POST, хотя существуют и другие методы). Надо заметить, что грамотно составленный сценарий должен сам определять на основе этой переменной, какой метод задействует пользователь, и принимать данные из соответствующего источника, а не рассчитывать, что передача будет осуществляться, например, только методом POST. Впрочем, все PHP-сценарии так и устроены.
Решение проблемы зацикливания обработчика
Помните, обработчик из листинга 29.5 мы связали только с расширениями html и htm, но не php? Мы сделали это, чтобы избежать зацикливания обработчика (см. соответствующее замечание). Давайте исправим положение. Очевидно, нужно связать с PHP еще одно расширение, которое не будет использоваться в сайте нигде, кроме как в имени обработчика из листинга 29.5. Пусть это будет, например, php4. Модифицируем наш .htaccess:
# Связываем расширение php4 с PHP
AddType application/x-httpd-php
php4
# Замкнем имя обработчика на конкретный файл
Action libhandler "/lib/libhandler.php4?"
# Документы этого типа мы желаем "пропускать" через наш обработчик
AddHandler libhandler .html .htm .php
Ну и, конечно, осталось только переименовать имеющийся у нас файл libhandler.php в libhandler.php4.
Теперь все сценарии с расширением php могут использовать функции, предоставляемые библиотекарем.
Рисунок для отправки формы (image)
<input type=image
[name=èìÿ]
src=èçîáðàæåíèå
>
Создает рисунок, при щелчке на котором кнопкой мыши будет происходить то же, что и при нажатии на кнопку submit, за тем исключением, что сценарию также будут пересланы координаты в пикселах того места, где произведен щелчок (отсчитываемые от левого верхнего угла рисунка). Придут они в форме: имя.x=X&имя.y=Y, где (X, Y) — координаты точки. Если же атрибут name не задан, то координаты поступят в формате: x=X&y=Y.
Сайт
Сайт— это часть логического пространства на хосте, состоящая из одной или нескольких HTML-страниц (иногда представляемых в виде HTML-документов). Хост вполне может содержать сразу несколько сайтов, размещенных, например, в разных его каталогах. Таким образом, сайт — термин весьма условный, обозначающий некоторый логически организованный набор страниц.
Сброс буфера вывода[E58]
void flush()
Эта функция имеет очень и очень отдаленное отношение к работе со строками, но она еще дальше отстоит от других функций. Именно поэтому я включил ее в данную главу. Начнем издалека: обычно при использовании echo
данные не прямо сразу отправляются клиенту, а накапливаются в специальном буфере, чтобы потом транспортироваться большой "пачкой". Так получается быстрее. Однако,
иногда бывает нужно досрочно отправить все данные из буфера пользователю, например, если вы что-то выводите в реальном времени (так зачастую работают чаты). Вот тут-то вам и поможет функция flush(), которая отправляет содержимое буфера echo
в браузер пользователя.
Не будет преувеличением сказать, что самой привлекательной чертой языка PHP является набор его стандартных, или встроенных, функций. Пожалуй, без них язык вообще представлял бы очень малую ценность. Главное, что этот набор постоянно пополняется с выходом новых версий языка: например, еще совсем недавно в PHP не было функций, поддерживающих концепцию сессии (то есть, устойчивого между запусками сценария окружения переменных, связанных по отдельности с каждым пользователем программы), функций для работы с изображениями и регулярными выражениями в формате PCRE (Perl-compatible regular expression — регулярные выражения языка Perl). Сейчас все это (и многое другое!) уже есть, и, конечно, грех не воспользоваться такими возможностями...Мы уже ознакомились с некоторыми базовыми функциями, которые в силу их специализации можно было бы даже назвать операторами. Среди них — функция вывода echo, функции для работы с массивами и переменными
и т. д. В этой части книги мы займемся остальными встроенными в PHP процедурами, которые чаще всего требуются в Web-программировании. Конечно, объем книги не позволяет описать абсолютно все функции, да это и невозможно, потому что такое описание тут же устареет с выходом новой версии языка. Так что, как всегда, лучшим другом программиста (может быть, правильнее сказать подругой?) обязательно должна стать документация, поставляемая вместе с дистрибутивами PHP, или ее online-версия, расположенная по адресу http://www.php.net
или http://ru.php.net. Использование документации из Интернета привлекательно еще и потому, что она фактически представляет собой один большой форум, в котором приведены различные комментарии, оставленные энтузиастами. Вы тоже можете внести свою лепту в это общее дело.
Что же, разработчики PHP не боги, и иногда даже во встроенных функциях встречаются ошибки. Если вы наткнулись на одну из таких ошибок, радуйтесь: у вас есть возможность внести вклад в общемировое дело Web-программирования! Вначале постарайтесь локализовать ошибку — напишите небольшой (как можно меньше!) сценарий, который будет работать неправильно. Далее, если у вас есть исходные тексты PHP и вы в состоянии в них разобраться (а это несколько проще, чем кажется на первый взгляд), попытайтесь найти в них то место, где происходит недоразумение. Наконец, отправьте накопленный материал по электронной почте разработчикам PHP (адрес можно узнать на сайте http://www.php.net) и, скорее всего, в будущих версиях языка ошибки уже не будет. Однажды со мной случился такой случай, и я был приятно удивлен той скоростью, с которой мне пришел ответ от разработчиков (в течение одного дня). Причем ответ развернутый, а не простая отписка и, главное, в следующей версии PHP, действительно, найденной ошибки уже не оказалось.
SCRIPT_NAME
Виртуальное имя выполняющегося сценария (то есть часть URL после имени сервера, но до символа?). Эту переменную окружения, опять же, очень удобно брать на вооружение при формировании заголовка Location
при переадресации на себя (self-redirect), а также при проставлении значения атрибута action
тэга <form>
на странице, которую выдает сценарий при запуске без параметров (для того чтобы не привязываться к конкретному имени сценария).
Secure
Этот параметр связан с защищенным протоколом передачи HTTPS, который в книге не рассматривается. Если вы не собираетесь писать сценарии для проведения банковских операций с кредитными карточками (или иные, требующие повышенной безопасности), вряд ли стоит обращать на него внимание.
После запуска сценария, выводящего соответствующий заголовок (или тэг <meta>),
у пользователя появится Cookie с именем name и значением value. Еще раз напоминаю: значения всех параметров Cookie должны быть URL-кодированы, в противном случае возможны неожиданности.
Секреты URL
Помните, я выше описывал, как выглядит URL? Каюсь, приврал. На самом деле URL имеет более "длинный" вид:
http://www.somehost.com:80/path/to/document.ext?parameters
Как нетрудно заметить, может существовать еще строка parameters, следующая после вопросительного знака. В некоторой степени эта строка аналогична командной строке ОС. В ней может быть все, что угодно, она может быть любой длины (однако следует учитывать, что некоторые символы должны быть URL-закодированы, см. ниже). Вот как раз эта-то строка и передается CGI-сценарию.
На самом деле существуют некоторые ограничения на длину строки параметров. Но нам приходится сталкиваться с ними слишком редко, чтобы имело смысл об этом говорить.
Вернемся к нашему предыдущему примеру. Теперь пользователь может указать свой часовой пояс сценарию, например,
так:
http://www.somehost.com/script.cgi?time=+5
Сценарий с именем script.cgi, после того как он запустится и получит эту строку параметров, должен ее проанализировать (например, создать переменную time и присвоить ей значение +5, т.е. 5 часов вперед) и дальше работать как ему нужно. Обращаю ваше внимание на то, что принято параметры сценариев указывать именно в виде переменная=значение.
А если нужно передать несколько параметров (например, не только часовой пояс, но и имя пользователя)? Сделаем это следующим образом:
http://www.somehost.com/script.cgi?time=+5&name=Vasya
Опять же, принято разделять параметры с помощью символа &. Будем в дальнейшем придерживаться этого соглашения, поскольку именно таким образом поступают браузеры при обработке форм. (Видели когда-нибудь на странице несколько полей ввода и переключателей, а под ними кнопку "Отправить"? Это и есть форма, с ее помощью можно автоматизировать процесс передачи данных сценарию). Ну и, разумеется, сценарий опять же должен адекватно среагировать на эти параметры: провести разбор строки, создать переменные и т. д. Обращаю ваше внимание на то, что все эти действия придется программировать вручную, если мы хотим воспользоваться языком Си.
Так вот, такой способ посылки параметров сценарию (когда данные помещаются в командную строку URL) называется методом GET. Фактически, даже если не передается никаких параметров (например, при загрузке статической страницы), все равно применяется метод GET. Все? Нет, не все. Существует еще один распространенный способ (не менее распространенный) — метод POST, но давайте прежде рассмотрим, на каком языке "общаются" браузер и сервер.
Семейство TCP/IP
Как мы уже знаем, в сети Интернет в качестве основного выбирается протокол TCP, хотя, конечно, этот выбор обусловлен скорее историческими причинами, нежели его действительными преимуществами (впрочем, преимуществ у TCP также предостаточно). Он ни в коей мере не претендует на роль низкоуровневого — наоборот, в свою работу он вовлекает другие протоколы, например, IP (в свою очередь, IP также базируется на услугах, предоставляемых некоторыми другими протоколами). Протоколы TCP и IP настолько сильно связаны, что принято объединять их в одну группу под названием
семейство TCP/IP (в него включается также протокол UDP, который мы рассматривать не будем). Ниже приводятся основные особенности протокола TCP, входящего в семейство.
r Корректная доставка данных до места назначения гарантируется — разумеется, если такая доставка вообще возможна. Даже если связь не вполне надежна (например, на линии помехи оттого, что в кабель попала вода, замерзшая зимой и разорвавшая оболочку провода),
"потерянные" фрагменты данных посылаются снова и снова до тех пор, пока вся информация не будет передана.
r Передаваемая информация представлена в виде потока — наподобие того, как осуществляется обмен с файлами практически во всех операционных системах. Иными словами, мы можем "открыть" соединение и затем выполнять с ним те же самые операции, к каким мы привыкли при работе с файлами. Таким образом, программы на разных машинах (возможно, находящихся за тысячи километров друг от друга), подключенных к Интернету, обмениваются данными так же непринужденно, как и расположенные на одном компьютере.
r TCP/IP устроен так, что он способен выбрать оптимальный путь распространения сигнала между передающей и принимающей стороной, даже если сигнал проходит через сотни промежуточных компьютеров. В последнем случае система выбирает путь, по которому данные могут быть переданы за минимальное время, основываясь при этом на статистическую информацию работы сети и так называемые таблицы маршрутизации.
r При передаче данные разбиваются на фрагменты — пакеты, которые и доставляются в место назначения по отдельности. Разные пакеты вполне могут следовать различными маршрутами в Интернете (особенно если их путь пролегает через десятки серверов), но для всех них гарантирована правильная "сборка" в месте назначения (в нужном порядке). Как уже упоминалось, принимающая сторона в случае обнаружения "недосдачи" пакета запрашивает передающую систему, чтобы та передала его еще раз. Все это происходит незаметно для программного обеспечения, эксплуатирующего TCP/IP.
В Web-программировании нам вряд ли придется работать с TCP/IP напрямую (разве что в очень экзотических случаях) — обычно можно использовать более высокоуровневые "языки", например, HTTP, служащий для обмена информацией между сервером и браузером. Собственно, этому протоколу посвящена значительная часть книги. Его мы рассмотрим подробно чуть позже. А пока давайте поговорим еще немного о том, что касается TCP/IP, чтобы не возвращаться к этому впоследствии.
Сериализация
Возможно, после прочтения описания функций implode()
и explode()
вы обрадовались, насколько просто можно сохранить массив, например, в файле, а затем его оттуда считать и быстро восстановить. Если вас посетила такая мысль, то, скорее всего, вы уже успели в ней разочароваться: во-первых, таким образом можно сохранять только массивы-списки (потому что ключи в любом случае теряются), а во-вторых, ничего не выйдет с многомерными массивами.
Давайте теперь предположим, что нам все-таки нужно сохранить какой-то массив (причем неизвестно заранее, сколько у него измерений) в файле, чтобы потом, при следующем запуске сценария, его аккуратно загрузить и продолжить работу. Можно, конечно, начинать писать универсальную рекурсивную функцию для упаковки массива в строку (ведь в файлы можно писать только строки), и еще одну, которая будет эту строку разбирать и восстанавливать на ее основе массив в исходном виде.
Рекомендую проделать это в качестве упражнения, заодно постарайтесь добиться, чтобы упакованные данные занимали минимум объема. Это пригодится вам в будущем, при работе с Cookies.
Однако вскоре вы поймете, что все не так просто в PHP, в котором работа со ссылочными переменными очень и очень ограничена. Особенно будет тяжело с функцией распаковки строки.
И тут нам на помощь опять приходят разработчики PHP. Оказывается, обе функции давным-давно реализованы, причем весьма эффективно со стороны быстродействия (но, к сожалению, непроизводительно с точки зрения объема упакованных данных). Называются они, соответственно, Serialize()
и Unserialize().
Функция Serialize() возвращает строку, являющуюся упакованным эквивалентом некоего объекта $Obj, переданного во втором параметре.
string Serialize(mixed $Obj)
При этом совершенно не важно, что это за объект: массив, целое число…. Да что угодно. Например:
$A=array("a"=>"aa", "b"=>"bb", "c"=>array("x"=>"xx"));
$st=Serialize($A);
echo $st;
// âûâåäåòñÿ ÷òî-òî òèïà нечто:
// a:2:{s:1:"a";s:2:"aa";s:1:"b";s:2:"bb";s:1:"c";a:1:{s:1:"x";s:2:"xx";}}
Вообще-то, я не уверен, что в будущих версиях PHP такой формат "упаковки" сохранится неизменным, хотя это очень и очень вероятно.
Функция Unserialize(), наоборот, принимает в лице своего
параметра $st строку, ранее созданную при помощи Serialize(), и возвращает целиком объект, который был упакован.
mixed Unserialize(string $st)
Например:
$a=array(1,2,3);
$s=Serialize($a);
$a="bogus";
echo count($a); // âûâîäèò 1
$a=Unserialize($s);
echo count($a); // âûâîäèò 3
Еще раз отмечу: сериализовать можно не только массивы, но и вообще что угодно. Однако в большинстве случаев все-таки используются массивы. Механизм сериализации часто применяется также и для того, чтобы сохранить какой-то объект в базе данных, и тогда без сериализации практически не обойтись.