или
), но неизвестно, какими. Требуется поместить эту подстроку в карман,
чтобы в
дальнейшем с ней работать. Разумеется, закрывающий тэг должен соответствовать
открывающему — например, к тэгу парный — , а к — .
Задача решается с помощью такого регулярного выражения:
<([[:alnum:]]+)>([^<]*)\1>
При этом результат окажется во втором кармане, а имя тэга — в первом. Вот как
это
работает: PHP пытается найти открывающий тэг, и, как только находит, записывает
его имя в первый карман (так как это имя обрамлено в выражении первой парой
ско-
бок). Дальше он смотрит вперед и, как только наталкивается на , определяет,
сле-
дует ли за ним то самое имя тэга, которое у него лежит в первом кармане. Это
дейст-
вие заставляет его предпринять конструкция \1, которая замещается на содержимое
первого кармана каждый раз, когда до нее доходит очередь. Если имя не совпадает,
то такой вариант PHP отбрасывает и "идет" дальше, а иначе сигнализирует о
совпа-
дении.
Вот фрагмент программы, который все описанное делает тремя строчками:
$str = "Hello, this word is bold!";
if(ereg("<([[:alnum:]]+)>([^<]*)\\1>",$str,$Pockets))
echo "Слово '$Pockets[2]' обрамлено тэгом '<$Pockets[1]>'";
Дополнительные функции
bool eregi(string $expr, string $str [,list &$Matches])
То же, что и ereg(), только без учета регистра символов.
Хотя регистр и не учитывается при поиске, в карманах $Matches все найден-
ные подстроки все же запишутся с точным сохранением регистра букв.
string eregi_replace(string $expr, strint $str, string $strToChange)
То же, что и ereg_replace(), но без учета регистра буквенных символов.
Часть IV. Стандартные функции PHP 308
int quotemeta(string $str)
Часто бывает нужно гарантировать, чтобы в какой-то переменной-строке ни один
символ не мог трактоваться как метасимвол. Этого можно добиться, предварив каж-
дый из них наклонной чертой, что и делает функция quotemeta(). А именно, она
"заслэшивает" следующие символы: . , \\, +, *, ? , [ ^ ] , ( $ ).
Перед | слэш почему-то не ставится. Будьте особо внимательны!
list split(string $pattern, string $string [,int $limit])
Эта функция очень похожа на explode(). Она тоже разбивает строку $string на
части, но делает это, руководствуясь регулярным выражением $pattern. А именно,
те участки строки, которые совпадают с этим выражением, и будут служить
раздели-
телями. Параметр $limit, если он задан, имеет то же самое значение, что и в
функ-
ции explode() — а именно, возвращается список из не более чем $limit элемен-
тов, последний из которых содержит участок строки от ($limit-1)-го совпадения
до
конца строки.
Наверное, вы уже догадались, что функция split() работает гораздо медленнее,
чем
explode(). Однако она, вместе с тем, имеет впечатляющие возможности, в чем мы
очень скоро убедимся. Тем не менее, не стоит применять split() там, где
прекрасно
подойдет explode(). Чаще всего этим грешат программисты, имеющие некоторый
опыт работы с Perl, потому что в Perl для разбиения строки на составляющие есть
только функция split().
list spliti(string $pattern, string $string [,int $limit])
Аналог функции split(), который делает то же самое, только при сопоставлении с
регулярным выражением не учитывается регистр символов.
Примеры использования
регулярных выражений
Какая же книга, описывающая (даже вкратце) регулярные выражения, обходится без
примеров…. Я не буду отступать от установленных канонов, хотя, конечно, понимаю,
что истинная свобода при работе с выражениями достигается только практикой. Не-
которые из следующих ниже примеров выглядят довольно сложно, но, если разо-
браться, смысл чаще всего оказывается на поверхности.
Имя и расширение файла
Задача: для имени файла в $fname установить расширение out независимо от его
предыдущего расширения.
Глава 22. Основы регулярных выражений в формате RegEx 309
Решение:
$fname=ereg_Replace(
'([[:alnum:]])(\\.[[:alnum:].]*)?$',
'\\1.out',
$fname
);
Обратите внимание на довольно интересную структуру этого выражения: мы не мо-
жем просто "привязать" его к концу строки при помощи $, что обусловлено
специфи-
кой работы RegEx. Мы также привязываем начало выражения к любой букве или
цифре, которой оканчивается имя файла.
Имя каталога и файла
Цель: разбить полное имя файла $path на имя каталога $dir и и имя файла $fname.
Средства:
$fname = ereg_Replace(".*[\\/]","",$path);
$dir = ereg_Replace("[\\/]?[^\\/]*$","",$path);
Проверка на идентификатор
Задача: проверить, является ли строка $id идентификатором, т. е. состоит ли она
ис-
ключительно из алфавитно-цифровых символов (чтобы сделать задачу более инте-
ресной, договоримся также, что первым символом строки не может быть цифра).
Решение:
if(eregi("[a-z_][[:alnum:]]*",$id)) echo "Это идентификатор!";
Модификация тэгов
Задача: в тексте, заданном в $text, у всех тэгов заменить в src
расширение
файла рисунка на gif, вне зависимости от того, какое расширение было у него до
этого и было ли вообще.
Решение:
$text=eregi_Replace(
'( ]*src="?[[:alnum:]/\\]*)(\\.[[:alnum:]]*)?',
'\\1.jpg',
$text
);
Часть IV. Стандартные функции PHP 310
Преобразование гиперссылок
Задача: имеется текст, в котором иногда встречаются подстроки вида
протокол://URL, где протокол — один из протоколов http, ftp или gopher, а URL —
какой-нибудь адрес в Интернете. Нужно заместить их на HTML-эквиваленты … .
Решение:
$w="[:alnum:]";
$p="[:punct:]";
$text=eregi_Replace(
"((https?|ftp|gopher)://". // протокол
"[$w-]+(\\.[$w-]+)*". // имя хоста
"(/[$w+&.%]*(\\?[$w?+&%]*)?)?". // имя файла и параметры
")",
'\\1 ',
$text
);
Преобразование адресов E-mail
Задача: имеется текст, в котором иногда встречаются строки вида
пользователь@хост, т. е. E-mail-адреса в обычном формате (или хотя бы большин-
ство таких E-mail). Необходимо преобразовать их в HTML-ссылки.
Решение:
$text=eregi_Replace(
'([[:alnum:]-.]+@'. // пользователь
'[[:alnum:]-]+(\\.[[:alnum:]-]+)*'. // домен
'(\\?([[:alnum:]?+&%]*)?)?'. // необязательные параметры
')',
'\\1 ',
$text
);
Этот пример, хоть и не безупречен, но все же преобразует правильно львиную долю
адресов электронной почты.
Глава 22. Основы регулярных выражений в формате RegEx 311
Выделение всех уникальных
слов из текста
Задача: перед нами некоторый довольно длинный текст в переменной $text. Необ-
ходимо выделить из него все слова и оставить из них только уникальные.
Результат
должен быть представлен в виде списка, отсортированного в алфавитном порядке.
Решение этой задачи может потребоваться, например, при написании индексирующей
поисковой системы на PHP.
Решение: воспользуемся функцией split() и ассоциативным массивом.
Листинг 22.1. Отбор уникальных слов
// Эта функция выделяет из текста в $text все уникальные слова и
// возвращает их список, отсортированный в алфавитном порядке.
function GetUniques($text)
{ // Сначала получаем все слова в тексте
$Words=split("[[:punct:][:blank:]]+",$text);
$Uniq=array(); // список уникальных слов
$Test=array(); // хэш уже обработанных слов
// Проходимся по всем словам в $Words и заносим в $Uniq уникальные
foreach($Words as $v) {
$v=strtolower($v); // в нижний регистр
// Слово уже нам встречалось? Если нет, то занести в $Uniq
if(!@$Test[$v]) $Uniq[]=$v;
// Указать, что это слово уже обрабатывалось
$Test[$v]=1;
}
// Наконец, сортируем список
sort($Uniq);
return $Uniq;
}
Данный пример довольно интересен, т. к. он имеет довольно большую функциональ-
ность при небольшом объеме. Его "сердце" — функция split() и цикл перебора
слов с отбором уникальных. Мы используем алгоритм, основанный на применении
ассоциативного массива для отбора уникальных элементов. Как он работает — наде-
юсь, ясно из комментариев.
Теперь мы можем воспользоваться функцией из листинга 22.1, например, в таком
контексте:
$fname="sometext.txt";
Часть IV. Стандартные функции PHP 312
$f=fopen($fname,"r");
$text=fread($f,filesize($fname));
fclose($f);
$Uniq=GetUniques($text);
foreach($Uniq as $v) echo "$v ";
Интересно будет отметить, что функция preg_split(), которая работает с
регулярными выражениями в формате PCRE, и которую мы не рассматриваем
в этой книге, показывает гораздо лучшую производительность в этом примере,
чем split() — чуть ли не в 3 раза быстрее! Если вам нужна максимальная
производительность, пожалуй, будет лучше воспользоваться именно ей, но
прежде почитайте что-нибудь о Perl и его регулярных выражениях — напри-
мер, в замечательной книге Perl Cookbook Тома Кристиансена и Ната Торкинг-
тона (русское издание: "Библиотека программиста. Perl", издательство Питер,
2000).
Заключение
Конечно, можно придумать и еще множество примеров применения регулярных вы-
ражений. Вы наверняка сможете это сделать самостоятельно — особенно после неко-
торой практики, которая так важна для понимания этого материала.
Однако я хочу обратить ваше внимание на то, что во многих задачах как раз не
обя-
зательно применять регулярные выражения. Так, например, задачи "поставить слэш
перед всеми кавычками в строке" и "заменить в строке все кавычки на ""
мож-
но и нужно решать при помощи str_replace(), а не ereg_Replace() (это сущест-
венно — раз в 20 — повысит быстродействие). Не забывайте, что регулярное выра-
жение — некоторого рода "насилие" над компьютером, принуждение делать нечто
такое, для чего он мало приспособлен. Этим объясняется медлительность
механизмов
обработки регулярных выражений, экспоненциально возрастающая с ростом сложно-
сти шаблона.
Глава 23
Работа с изображениями
Как мы знаем, одним из самых важных достижений WWW по сравнению со всеми
остальными службами Интернета стала возможность представления в браузерах
пользователей мультимедиа-информации, а не только "сухого" текста. Основной
объ-
ем этой информации приходится, конечно же, на изображения.
Разумеется, было бы довольно расточительно хранить и передавать все рисунки в
обыкновенном растровом формате (наподобие BMP), тем более, что современные
алгоритмы сжатия позволяют упаковывать такого рода данные в сотни и более раз
эффективней. Чаще всего для хранения изображений используются три формата сжа-
тия с перечисленными ниже свойствами.
r JPEG. Идеален для фотографий, но сжатие изображения происходит с потерями
качества, так что этот формат совершенно не подходит для хранения различных
диаграмм и графиков.
r GIF. Позволяет достичь довольно хорошего соотношения размер/качество, в то же
время не искажая изображение; применяется в основном для хранения небольших
точечных рисунков и диаграмм.
r PNG. Сочетает в себе хорошие стороны как JPEG, так и GIF, но в настоящий мо-
мент ему почему-то не выражают особого доверия — скорее, по историческим
причинам, из-за нежелания отказываться от GIF и т. д.
В последнее время GIF все более вытесняется форматом PNG, что связано в первую
очередь с окончанием действия бесплатной лицензии изобретателя на его использо-
вание. К сожалению, для небольших изображений GIF все еще остается самым опти-
мальным форматом, оставляя позади (иногда далеко позади) PNG.
Зачем может понадобиться в Web-программировании работа с изображениями? Разве
это не работа дизайнера?
В большинстве случаев это действительно так. Однако есть и исключения, например,
графические счетчики (автоматически создаваемые картинки с отображаемым поверх
числом, которое увеличивается при каждом "заходе" пользователя на страницу),
или
же графики, которые пользователь может строить в реальном времени — скажем,
диаграммы сбыта продукции или снижения цен на комплектующие. Все эти прило-
жения требуют как минимум умения генерировать изображения "на лету", причем с
довольно большой скоростью. Чтобы этого добиться на PHP, можно применить два
способа: задействовать какую-нибудь внешнюю утилиту для формирования изобра-
Глава 23. Работа с изображениями 315
жения (например, известную программу fly), или же воспользоваться встроенными
функциями PHP для работы с графикой. Оба способа имеют как достоинства, так и
недостатки, но, пожалуй, недостатков меньше у второго метода, так что им-то мы
и
займемся в этой главе.
С недавнего времени все программные продукты, которые умели формировать изо-
бражения в формате GIF, переориентируются на PNG. В частности, не так давно
ком-
пания, поддерживающая библиотеку GD для работы с GIF-изображениями, перепи-
сала ее код с учетом формата PNG. Так как PHP использует эту библиотеку, то
поддержка GIF автоматически исключилась и из него. К счастью, в Интернете все
еще можно найти старые версии GD с поддержкой GIF и, таким образом, настроить
PHP для работы с этим форматом, но задумайтесь: стоит ли теперь применять GIF,
если весь мир вполне успешно переходит на PNG, тем более, что его поддерживают
практически все современные браузеры (четвертой версии) — а это 98% от исполь-
зуемого их числа...
Универсальная функция
GetImageSize()
Что же, работать с картинками приходится часто — гораздо чаще, чем может пока-
заться на первый взгляд. Среди наиболее распространенных операций можно особо
выделить одну — определение размера рисунка. Чтобы сделать программистам
"жизнь раем", разработчики PHP встроили в него функцию, которая работает
практи-
чески со всеми распространенными форматами изображений, в том числе с GIF,
JPEG и PNG.
list GetIimageSize(string $filename [,array& $imageinfo])
Эта функция предназначена для быстрого определения в сценарии размеров (в
пиксе-
лах) и формата рисунка, имя файла которого передано ей в первом параметре. Она
возвращает список из четырех элементов. Первый элемент (с ключом 0) хранит ши-
рину картинки в пикселах, второй (с ключом 1) — его высоту. Ячейка массива с
клю-
чом 2 определяется форматом изображения: 0, если это GIF, 1 в случае JPG и 2
для
PNG. Следующий элемент, имеющий ключ 3, будет содержать после вызова функции
строку примерно следующего вида: height=sx width=sy, где sx и sy — соответст-
венно ширина и высота изображения. Это применение задумывалось для того, чтобы
облегчить вставку данных о размере изображения в тэг , который может быть
сгенерирован сценарием.
Часть IV. Стандартные функции PHP 316
Работа с изображениями
и библиотека GD
Давайте теперь рассмотрим идею создания рисунков сценарием "на лету". Например,
как мы уже замечали, это очень может пригодиться при создании сценариев-
счетчиков, графиков, картинок-заголовков да и многого другого.
Для деятельности такого рода существует специальная библиотека под названием
GD. Она содержит в себе множество функций (такие как рисование линий, растяже-
ние/сжатие изображения, заливка до границы, вывод текста и т. д.), которые
могут
использовать программы, поддерживающие работу с данной библиотекой. PHP (со
включенной поддержкой GD) как раз и является такой программой.
Поддержка GD включается при компиляции и установке PHP. Возможно, неко-
торые хостинг-провайдеры ее не имеют. Выясните, работает ли PHP вашего
хостера с библиотекой GD.
Пример
Начнем сразу с примера сценария, который представляет собой не HTML-страницу в
обычном смысле, а рисунок PNG. То есть URL этого сценария можно поместить в
тэг:
Как только будет загружена страница, содержащая указанный тэг, сценарий запус-
тится и отобразит надпись Hello world! на фоне рисунка, лежащего в
images/button.png. Полученная картинка нигде не будет храниться — она созда-
ется "на лету".
Рис. 23.1. Демонстрация
возможностей вывода
TrueType-шрифтов на PHP
Глава 23. Работа с изображениями 317
Листинг 23.1. Создание картинки "на лету"
// Получаем строку, которую нам передали в параметрах
$string=$QUERY_STRING;
// Загружаем рисунок фона с диска
$im = imageCreateFromPng("images/button.png");
// Создаем в палитре новый цвет — оранжевый
$orange = imageColorAllocate($im, 220, 210, 60);
// Вычисляем размеры текста, который будет выведен
$px = (imageSx($im)-7.5*strlen($string))/2;
// Выводим строку поверх того, что было в загруженном изображении
imageString($im,3,$px,9,$string,$orange);
// Сообщаем о том, что далее следует рисунок PNG
Header("Content-type: image/png");
// Теперь — самое главное: отправляем данные картинки в
// стандартный выходной поток, т. е. в браузер
imagePng($im);
// В конце освобождаем память, занятую картинкой
imageDestroy($im);
?>
Итак, мы получили возможность "на лету" создавать стандартные кнопки с разными
надписями, имея только "шаблон" кнопки.
Создание изображения
Давайте теперь разбираться, как работать с картинками в GD. Для начала нужно
кар-
тинку создать — пустую (при помощи imageCreate()) или же загруженную с диска
(imageCreateFromPng(), imageCreateFromJpeg() или
imageCreateFromGif(), в зависимости от того, какие форматы поддерживаются
PHP и GD).
int imageCreate(int $x, int $y)
Создает пустую картинку размером $x на $y точек и возвращает ее идентификатор.
После того, как картинка создана, вся работа с ней осуществляется именно через
этот
идентификатор, по аналогии с тем, как мы работаем с файлом через его дескриптор.
int imageCreateGromPng(string $filename) или
int imageCreateGromJpeg(string $filename) или
int imageCreateGromif(string $filename)
Часть IV. Стандартные функции PHP 318
Эти функции загружают изображения из файла в память и возвращают его иденти-
фикатор. Как и после вызова imageCreate(), дальнейшая работа с картинкой
возможна только через этот идентификатор. При загрузке с диска изображение
распаковывается и хранится в памяти уже в неупакованном формате, для того чтобы
можно было максимально быстро производить с ним различные операции, такие как
масштабирование, рисование линий
и т. д. При сохранении на диск или выводе в браузер функцией imagePng() (или,
соответственно, imageJpeg() и imageGif()) картинка автоматически упаковывает-
ся.
Стоит заметить, что не обязательно все три функции будут доступны в вашей
версии PHP.
Скорее всего, в ней не окажется функции imageCreateFromGif(), а возможно, и
imageCreateFromJpeg(), потому что от первой разработчики GD просто отказа-
лись, а вторая появилась сравнительно недавно. Надеюсь, в скором времени
ситуация
нормализуется, но сейчас, к сожалению, это не так.
Определение параметров
изображения
Как только картинка создана и получен ее идентификатор, GD становится совершен-
но все равно, какой формат она имеет и каким путем ее создали. То есть все
осталь-
ные действия с картинкой происходят через ее идентификатор, вне зависимости от
формата, и это логично — ведь в памяти изображение все равно хранится в
распако-
ванном виде (наподобие BMP), а значит, информация о ее формате нигде не исполь-
зуется. Так что вполне возможно открыть PNG-изображение с помощью
imageCreateFromPng() и сохранить ее на диск функцией imageJpeg(), уже в дру-
гом формате. В дальнейшем можно в любой момент времени определить размер за-
груженной картинки, воспользовавшись функциями imageSX() и imageSY():
int imageSX(int $im)
Функция возвращает горизонтальный размер изображения, заданного своим иденти-
фикатором, в пикселах.
int imageSY(int $im)
Возвращает высоту картинки в пикселах.
int imageColorsTotal(int $im)
Эту функцию имеет смысл применять только в том случае, если вы работаете с изо-
бражениями, "привязанными" к конкретной палитре — например, с файлами GIF.
Она возвращает текущее количество цветов в палитре. Как мы вскоре увидим, каж-
дый вызов imageColorAllocate() увеличивает размер палитры. В то же время
известно, что если при небольшом размере палитры GIF-картинка сжимается очень
хорошо, то при переходе через степень двойки (например, от 16 к 17 цветам)
эффек-
тивность сжатия заметно падает, что ведет к увеличению размера (так уж устроен
Глава 23. Работа с изображениями 319
формат GIF). Если мы не хотим этого допустить и собираемся вызывать
imageColorAllocate() только до предела 16 цветов, а затем перейти на использо-
вание imageColorClosest(), нам очень может пригодиться рассматриваемая функ-
ция.
Сохранение изображения
Давайте займемся функцией, поставленной в листинге 23.1 "на предпоследнее
место",
которая, собственно, и выполняет большую часть работы — выводит изображение в
браузер пользователя. Оказывается, эту же функцию можно применять и для сохра-
нения рисунка в файл.
int imagePng(int $im [,string $filename]) или
int imageJpeg(int $im [,string $filename]) или
int imageGif(int $im [,string $filename])
Эти функции сохраняют изображение, заданное своим идентификатором и находя-
щееся в памяти, на диск, или же выводят его в браузер. Разумеется, вначале
изобра-
жение должно быть загружено или создано при помощи функции imageCreate(),
т. е. мы должны знать его идентификатор $im.
Если аргумент $filename опущен, то сжатые данные в соответствующем формате
выводятся прямо в стандартный выходной поток, т. е. в браузер. Нужный заголовок
Content-type при этом не выводится, ввиду чего нужно выводить его вручную при
помощи Header(), как это было показано в примере из листинга 23.1.
Некоторые браузеры не требуют вывода правильного Content-type, а опре-
деляют, что перед ними рисунок, по нескольким первым байтам присланных
данных. Ни в коем случае не полагайтесь на это! Дело в том, что все еще су-
ществуют браузеры, которые этого делать не умеют. Кроме того, такая техника
идет вразрез со стандартами HTTP.
Фактически, вы должны вызвать одну из трех команд, в зависимости от типа
изобра-
жения:
Header("Content-type: image/png") для PNG
Header("Content-type: image/jpeg") для JPEG
Header("Content-type: image/gif") для GIF
Рекомендую их вызывать не в начале сценария, а непосредственно перед вызовом
imagePng(), imageGif() или imageJpeg(), поскольку иначе вы не сможете никак
увидеть сообщения об ошибках и предупреждения, которые, возможно, будут сгене-
рированы программой.
Часть IV. Стандартные функции PHP 320
К рассмотренным только что функциям можно сделать точно такие же замеча-
ния, как и к семейству imageCreateFromXXX(), т. е., некоторые из них могут
отсутствовать — скорее всего, последняя. Однако случаются и забавные казу-
сы. Я видел версию PHP, в которой не поддерживалась вообще ни одна из
этих функций, ровно как и функции imageCreateFromXXX(). В то же время
imageCreate() работала (во всяком случае, так казалось). Возникает инте-
ресный вопрос: мы можем создавать изображения, рисовать в них линии, кру-
ги, выводить текст, но не в состоянии ни сохранить их где-нибудь, ни даже за-
грузить уже готовую картинку с диска. Зачем тогда вообще были нужны все
остальные функции?..
Работа с цветом в формате RGB
Наверное, теперь вам захочется что-нибудь нарисовать на пустой или только что
за-
груженной картинке. Но чтобы рисовать, нужно определиться, каким цветом это де-
лать. Проще всего указать цвет заданием тройки RGB-значений (от red-green-blue)
—
это три цифры от 0 до 255, определяющие содержание красной, зеленой и синей со-
ставляющих в нужном нам цвете. Число 0 обозначает нулевую яркость соответст-
вующей компоненты, а 255 — максимальную интенсивность. Например, (0,0,0) зада-
ет черный цвет, (255,255,255) — белый, (255,0,0) — ярко-красный, (255,255,0) —
желтый и т. д.
Правда, GD не умеет работать с такими тройками напрямую. Она требует, чтобы пе-
ред использованием RGB-цвета был получен его специальный идентификатор. Даль-
ше вся работа опять же осуществляется через этот идентификатор. Скоро станет
ясно,
зачем нужна такая техника.
Создание нового цвета
int imageColorAllocate(int $im, int $red, int $green, int $blue)
Функция возвращает идентификатор цвета, связанного с соответствующей тройкой
RGB. Обратите внимание, что первым параметром функция требует идентификатор
изображения, загруженного в память или созданного до этого. Практически каждый
цвет, который планируется в дальнейшем использовать, должен быть получен (опре-
делен) при помощи вызова этой функции. Почему "практически" — станет ясно после
рассмотрения функции imageColorClosest().
Получение ближайшего цвета
Давайте разберемся, зачем это придумана такая технология работы с цветами через
промежуточное звено — идентификатор цвета. А дело все в том, что некоторые фор-
маты изображений (такие как GIF и частично PNG) не поддерживают любое количе-
Глава 23. Работа с изображениями 321
ство различных цветов в изображении. А именно, в GIF количество одновременно
присутствующих цветов ограничено цифрой 256, причем чем меньше цветов исполь-
зуется в рисунке, тем лучше он "сжимается" и тем меньший размер имеет файл. Тот
набор цветов, который реально использован в рисунке, называется его палитрой.
Представим себе, что произойдет, если все 256 цветов уже "заняты" и вызывается
функция imageColorAllocate(). В этом случае она обнаружит, что палитра запол-
нена полностью, и найдет среди занятых цветов тот, который ближе всего
находится
к запрошенному — будет возвращен именно его идентификатор. Если же "свободные
места" в палитре есть, то они и будут использованы этой функцией (конечно, если
в
палитре вдруг не найдется точно такой же цвет, как запрошенный — обычно дубли-
рование одинаковых цветов всячески избегается).
int imageColorClosest(int $im, int $red, int $green, int $blue)
Наверное, вы уже догадались, зачем нужна функция imageColorClosest(). Вместо
того чтобы пытаться выискать свободное место в палитре цветов, она просто
возвра-
щает идентификатор цвета, уже существующего в рисунке и находящегося ближе все-
го к затребованному. Таким образом, нового цвета в палитру не добавляется. Если
палитра невелика, то функция может вернуть не совсем тот цвет, который вы
ожидае-
те. Например, в палитре из трех цветов "красный-зеленый-синий" на запрос
желтого
цвета будет, скорее всего, возвращен идентификатор зеленого — он "ближе всего"
с
точки зрения GD соответствует понятию "зеленый".
Эффект прозрачности
Функцию imageColorClosest() можно и нужно использовать, если мы не хотим
допустить разрастания палитры и уверены, что требуемый цвет в ней уже есть.
Одна-
ко есть и другое, гораздо более важное, ее применение — определение эффекта
про-
зрачности для изображения. "Прозрачный" цвет рисунка — это просто те точки, ко-
торые в браузер не выводятся. Таким образом, через них "просвечивает" фон.
Прозрачный цвет у картинки всегда один, и задается он при помощи функции
imageColorTransparent().
int imageColorTransparent(int $im [,$int col])
Функция imageColorTransparent() указывает GD, что соответствующий цвет
$col (заданный своим идентификатором) в изображении $im должен обозначиться
как прозрачный. Возвращает она идентификатор установленного до этого прозрачно-
го цвета, либо false, если таковой не был определен ранее.
Не все форматы поддерживают задание прозрачного цвета — например, JPEG
не может его содержать.
Например, мы нарисовали при помощи GD птичку на кислотно-зеленом фоне и хо-
тим, чтобы этот фон как раз и был "прозрачным" (вряд ли у птички есть части
тела
Часть IV. Стандартные функции PHP 322
такого цвета, хотя с нашей экологией все может быть...). В этом случае нам
потребу-
ются такие команды:
$tc=imageColorClosest($im,0,255,0);
imageColorTransparent($im,$tc);
Обратите внимание на то, что применение функции imageColorAllocate() здесь
совершенно бессмысленно, потому что нам нужно сделать прозрачным именно тот
цвет, который уже присутствует в изображении, а не новый, только что созданный.
Получение RGB-составляющих
array imageColorsForIndex(int $im, int $index)
Функция возвращает ассоциативный массив с ключами red, green и blue (именно в
таком порядке), которым соответствуют значения, равные величинам компонент RGB
в идентификаторе цвета $index. Впрочем, мы можем и не обращать особого внима-
ния на ключи и преобразовать возвращенное значение как список:
$c=imageColorAt($i,0,0);
list($r,$g,$b)=array_values(imageColorsForIndex($i,$c));
echo "R=$r, g=$g, b=$b";
Эта функция ведет себя противоположно по отношению к imageCollorAllocate()
или imageColorClosest().
Графические примитивы
Здесь мы рассмотрим минимальный набор функций для работы с картинками. При-
веденный список функций не полон и постоянно расширяется вместе с развитием GD.
Но все же он содержит те функции, которые вы будете употреблять в 99% случаев.
За
полным списком функций обращайтесь к документации или на http://ru.php.net.
Копирование изображений
int imageCopyResized(int $dst_im, int $src_im, int $dstX, int $dstY,
int $srcX, int $srcY, int $dstW, int $dstH,
int $srcW, int $srcH)
Эта функция — одна из самых мощных и универсальных, хотя и выглядит просто
ужасно. С помощью нее можно копировать изображения (или их участки), переме-
щать и масштабировать их…. Пожалуй, 10 параметров для функции — чересчур, но
разработчики PHP пошли таким путем. Что же, это их право...
Итак, $dst_im задает идентификатор изображения, в который будет помещен ре-
зультат работы функции. Это изображение должно уже быть создано или загружено и
иметь надлежащие размеры. Соответственно, $src_im — идентификатор изображе-
Глава 23. Работа с изображениями 323
ния, над которым проводится работа. Впрочем, $src_im и $dst_im могут и совпа-
дать.
Параметры ($srcX, $srcY, $srcW, $srcH) (обратите внимание на то, что они следу-
ют при вызове функции не подряд!) задают область внутри исходного изображения,
над которой будет осуществлена операция — соответственно, координаты ее
верхнего
левого угла, ширину и высоту.
Наконец, четверка ($dstX, $dstY, $dstW, $dstH) задает то место на изображении
$dst_im, в которое будет "втиснут" указанный в предыдущей четверке прямоуголь-
ник. Заметьте, что, если ширина или высота двух прямоугольников не совпадают,
то
картинка автоматически будет нужным образом растянута или сжата.
Таким образом, с помощью функции imageCopyResized() мы можем:
r копировать изображения;
r копировать участки изображений;
r масштабировать участки изображений;
r копировать и масштабировать участки изображения в пределах одной картинки.
В последнем случае возникают некоторые сложности, а именно, когда тот блок, из
которого производится копирование, частично налагается на место, куда он должен
быть перемещен. Убедиться в этом проще всего экспериментальным путем. Почему
разработчики GD не предусмотрели средств, которые бы корректно работали и в
этом
случае, не совсем ясно.
Прямоугольники
int imageFilledRectangle(int $im,int $x1,int $y1,int $x2,int $y2,int $c)
Название этой функции говорит за себя: она рисует закрашенный прямоугольник в
изображении, заданном идентификатором $im, цветом $col (полученным, например,
при помощи функции imageColorAllocate()). Координаты ($x1,$y1) и ($x2,$y2)
задают координаты верхнего левого и правого нижнего углов, соответственно
(отсчет,
как обычно, начинается с левого верхнего угла и идет слева направо и сверху
вниз).
Эта функция часто применяется для того, чтобы целиком закрасить только что соз-
данный рисунок, например, прозрачным цветом:
$i=imageCreate(100,100);
$c=imageColorAllocate($i,0,0,0);
imageColorTransparent($i,$c);
imageFilledRectangle($i,0,0,imageSX($i)-1,imageSY($i)-1,$c);
// дальше работаем с изначально прозрачным фоном
int imageRectangle(int $im, int $x1, int $y1, int $x2, int $y2, int $col)
Функция imageRectangle() рисует в изображении прямоугольник с границей тол-
щиной 1 пиксел цветом $col. Параметры задаются так же, как и в функции
imageFilledRectangle().
Часть IV. Стандартные функции PHP 324
Линии
int imageLine(int $im, int $x1, int $y1, int $x2, int $y2, int $col)
Эта функция рисует сплошную тонкую линию в изображении $im, проходящую через
точки ($x1,$y1) и ($x2,$y2), цветом $col. Линия получается слабо связанной (про
связность см. чуть ниже).
int imageDashedLline(int $im,int $x1,int $y1,int $x2,int $y2,int $col)
Функция imageDashedLine() работает почти так же, как и imageLine(), только
рисует не сплошную, а пунктирную линию. К сожалению, ни размер, ни шаг штрихов
задавать нельзя, так что, если вам обязательно нужна пунктирная линия
произволь-
ной фактуры, придется заняться математическими расчетами и использовать
imageLine().
Дуга сектора
int imageArc(int $im,int $cx,int $cy,int $w,int $h,int $s,int $e,int $c)
Функция imageArc() рисует в изображении $im дугу сектора эллипса от угла $s до
$e (углы указываются в градусах против часовой стрелки, отсчитываемых от гори-
зонтали). Эллипс рисуется такого размера, чтобы вписываться в прямоугольник
($x,$y,$w,$h), где $w и $h задают его ширину и высоту, а $x и $y — координаты
ле-
вого верхнего угла. Сама фигура не закрашивается, обводится только ее контур,
для
чего используется цвет $c.
Закраска произвольной области
int imageFill(int $im, int $x, int $y, int $col)
Функция imageFill() выполняет сплошную заливку одноцветной области, содер-
жащей точку с координатами ($x,$y) цветом $col. Нужно заметить, что современные
алгоритмы заполнения работают довольно эффективно, так что не стоит особо забо-
титься о скорости ее работы. Итак, будут закрашены только те точки, к которым
можно проложить "одноцветный сильно связанный путь" из точки ($x,$y).
Две точки называются связанными сильно, если у них совпадает по крайней
мере одна координата, а по другой координате они отличаются не более, чем
на 1 в любую сторону.
int imageFillToBorder(int $im, int $x, int $y, int $border, int $col)
Эта функция очень похожа на imageFill(), только она выполняет закраску не од-
ноцветных точек, а любых, но до тех пор, пока не будет достигнута граница цвета
Глава 23. Работа с изображениями 325
$border. Под границей здесь понимается последовательность слабо связанных то-
чек.
Две точки называются слабо связанными, если каждая их координата отлича-
ется от другой не более, чем на 1 в любом направлении. Очевидно, всякая
сильная связь является также и слабой.
Многоугольники
int imagePolygon(int $im, list $points, int $num_points, int $col)
Эта функция рисует в изображении $im многоугольник, заданный своими вершина-
ми. Координаты углов передаются в массиве-списке $points, причем
$points[0]=x0, $points[1]=y0, $points[2]=x1, $points[3]=y1, и т. д. Пара-
метр $num_points указывает общее число вершин — на тот случай, если в массиве
их больше, чем нужно нарисовать. Многоугольник не закрашивается — только рису-
ется его граница цветом $col.
int imageFilledPolygon(int $im, list $points, int $num_points, int $col)
Функция imageFilledPolygon() делает практически то же самое, что и
imagePolygon(), за исключением одного очень важного свойства: полученный мно-
гоугольник целиком заливается цветом $col. При этом правильно обрабатываются
вогнутые части фигуры, если она не выпукла.
Работа с пикселами
int imageSetPixel(int $im, int $x, int $y, int $col)
Эта функция практически не интересна, т. к. выводит всего один пиксел цвета
$col в
изображении $im, расположенный в точке ($x,$y). Не думаю, чтобы с помощью нее
можно было закрасить хоть какую-нибудь сложную фигуру, потому что, как мы зна-
ем, PHP довольно медленно работает с длинными циклами, а значит, даже рисование
обычной линии с использованием этой функции будет очень дорогим занятием.
int imageColorAt(int $im, int $x, int $y)
В противоположность своему антиподу — функции imageSetPixel() — функция
imageColorAt() не рисует, а возвращает цвет точки, расположенной на координа-
тах ($x,$y). Возвращается идентификатор цвета, а не его RGB-представление.
Функцию удобно использовать, опять же, для определения, какой цвет в картинке
должен быть прозрачным. Например, все у той же птички на кислотно-зеленом фоне
мы достоверно знаем, что прозрачный цвет точно приходится на точку с
координата-
ми (0,0). Таким образом, теперь мы сможем в любой момент сменить цвет фона на
Часть IV. Стандартные функции PHP 326
мертвенно-голубой (который тоже у реальной птицы вряд ли встретится), и
програм-
ма все равно будет работать правильно.
Работа с фиксированными шрифтами
Библиотека GD имеет некоторые возможности по работе с текстом и шрифтами.
Шрифты представляют собой специальные ресурсы, имеющие собственный иденти-
фикатор, и чаще всего загружаемые из файла или встроенные в GD. Каждый символ
шрифта может быть отображен лишь в моноцветном режиме, т. е. "рисованные" сим-
волы не поддерживаются. Встроенных шрифтов всего 5 (идентификаторы от 1 до 5),
чаще всего в них входят моноширинные символы разных размеров. Остальные
шрифты должны быть предварительно загружены.
Загрузка шрифта
int imageLoadFont(string $file)
Функция загружает файл шрифтов и возвращает идентификатор шрифта — это будет
цифра, большая 5, потому что пять первых номеров зарезервированы как встроенные.
Формат файла — бинарный, а потому зависит от архитектуры машины. Это значит,
что файл со шрифтами должен быть сгенерирован по крайней мере на машине с про-
цессором такой же архитектуры, как и у той, на котором вы собираетесь
использовать
PHP. Вот формат этого файла (табл. 23.1). Левая колонка задает смещение начала
данных внутри файла, а группами цифр, записанных через дефис, определяется, до
какого адреса продолжаются данные.
Таблица 23.1. Формат файла со шрифтом
Смещение Тип Описание
Byte 0-3 long Число символов в шрифте (nchars)
byte 4-7 long Индекс первого символа шрифта (обычно 32 — пробел)
Таблица 23.1 (окончание)
Смещение Тип Описание
byte 8-11 long Ширина (в пикселах) каждого знака (width)
byte 12-15 long Высота (в пикселах) каждого знака (height)
byte 16-... array Массив с информацией о начертании каждого символа, по
одному байту на пиксел. На один символ, таким образом,
приходится width*height байтов, а на все —
width*height*nchars байтов. 0 означает отсутствие точки в
данной позиции, все остальное — ее присутствие
Глава 23. Работа с изображениями 327
Параметры шрифта
После того как шрифт загружен, его можно использовать (встроенные шрифты, ко-
нечно же, загружать не требуется).
int imageFontHeight(int $font)
Возвращает высоту в пикселах каждого символа в заданном шрифте.
int imageFontWidth(int $font)
Возвращает ширину в пикселах каждого символа в заданном шрифте.
Вывод строки
int imageString(int $im, int $font, int $x, int $y, string $s, int $col)
Выводит строку $s в изображение $im, используя шрифт $font и цвет $col. Коор-
динаты ($x,$y) будут координатами левого верхнего угла прямоугольника, в
который
вписана строка.
int imageStringUp(int $im, int $font, int $x, int $y, string $s, int $c)
Эта функция также выводит строку текста, но не в горизонтальном, а в
вертикальном
направлении. Верхний левый угол, по-прежнему, задается координатами ($x,$y).
Работа со шрифтами TrueType
Библиотека GD поддерживает также работу со шрифтами PostScript и TrueType. Мы с
вами рассмотрим только последние, т. к., во-первых, их существует великое
множе-
ство (благодаря платформе Windows), а во-вторых, с ними проще всего работать в
PHP.
Для того чтобы заработали приведенные ниже функции, PHP должен быть от-
компилирован и установлен вместе с библиотекой FreeType, доступной по ад-
ресу http://www.freetype.org. В Windows-версии PHP она установлена по
умолчанию.
Всего существует две функции для работы со шрифтами TrueType. Одна из них вы-
водит строку в изображение, а вторая — определяет, какое положение эта строка
бы
заняла при выводе.
Вывод строки
list imageTTFText(int $im, int $size, int $angle, int $x, int $y,
int $col, string $fontfile, string $text)
Часть IV. Стандартные функции PHP 328
Эта функция помещает строку $text в изображение $im цветом $col. Как обычно,
$col должен представлять собой допустимый идентификатор цвета. Параметр
$angle задает угол наклона в градусах выводимой строки, отсчитываемый от гори-
зонтали против часовой стрелки. Координаты ($x,$y) указывают положение так на-
зываемой базовой точки строки (обычно это ее левый нижний угол). Параметр
$size задает размер шрифта, который будет использоваться при выводе строки. На-
конец, $fontfile должен содержать имя TTF-файла, в котором, собственно, и хра-
нится шрифт.
Хотя в официальной документации об этом ничего не сказано, я рискну взять
на себя ответственность и заявить, что параметр $fontfile должен всегда
задавать абсолютный путь (от корня файловой системы) к требуемому файлу
шрифтов. Что самое интересное, в PHP версии 3 функции все же работают с
относительными именами. Но в любом случае лучше подстелить соломку —
абсолютные пути еще никому не вредили, не правда ли?..
Функция возвращает список из 8 элементов. Первая их пара задает координаты (x,
y)
верхнего левого угла прямоугольника, описанного вокруг строки текста в
изображе-
нии, вторая пара — координаты верхнего правого угла,
и т. д. Так как в общем случае строка может иметь любой наклон $angle, здесь
тре-
буются 4 пары координат.
Вот пример использования этой функции:
Листинг 23.2. Вывод TrueType-строки
// Выводимая строка
$string="Hello world!";
// Создаем рисунок подходящего размера
$im = imageCreate(300,40);
// Создаем в палитре новые цвета
$black = imageColorAllocate($im, 0, 0, 0);
$orange = imageColorAllocate($im, 220, 210, 60);
// Закрашиваем картинку
imageFill($im,0,0,$black);
// Рисуем строку текста (файл times.ttf расположен в текущем каталоге)
imagettftext($im,50,0,20,35,$orange,getcwd()."/times.ttf",$string);
// Сообщаем о том, что далее следует рисунок PNG
Header("Content-type: image/png");
// Выводим рисунок
Глава 23. Работа с изображениями 329
imagePng($im);
?>
Определение границ строки
list imageTTFBBox(int $size, int $angle, string $fontfile, string $text)
Эта функция ничего не выводит в изображение, а просто определяет, какой размер
и
положение заняла бы строка текста $text размера $size, выведенная под углом
$angle в какой-нибудь рисунок. Параметр $fontfile, как и в функции
imageTTFText(), задает абсолютный путь к файлу шрифта, который будет исполь-
зован при выводе.
Возвращаемый список содержит всю информацию о размерах строки в формате, по-
хожем на тот, что выдает функция imageTTFText(). Однако порядок точек в нем
отличается (табл. 23.2).
Таблица 23.2. Содержимое списка, возвращаемого функцией
Индексы Что содержится
0 и 1 (x,y) левого нижнего угла
2 и 3 (x,y) правого нижнего угла
4 и 5 (x,y) правого верхнего угла
4 и 5 (x,y) левого верхнего угла
Пример
В листинге 23.3 я привожу пример сценария, который использует возможности выво-
да TrueType-шрифтов, а также демонстрирует работу с цветом RGB. Хотя размер
примера довольно велик, рисунок, который он генерирует, выглядит довольно при-
влекательно (см. рис. 23.1).
Листинг 23.3. Вывод строки произвольного формата
// Аналог imageColorAllocate() (по умолчанию), но работает не с
// RGB-тройкой, а с цветом в формате XXYYZZ, где:
// * XX — red-составляющая в шестнадцатеричном формате;
// * YY — green-составляющая в шестнадцатеричном формате;
// * ZZ — blue-составляющая в шестнадцатеричном формате.
Часть IV. Стандартные функции PHP 330
// Можно указать другую функцию получения цвета, задав ее
// имя в параметре $func (например, imageColorClosest).
function imageColorHex($im, $c, $func="imageColorAllocate")
{ // Сначала дополняем нулями в начале, если нужно
for($i=strlen($c); $i<6; $i++) $c='0'.$c;
$r=hexdec(substr($c,0,2));
$g=hexdec(substr($c,2,2));
$b=hexdec(substr($c,4,2));
return $func($im,$r,$g,$b);
}
// Первым делом устанавливаем параметры по умолчанию. Эти
// параметры можно переопределять при вызове сценария
// (например, ttf.php?a=20&f=arial&text=Hi+there)
if(!@$a) $a=30; // угол поворота (по умолчанию 30)
if(!@$s) $s=80; // размер шрифта (80)
if(!@$b) $b="00AAAA"; // цвет заднего плана (зеленовато-голубой)
if(!@$c) $c="FFFF00"; // цвет букв (ярко-желтый)
if(!@$d) $d=10; // зазор между текстом и границей рисунка
if(!@$f) $f="times"; // шрифт
if(!@$text) $text="Hello world!"; // текст
// Получаем границы рамки текста
$Bnd=imageTTFBBox($s,$a,getcwd()."/$f.ttf",$text);
// Массивы x- и y-координат всех точек
$X=$Y=array();
// Заполняем эти массивы на основании $Bnd
for($i=0; $i<4; $i++) {
$X[]=$Bnd[$i*2];
$Y[]=$Bnd[$i*2+1];
}
// Вычисляем размер картинки с учетом зазора $d
$MX=max($X)-min($X)+$d*2; // размер по x
$MY=max($Y)-min($Y)+$d*2; // размер по y
// Теперь вычисляем координаты базовой точки строки, чтобы
// она располагалась точно по центру поля картинки
$x=$d+$Bnd[0]-min($X)+2;
$y=$d+$Bnd[1]-min($Y)+2;
// Создаем рисунок нужного размера
Глава 23. Работа с изображениями 331
$im = imageCreate($MX,$MY);
// Создаем в палитре новые цвета
$black = imageColorHex($im, 0); // черный (тень)
$back = imageColorHex($im, $b); // задний план
$front = imageColorHex($im, $c); // цвет букв
// Очищаем задний план
imageFill($im,0,0,$back);
imageRectangle($im,0,0,$MX-1,$MY-1,$black);
// Выводим тень от текста
imagettftext($im,$s,$a,$x+2,$y+2,$black,getcwd()."/$f.ttf",$text);
// Выводим текст
imagettftext($im,$s,$a,$x,$y,$front,getcwd()."/$f.ttf",$text);
// Выводим рисунок в браузер
Header("Content-type: image/png");
imagePng($im);
?>
Сценарий из листинга 23.3 (назовем его ttf.php) генерирует картинку с заданным
цветом заднего плана, в которую выводится указанная строка с тенью. При этом
ис-
пользуется TrueType-шрифт, а также определяются размер строки, угол ее наклона,
цвет и т. д.
Формат вызова сценария имеет следующий общий вид:
ttf.php?a=Градусы&s=Размер&b=ЗаднийЦвет&c=Цвет&d=Зазор&f=Фонт&text=Текст
Ни один из этих параметров не является обязательным — в случае пропуска
подстав-
ляются значения по умолчанию (см. листинг 23.3).
Необходимо заметить, что прежде, чем запускать сценарий, нужно скопировать TTF-
файл со шрифтом в каталог, где расположена программа (например, взяв его из
C:/WINDOWS/FONTS для платформы Windows). Параметр f задает имя этого файла
без расширения, и ищется он в текущем каталоге. По умолчанию выбран шрифт
Times.
Глава 24
Управление
интерпретатором
PHP, как и любая другая крупная программа, имеет множество различных настроеч-
ных параметров. Слава богу, большинство из них по умолчанию уже имеют правиль-
ные значения. Тем не менее, нередко приходится эти параметры изменять или
прове-
рять. В этой главе мы вкратце рассмотрим основные возможности конфигурирования
PHP и некоторые полезные функции, управляющие работой интерпретатора.
Информационные
функции
Прежде всего давайте познакомимся с двумя функциями, одна из которых выводит
текущее состояние всех параметров PHP, а вторая — версию интерпретатора.
int phpinfo()
Эта функция, которая в общем-то не должна появляться в законченной программе,
выводит в браузер большое количество различной информации, касающейся настроек
PHP и параметров вызова сценария. Именно, в стандартный выходной поток (то есть
в браузер пользователя) печатается:
r версия PHP;
r опции, которые были установлены при компиляции PHP;
r информация о дополнительных модулях;
r переменные окружения, в том числе и установленные сервером при получении
запроса от пользователя на вызов сценария;
r версия операционной системы;
r состояние основных и локальных настроек интерпретатора;
r HTTP-заголовки;
r лицензия PHP.
Глава 24. Управление интерпретатором 333
Как видим, вывод довольно объемист. Воочию в этом можно убедиться, запустив
такой сценарий:
phpinfo();
?>
Надо заметить, что функция phpinfo() в основном применяется при первоначаль-
ной установке PHP для проверки его работоспособности. Думаю, для других целей
использовать ее вряд ли целесообразно — слишком уж много информации она выда-
ет.
string phpversion()
Функция phpversion(), пожалуй, могла бы по праву занять первое место на сорев-
нованиях простых функций, потому что все, что она делает — возвращает текущую
версию PHP.
int getlastmod()
Завершающая функция этой серии — getlastmod() — возвращает время последне-
го изменения файла, содержащего сценарий. Она не так полезна, как это может
пока-
заться на первый взгляд, потому что учитывает время изменения только главного
файла, того, который запущен сервером, но не файлов, которые включаются в него
директивами require или include. Время возвращается в формате timestamp (то
есть, это число секунд, прошедших с 1 января 1970 года до момента модификации
файла), и оно может быть затем преобразовано в читаемую форму, например:
echo "Последнее изменение: ".date("d.m.Y H:i.s.", getlastmod());
// Выводит что-то вроде 'Последнее изменение: 13.11.2000 11:23.12'
Настройка параметров PHP
Все параметры находятся в файле php.ini. Задаются они в формате
параметр=значение, на одной строке может определяться только один параметр.
Любые символы, расположенные после ; и до конца строки, игнорируются (таким
образом, точка с запятой — это признак начала комментария).
Если PHP установлен как модуль Apache, применяется несколько другой способ кон-
фигурирования. Можно задавать настройки PHP в главном конфигурационном файле
сервера httpd.conf или в файлах .htaccess. Только для этого перед именем каж-
дого параметра нужно поставить префикс php_ и, конечно же, как это принято в
Apache, разделять имя параметра и его значение не знаком равенства, а пробелом.
Некоторые из следующих далее настроек можно переопределить в сценарии с помо-
щью специальных функций (такой, например, как Error_Reporting()), некото-
рые — нельзя. За полным списком настроечных директив PHP обращайтесь к При-
ложению 2.
Часть IV. Стандартные функции PHP 334
error_reporting
Устанавливает уровень строгости для системы контроля ошибок PHP. Значение этого
параметра должно представлять из себя целое число, которое интерпретируется как
десятичное представление двоичной битовой маски. Установленные в 1 биты задают,
насколько детальным должен быть контроль. Можно также не возиться с битами, а
использовать константы.
Таблица 24.1. Биты, управляющие контролем ошибок
Бит Константа PHP Назначение
1 E_ERROR Фатальные ошибки
2 E_WARNING Общие предупреждения
4 E_PARSE Ошибки трансляции
8 E_NOTICE Предупреждения
16 E_CORE_ERROR Глобальные предупреждения (почти не используются)
32 E_CORE_WARNING Глобальные ошибки (не используется)
Наиболее часто встречающееся сочетание — 7 (1+2+4), которое, как мы можем ви-
деть, задает полный контроль, кроме некритичных предупреждений интерпретатора
(таких, например, как обращение к неинициализированной переменной). Оно часто
задается по умолчанию при установке PHP. Я же рекомендую первым делом устанав-
ливать значение этой настройки равным 255 (соответствует битовой маске со всеми
единичками), т. е. включить абсолютно все сообщения об ошибках, или же восполь-
зоваться константой E_ALL, делающей то же самое.
magic_quotes_gpc on|off
Эта настройка указывает PHP, нужно ли ему ставить дополнительный слэш перед
всеми апострофами ', кавычками ", обратными слэшами \ и нулевыми символами
(0) при приеме данных из браузера пользователя — например, поступивших из фор-
мы. Я предпочитаю всегда отключать этот параметр, потому что от него больше
про-
блем, чем пользы. Например, следующий вроде бы верный сценарий при повторном
нажатии кнопки, если в каком-нибудь текстовом поле введена кавычка, будет ее
"размножать":
// Делаем что-нибудь, если нажата кнопка Go!
?>
Мы получаем явно не то, что требовалось: мы хотели просто, чтобы значение поля
text сохранялось неизменным между запусками сценария. Оператор @ подавляет
сообщение об ошибке для следующего за ним выражения, если она происходит (в
нашем случае — при первом запуске сценария, когда переменные $name и $email
еще не инициализированы).
max_execution_time
Директива устанавливает время (в секундах), через которое работа сценария будет
принудительно прервана. Используется она в основном для того, чтобы запретить
пользователям захватывать слишком много ресурсов центрального процессора и из-
бежать "зависания" сценария.
track_vars on|off
Этот параметр очень полезен при программировании. Если он установлен в On, все
данные, доставленные методами GET и POST, а также Cookies, будут дополнительно
помещены в глобальные массивы $HTTP_GET_VARS, $HTTP_POST_VARS и
$HTTP_COOKIE_VARS соответственно.
Существуют и другие, более специфичные, параметры, такие как настройка интер-
фейсов с базами данных, настройка почтовых возможностей и др. Обычно их уста-
новки по умолчанию удовлетворяют всех. Подробнее о них можно прочитать в При-
ложении 2 или на сайте http://www.php.net.
Контроль ошибок
В процессе работы программы в ней могут возникать ошибки. Одна из самых силь-
ных черт PHP — возможность отображения сообщений об ошибках прямо в браузере,
не генерируя пресловутую 500-ю Ошибку сервера (Internal Server Error), как это
де-
лают другие языки. В зависимости от состояния интерпретатора сообщения будут
либо выводиться в браузер, либо подавляться. Для установки режима вывода ошибок
служит функция Error_Reporting().
int Error_Reporting([int $level])
Устанавливает уровень строгости для системы контроля ошибок PHP, т. е. величину
параметра error_reporting в конфигурации PHP, который мы недавно рассматри-
вали. Рекомендую первой строкой сценария ставить вызов:
Error_Reporting(1+2+4+8);
Часть IV. Стандартные функции PHP 336
Да, поначалу будут очень раздражать "мелкие" сообщения типа "использование не-
инициализированной переменной". Практика показывает, что эти предупреждения на
самом деле свидетельствуют (чаще всего) о возможной логической ошибке в про-
грамме, и что при их отключении может возникнуть ситуация, когда программу
будет
очень трудно отладить.
Однажды я просидел несколько часов, тщетно пытаясь найти ошибку в сцена-
рии (он работал, но неправильно). После того как я включил полный контроль
ошибок, все выяснилось в течение 5 минут. Вот вам и выигрыш по времени...
Оператор отключения ошибок
Есть и еще один аргумент за то, чтобы всегда использовать полный контроль
ошибок.
Это — существование в PHP оператора @. Если этот оператор поставить перед любым
выражением, то все ошибки, которые там возникнут, будут проигнорированы. На-
пример:
if(!@filemtime("notextst.txt"))
echo "Файла не существует!";
Попробуйте убрать оператор @ — тут же получите сообщение: "Файл не найден", а
только после этого — вывод оператора echo. Однако с оператором @ предупреждение
будет подавлено, что нам и требовалось.
Кстати, в приведенном примере, возможно, несколько логичнее было бы воспользо-
ваться функцией file_exists(), которая как раз и предназначена для определения
факта существования файла, но в некоторых ситуациях это нам не подойдет. Напри-
мер:
// Обновить файл, если его не существует или он очень старый
if(!file_exists($fname) || filemtime($fname)
Но, согласитесь, следующий код куда элегантнее:
if(@$submit) echo "Кнопка нажата!"
?>
Старайтесь чаще пользоваться оператором @ и реже — установкой слабого контроля
ошибок.
Принудительное завершение
программы
void exit()
Эта функция немедленно завершает работу сценария. Из нее никогда не происходит
возврата. Перед окончанием программы вызываются функции-финализаторы, кото-
рые скоро будут нами рассмотрены.
void die(string $message)
Функция делает почти то же самое, что и exit(), только перед завершением работы
выводит строку, заданную в параметре $message. Чаще всего ее применяют, если
нужно напечатать сообщение об ошибке и аварийно завершить программу.
Полезным примером применения die() может служить такой код:
$filename='/path/to/data-file';
$file=fopen($filename, 'r') or die("не могу открыть файл $filename!");
Здесь мы ориентируемся на специфику оператора or — "выполнять" второй операнд
только тогда, когда первый "ложен". Мы уже встречались с этим приемом в главе,
посвященной работе с файлами. Заметьте, что оператор || здесь применять нельзя
—
он имеет более высокий приоритет, чем =. С использованием || последний пример
нужно было бы переписать следующим образом:
Часть IV. Стандартные функции PHP 338
$filename='/path/to/data-file';
($file=fopen($filename, 'r')) || die("не могу открыть файл $filename!");
Согласитесь, последнее практически полностью исключает возможность применения
|| в подобных конструкциях.
Финализаторы
Слава богу, разработчики PHP предусмотрели возможность указать в программе
функцию-финализатор, которая будет автоматически вызвана, как только работа
сце-
нария завершится — неважно, из-за ошибки или легально. В такой функции мы мо-
жем, например, записать информацию в кэш или обновить какой-нибудь файл жур-
нала работы программы. Что же нужно для этого сделать?
Во-первых, написать саму функцию и дать ей любое имя. Желательно также, чтобы
она была небольшой, и чтобы в ней не было ошибок, потому что сама функция,
впол-
не возможно, будет вызываться перед завершением сценария из-за ошибки. Во-
вторых зарегистрировать ее как финализатор, передав ее имя стандартной функции
Register_shutdown_function().
int Register_shutdown_function(string $func)
Регистрирует функцию с указанным именем с той целью, чтобы она автоматически
вызывалась перед возвратом из сценария. Функция будет вызвана как при окончании
программы, так и при вызовах exit() или die(), а также при фатальных ошибках,
приводящих к завершению сценария — например, при синтаксической ошибке.
Конечно, можно зарегистрировать несколько финальных функций, которые будут
вызываться в том же порядке, в котором они регистрировались.
Правда, есть одно "но". Финальная функция вызывается уже после закрытия
соедине-
ния с браузером клиента. Поэтому все данные, выведенные в ней через echo,
теряют-
ся (во всяком случае, так происходит в Unix-версии PHP, а под Windows
CGI-версия
PHP и echo работают прекрасно). Так что лучше не выводить никаких данных в та-
кой функции, а ограничиться работой с файлами и другими вызовами, которые ниче-
го не направляют в браузер.
Последнее обстоятельство, к сожалению, ограничивает функциональность финализа-
торов: им нельзя поручить, например, вывод окончания страницы, если сценарий по
каким-то причинам прервался из-за ошибки. Вообще говоря, надо заметить, что в
PHP никак нельзя в случае ошибки в некотором запущенном коде проделать какие-
либо разумные действия (кроме, разумеется, мгновенного выхода). Это несколько
может ограничивать область применимости PHP для написания шаблонизатора (о
шаблонах будет подробно рассказано в части V этой книги).
Глава 24. Управление интерпретатором 339
Генерация кода
во время выполнения
Так как PHP в действительности является транслирующим интерпретатором, в нем
заложены возможности по созданию и выполнению кода программы прямо во время
ее выполнения. То есть мы можем писать сценарии, которые в буквальном смысле
создают сами себя, точнее, свой код! Это незаменимо при написании
шаблонизаторов
и функций, занимающихся динамическим формированием писем. Мы поговорим о
таких функциях в части V книги.
Выполнение кода
int eval(string $code)
Эта функция делает довольно интересную вещь: она берет параметр $st и, рассмат-
ривая его как код программы на PHP, запускает. Если этот код возвратил какое-то
значение оператором return (как, например, это обычно делают функции), eval()
также вернет эту величину.
Параметр $st представляет собой обычную строку, содержащую участок PHP-
программы. То есть в ней может быть все, что допустимо в сценариях:
r ввод-вывод, в том числе закрытие и открытие тэгов и ?>;
r управляющие инструкции: циклы, условные операторы и т. д.;
r объявления и вызовы функций;
r вложенные вызовы функции eval().
Тем не менее, нужно помнить несколько важных правил.
r Код в $st будет использовать те же самые глобальные переменные, что и вы-
звавшая программа. Таким образом, переменные не локализуются внутри
eval().
r Любая критическая ошибка (например, вызов неопределенной функции) в коде
строки $st приведет к завершению работы всего сценария (разумеется, сообще-
ние об ошибке также напечатается в браузер). Это значит, что мы не можем пере-
хватить все ошибки в коде, вставив его в eval().
Последний факт вызывает довольно удручающие мысли. К сожалению, разра-
ботчики PHP опять не задумались о том, как было бы удобно, если бы eval()
при ошибке в вызванном ей коде просто возвращала значение false, поме-
щая сообщение об ошибке в какую-нибудь переменную (как это сделано, на-
пример, в Perl).
Часть IV. Стандартные функции PHP 340
r Тем не менее, синтаксические ошибки и предупреждения, возникающие при
трансля-
ции кода в $st, не приводят к завершению работы сценария, а всего лишь вызывают
возврат из eval()значения ложь. Что ж, хоть кое-что.
Не забывайте, что переменные в строках, заключенных в двойные кавычки, в PHP
интерполируются (то есть заменяются на соответствующие значения). Это значит,
что, если мы хотим реже использовать обратные слэши для защиты символов-
кавычек, нужно стараться применять строки в апострофах для параметра,
передавае-
мого eval(). Например:
eval("$a=$b;"); // Неверно!
// Вы, видимо, хотели написать следующее:
eval("\$a=\$b");
// но короче будет так:
eval('$a=$b');
Возможно, вы спросите: зачем нам использовать eval(), если она занимается лишь
выполнением кода, который мы и так можем написать прямо в нужном месте про-
граммы? Например, следующий фрагмент
eval('for($i=0; $i<10; $i++) echo $i; ');
эквивалентен такому коду:
for($i=0; $i<10; $i++) echo $i;
Почему бы всегда не пользоваться последним фрагментом? Да, конечно, в нашем
примере лучше было бы так и поступить. Однако сила eval() заключается прежде
всего в том, что параметр $st может являться (и чаще всего является) не
статической
строковой константой, а сгенерированной переменной. Вот, например, как мы можем
создать 100 функций с именами Func1()...Func100(), которые будут печатать
квадраты первых 100 чисел:
Листинг 24.1. Генерация семейства функций
for($i=1; $i<=100; $i++)
eval("function Func$i() { return $i*$i; }");
Попробуйте-ка сделать это, не прибегая к услугам eval()!
Я уже говорил, что в случае ошибки (например, синтаксической) в коде, обрабаты-
ваемом eval(), сценарий завершает свою работу и выводит сообщение об ошибке в
браузер. Как обычно, сообщение сопровождается указанием того, в какой строке
про-
изошла ошибка, однако вместе с именем файла выдается уведомление, что програм-
ма оборвалась в функции eval(). Вот как, например, может выглядеть такое сооб-
щение:
Parse error: parse error in eval.php(4) : eval()'d code on line 1
Глава 24. Управление интерпретатором 341
Как видим, в круглых скобках после имени файла PHP печатает номер строки, в ко-
торой была вызвана сама функция eval(), а после "on line" — номер строки в
параметре eval() $st. Впрочем, мы никак не можем перехватить эту ошибку, по-
этому последнее нам не особенно-то интересно.
Давайте теперь в качестве тренировки напишем код, являющийся аналогом инструк-
ции include. Пусть нам нужно включить файл, имя которого хранится в $fname.
Вот как это будет выглядеть:
$code=join("",File($fname));
eval("?>$code");
Всего две строчки, но какие...… Рассмотрим их подробнее.
Что делает первая строка — совершенно ясно: она сначала считывает все
содержимое
файла $fname по строкам в список, а затем образует одну большую строку путем
"склеивания" всех элементов этого списка. Заметьте, как получилось лаконично:
нам
не нужно ни открывать файл, ни использовать функцию fread() или fgets().
Вторая строка, собственно, запускает тот код, который мы только что считали. Но
cr.le iir ddlaard.lnn. nceaierec ?> c creri.carlnn. — nyarec inedunc. c
credunc. eiar
PHP? Наверное, вы уже догадались: суть в том, что функция eval() воспринимает
свой параметр именно как код, а не как документ со вставками PHP-кода. В то же
время, считанный нами файл представляет собой обычный PHP-сценарий, т. е. доку-
мент со "вставками" PHP. Иными словами, настоящая инструкция include воспри-
нимает файл в контексте документа, а функция eval() — в контексте кода. По-
этому-то мы и используем ?> — переводим текущий контекст в режим восприятия
документа, чтобы eval() "осознала" статический текст верно. Мы еще неоднократно
столкнемся с этим приемом в будущем.
Генерация функций
В последнем примере мы рассмотрели, как можно создать 100 функций с разными
именами, написав программу длиной в 2 строчки. Это, конечно, впечатляет, но мы
должны жестко задавать имена функций. Почему бы не поручить эту работу PHP,
если нас не особо интересуют получающиеся имена?
Листинг 24.2. Генерация "анонимных" функций
$Funcs=array();
for($i=0; $i<=100; $i++) {
$id=uniqid("F");
eval("function $id() { return $i*$i; }");
$Funcs[]=$id;
}
Часть IV. Стандартные функции PHP 342
Теперь мы имеем список $Funcs, который содержит имена наших сгенерированных
функций. Как нам вызвать какую-либо из них? Это очень просто:
echo $Funcs[12](); // выводит 144
Однако мы могли бы написать с тем же результатом и
echo Func12();
при том условии, если бы воспользовались кодом генерации функций из листинга
24.1. Кажется, что так короче? Тогда не торопитесь. Все хорошо, если мы точно
зна-
ем, что надо вызвать 12-ю функцию, но как же быть, если номер хранится в
перемен-
ной — например, в $n? Вот решение:
echo $Funcs[$n](); // выводит результат работы $n-й функции
Не правда ли, просто? Выглядит явно лучше, чем такой код:
$F="Func$n";
$F();
Тут нам не удастся обойтись без временной переменной $F (вариант с допол-
нительной eval() тоже не подойдет, т. к. у функции могут быть строковые па-
раметры, и придется перед всеми кавычками ставить слэши, чтобы поместить
их в параметр функции eval().
Оказывается, в PHP версии 4 существует функция, которая поможет нам упростить
генерацию "анонимных" функций, подобных полученным в примере из листинга
24.2. Называется она create_function().
string create_function(string $args, string $code)
Создает функцию с уникальным именем, выполняющую действия, заданные в коде
$code (это строка, содержащая программу на PHP). Созданная функция будет при-
нимать параметры, перечисленные в $args. Перечисляются они в соответствии со
стандартным синтаксисом передачи параметров любой функции. Возвращаемое зна-
чение представляет собой уникальное имя функции, которая была сгенерирована.
Вот
несколько примеров:
$Mul=create_function('$a,$b', 'return $a*$b;');
$Neg=create_function('$a', 'return -$a;');
echo $Mul(10,20); // выводит 200
echo $Neg(2); // выводит -2
Не пропустите последнюю точку с запятой в конце строки, переданной вторым
параметром create_function()!
Глава 24. Управление интерпретатором 343
Давайте теперь перепишем наш пример из листинга 24.2 с учетом
create_function(). Это довольно несложно. Обратите внимание, насколько сокра-
тился код.
$Funcs=array();
for($i=0; $i<=100; $i++)
$Funcs[]=create_function("","return $i*$i;");
echo $Funcs[12](); // выводит 144
И последний пример применения анонимных функций — в программах сортировки с
использованием пользовательских функций:
$a=array("orange", "apple", "apricot", "lemon");
usort($a,create_function('$a,$b', 'return strcmp($a,$b);'));
foreach($a as $key=>$value) echo "$key: $value \n";
Проверка синтаксической
корректности кода
С помощью 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-сценарием.
Другие функции
void usleep(int $micro_seconds)
Часть IV. Стандартные функции PHP 344
Вызов этой функции позволяет сценарию "замереть" не указанное время
(в микросекундах). При этом затрачивается очень немного ресурсов процессора,
так
что функцию вполне можно вызывать, чтобы дождаться выполнения какой-нибудь
операции другого процесса — например, закрытия им файла.
Существует также функция sleep(), которая принимает в параметрах не мик-
росекунды, а секунды, на которые нужно задержать выполнение программы.
int uniqid(string $prefix)
Функция uniqid() возвращает строку, при каждом вызове отличающуюся от ре-
зультата предыдущего вызова. Параметр $prefix задает префикс (до 114 символов
длиной) этого идентификатора.
Зачем нужен префикс? Представьте себе, что сразу несколько интерпретаторов на
разных хостах одновременно вызвали функцию uniqid(). В этом случае существует
вероятность того, что результат работы функций совпадет, чего нам бы не
хотелось.
Задание в качестве префикса имени хоста решит проблему.
Чтобы добиться большей уникальности, можно использовать uniqid()
"в связке" с функциями mt_rand() и md5(), описанными в предыдущих главах.
Глава 25
Управление сессиями
Сессии, наконец-то появившиеся в PHP версии 4, представляют собой механизм, по-
зволяющий хранить некоторые (и произвольные) данные, индивидуальные для каж-
дого пользователя (например, его имя и номер счета), между запусками сценария.
Термин "сессия" является транслитерацией от английского слова session, что в
буквальном переводе должно бы означать "сеанс". Однако последнее слово в
программистском жаргоне не особенно-то прижилось (насколько я знаю), по-
этому я буду употреблять термин "сессия". И да простят меня студенты, если у
них это вызывает нехорошие ассоциации.
Фактически, сессия — это некоторое место долговременной памяти (обычно часть на
жестком диске и часть — в Cookies браузера), которое сохраняет свое состояние
меж-
ду вызовами сценариев одним и тем же пользователем. Иными словами, поместив в
сессию переменную (любой структуры), мы при следующем запуске сценария полу-
чим ее в целости и сохранности. Трудно переоценить удобства, которые это
предос-
тавляет нам, программистам.
Зачем нужны сессии?
В Web-программировании есть один класс задач, который может вызвать довольно
много проблем, если писать сценарии "в лоб". Речь идет о слабой стороне CGI —
не-
возможности запустить программу на длительное время, позволив ей при этом обме-
ниваться данными с пользователями.
В общем и целом, сценарии должны запускаться, моментально выполняться и воз-
вращать управление системе. Теперь представьте, что мы пишем форму, но в ней
такое большое число полей, что было бы глупо поместить их на одну страницу. Нам
нужно разбить процесс заполнения формы на несколько этапов, или стадий, и пред-
ставить их в виде отдельных HTML-документов. Это похоже на работу мастеров
Windows — диалоговых окон для ввода данных с кнопками Назад и Дальше, благо-
даря которым можно переместиться на шаг в любом направлении.
Например, в первом документе с диалогом у пользователя может запрашиваться его
имя и фамилия, во втором (если первый был заполнен верно) — данные о его месте
Часть IV. Стандартные функции PHP 346
жительства, и в третьем — номер кредитной карточки.
В любой момент можно вернуться на шаг назад, чтобы исправить те или иные дан-
ные. Наконец, если все в порядке, накопленная информация обрабатывается — на-
пример, помещается в базу данных.
Реализация такой схемы оказывается для Web-приложений довольно нетривиальной
проблемой. Действительно, нам придется хранить все ранее введенные данные в ка-
ком-нибудь временном хранилище, которое должно аннулироваться, если пользова-
тель вдруг передумает и "уйдет" с сайта. Для этого, как мы знаем, можно
использо-
вать функции сериализации и файлы. Однако ими мы решаем только половину
проблемы: нам нужно также как-то привязывать конкретного пользователя к кон-
кретному временному хранилищу. Действительно, предположим, что мы этого не
сделали. Тогда, если в момент заполнения какой-нибудь формы одним пользователем
на сайт "зайдет" другой и тоже попытается ввести свои данные, получится куча
мала.
Все эти проблемы решаются с применением сессий PHP, о которых сейчас и пойдет
речь.
Механизм работы сессий
Как же работают сессии? Для начала должен существовать механизм, который бы
позволил PHP идентифицировать каждого пользователя, запустившего сценарий. То
есть при следующем запуске PHP нужно однозначно определить, кто его запустил:
тот
же человек, или другой. Делается это путем присвоения клиенту так называемого
уникального идентификатора сессии. Чтобы этот идентификатор был доступен при
каждом запуске сценария, PHP помещает его в Cookies браузера.
Использовать Cookies не обязательно, существует и другой способ. Мы пого-
ворим о нем чуть позже.
Теперь, зная идентификатор (дальше для краткости я буду называть его SID), PHP
может определить, в каком же файле на диске хранятся данные пользователя.
Немного о том, как сохранять переменную (обязательно глобальную) в сессии. Для
этого мы должны ее зарегистрировать с помощью специальной функции. После реги-
страции мы можем быть уверены, что при следующем запуске сценария тем же
пользователем она получит то же самое значение, которое было у нее при предыду-
щем завершении программы. Это произойдет потому, что при завершении сценария
PHP автоматически сохраняет все переменные, зарегистрированные в сессии, во
вре-
менном хранилище. Конечно, можно в любой момент аннулировать переменную —
"вычеркнуть" ее из сессии, или же уничтожить вообще все данные сессии.
Где же находится то промежуточное хранилище, которое использует PHP? Вообще
говоря, вы вольны сами это задать, написав соответствующие функции и
зарегистри-
Глава 25. Управление сессиями 347
ровав их как обработчики сессии. Впрочем, делать это не обязательно: в PHP уже
существуют обработчики по умолчанию, которые хранят данные в файлах (в систе-
мах Unix для этого обычно используется директория /tmp). Если вы не собираетесь
создавать что-то особенное, вам они вполне подойдут.
Инициализация сессии
Но прежде, чем работать с сессией, ее необходимо инициализировать. Делается это
путем вызова специальной функции session_start().
Если вы поставили в настройках PHP режим session.auto_start=1, то
функция инициализации вызывается автоматически при запуске сценария. Од-
нако, как мы вскоре увидим, это лишает нас множества полезных возможно-
стей (например, не позволяет выбирать свою, особенную, группу сессий). Так
что лучше не искушать судьбу и вызывать session_start() в первой строч-
ке вашей программы. Следите также за тем, чтобы до нее не было никакого
вывода в браузер — иначе PHP не сможет установить SID для пользователя!
void session_start()
Эта функция инициализирует механизм сессий для текущего пользователя, запустив-
шего сценарий. По ходу инициализации она выполняет ряд действий.
r Если посетитель запускает программу впервые, у него устанавливается Cookies с
уникальным идентификатором, и создается временное хранилище, ассоциирован-
ное с этим идентификатором.
r Определяется, какое хранилище связано с текущим идентификатором пользовате-
ля.
r Если в хранилище имеются какие-то переменные, их значения восстанавливают-
ся. Точнее, создаются глобальные переменные, которые были сохранены в сессии
при предыдущем завершении сценария.
Вообще говоря, рассмотренный механизм, как всегда, не совсем точно соот-
ветствует истинному положению вещей. А именно, все зависит от того, какое
значение присвоено настроечному параметру register_globals. Если
register_globals=0, то в сессии можно будет сохранять (а потом и восста-
навливать) только величины, содержащиеся в глобальном ассоциативном мас-
сиве $HTTP_SESSION_VARS. Если же этот параметр содержит значение "исти-
на" (как обычно и происходит по умолчанию), то в сессии можно
регистрировать глобальные переменные.
Часть IV. Стандартные функции PHP 348
Регистрация переменных
bool session_register(mixed $name [, mixed $name1, ...])
PHP узнает о том, что ту или иную переменную нужно сохранить в сессии, если ее
предварительно зарегистрировать. Для этого и предназначена функция
session_register(). Она принимает в параметрах одно или несколько имен пере-
менных (имена задаются в строках, без знака $ слева), регистрирует их в текущей
запущенной сессии и возвращает значение "истина", если все прошло корректно.
Почему же тогда я описал типы параметров как mixed, а не как string? Да
потому, что на самом деле в функцию можно передавать не одну строку в ка-
ждом параметре, а сразу список строк. Каждая такая строка будет регистриро-
вать отдельную переменную с соответствующим именем. Более того — эле-
ментом списка может опять же быть список строк, и т. д.
Нет ничего страшного, если мы дважды зарегистрируем одну и ту же переменную в
сессии. На самом деле, чаще всего как раз так и происходит — при повторном
запус-
ке сценария. Вот пример:
Листинг 25.1. Пример работы с сессиями
session_start();
session_register("count");
$count=@$count+1;
?>
Счетчик
В текущей сессии работы с браузером Вы открыли эту страницу
=$count?> раз(а). Закройте браузер, чтобы обнулить счетчик.
Как видим, все предельно просто.
Идентификатор сессии
и имя группы
Что же, теперь мы уже можем начать писать кое-какие сценарии. Но вскоре возник-
нет небольшая проблема. Дело в том, что на одном и том же сайте могут
сосущество-
Глава 25. Управление сессиями 349
вать сразу несколько сценариев, которые нуждаются в услугах поддержки сессий
PHP. Они "ничего не знают" друг о друге, поэтому временные хранилища для сессий
должны выбираться не только на основе идентификатора пользователя, но и на
осно-
ве того, какой из сценариев запросил обслуживание сессии.
Имя группы сессий
Что, не совсем понятно? Хорошо, тогда рассмотрим пример. Пусть разработчик A
написал сценарий счетчика, приведенный в листинге 25.1. Он использует перемен-
ную $count, и не имеет никаких проблем. До тех пор, пока разработчик B, ничего
не
знающий о сценарии A, не создал систему статистики, которая тоже использует
сес-
сии. Самое ужасное, что он также регистрирует переменную $count, не зная о том,
что она уже "занята". В результате, как всегда, страдает пользователь: запустив
сна-
чала сценарий разработчика B, а потом — A, он видит, что данные счетчиков пере-
мешались. Непорядок!
Нам нужно как-то разграничить сессии, принадлежащие одному сценарию, от сессий,
принадлежащих другому. К счастью, разработчики PHP предусмотрели такое поло-
жение вещей. Мы можем давать группам сессий непересекающиеся имена, и сцена-
рий, знающий имя своей группы сессии, сможет получить к ней доступ. Вот
теперь-то
разработчики A и B могут оградить свои сценарии от проблем с пересечениями имен
переменных. Достаточно в первой программе указать PHP, что мы хотим использо-
вать группу с именем, скажем, sesA, а во второй — sesB.
string session_name([string $newname])
Эта функция устанавливает или возвращает имя группы сессии, которая будет ис-
пользоваться PHP для хранения зарегистрированных переменных. Если $newname не
задан, то возвращается текущее имя. Если же этот параметр указан, то имя группы
будет изменено на $newname, при этом функция вернет предыдущее имя.
Session_name() лишь сменяет имя текущей группы и сессии, но не создает
новую сессию и временное хранилище! Это значит, что мы должны в большин-
стве случаев вызывать session_name(имя_группы) еще до ее инициализа-
ции — вызова session_start(), в противном случае мы получим совсем не
то, что ожидали.
Если функция session_name() не была вызвана до инициализации, PHP будет ис-
пользовать имя по умолчанию — PHPSESID.
Кстати говоря, имя группы сессий, устанавливаемое рассматриваемой функ-
цией, — это как раз имя того самого Cookie, который посылается в браузер
клиента для его идентификации. Таким образом, пользователь может одно-
временно активизировать две и более сессий — с точки зрения PHP он будет
Часть IV. Стандартные функции PHP 350
менно активизировать две и более сессий — с точки зрения PHP он будет вы-
глядеть как два ли более различных пользователя. Однако не забывайте, что,
случайно установив в сценарии Cookie, имя которого совпадает с одним из
имен группы сессий, вы "затрете" Cookie.
Вот простой пример применения этой функции.
session_name("CounterScript"
session_start();
session_register("count");
$count=@$count+1;
?>
В текущей сессии Вы открыли эту страницу =$count?> раз(а).
Рекомендую всегда указывать имя группы сессии вручную, не полагаясь на значение
по умолчанию. За это вам скажут спасибо разработчики других сценариев, когда
они
захотят использовать вашу программу вместе со своими.
Идентификатор сессии
Мы уже говорили с вами, зачем нужен идентификатор сессии (SID). Фактически, он
является именем временного хранилища, которое будет использовано для хранения
данных сессии между запусками сценария. Итак, один SID — одно хранилище. Нет
SID, нет и хранилища, и наоборот.
В этом месте очень легко запутаться. В самом деле, как же соотносится
идентифика-
тор сессии и имя группы? А вот как: имя — это всего лишь собирательное название
для нескольких сессий (то есть, для многих SID), запущенных разными пользовате-
лями. Один и тот же клиент никогда не будет иметь два различных SID в пределах
одного имени группы. Но его браузер вполне может работать (и часто работает) с
не-
сколькими SID, расположенными логически в разных "пространствах имен".
Итак, все SID уникальны и однозначно определяют сессию на компьютере, выпол-
няющем сценарий — независимо от имени сессии. Имя же задает "пространство
имен", в которое будут сгруппированы сессии, запущенные разными пользователями.
Один клиент может иметь сразу несколько активных пространств имен (то есть не-
сколько имен групп сессий).
string session_id([string $sid])
Функция возвращает текущий идентификатор сессии SID. Если задан параметр $sid,
то у активной сессии изменяется идентификатор на $sid. Делать это, вообще
говоря,
не рекомендуется.
Фактически, вызвав session_id() до session_start(), мы можем подключиться
к любой (в том числе и к "чужой") сессии на сервере, если знаем ее
идентификатор.
Глава 25. Управление сессиями 351
Мы можем также создать сессию с угодным нам идентификатором, при этом автома-
тически установив его в Cookies пользователя. Но это — не лучшее решение, —
предпочтительнее переложить всю "грязную работу" на PHP.
Другие функции
Здесь мы для полноты картины рассмотрим функции для работы с сессиями, которые
применяются гораздо реже, чем уже описанные.
bool session_is_registered(string $name)
Функция session_is_registered() возвращает значение true, если переменная с
именем $name была зарегистрирована в сессии, иначе возвращается false.
bool session_unregister(struing $name)
Эта функция отменяет регистрацию для переменной с именем $name для текущей
сессии. Иными словами, при завершении сценария все будет выглядеть так, словно
переменная с именем $name и не была никогда зарегистрирована. Возвращает true,
если все прошло успешно, и false — в противном случае.
После вызова функции session_unregister() глобальная переменная, ко-
торая была "аннулирована", не уничтожается, а сохраняет свое значение.
void session_unset()
Функция session_unset(), в отличие от session_unregister(), не только отме-
няет регистрацию переменных (кстати говоря, всех переменных сессии, а не
какой-то
одной), но и уничтожает глобальные переменные, которые были зарегистрированы в
сессии.
string session_save_path([string $path])
Эта функция возвращает имя каталога, в котором будут помещаться файлы — вре-
менные хранилища данных сессии. В случае, если указан параметр, как обычно, ак-
тивное имя каталога будет переустановлено на $path. При этом функция вернет
пре-
дыдущий каталог.
К сожалению, функции, которая бы возвращала список всех зарегистрированных в
сессии переменных, почему-то нет. Во всяком случае, в PHP версии 4.0.3.
Установка обработчиков сессии
До сих пор мы с вами пользовались стандартными обработчиками сессии, которые
PHP использовал каждый раз, когда нужно было сохранить или загрузить данные из
временного хранилища. Возможно, они вас не устроят — например, вы захотите хра-
Часть IV. Стандартные функции PHP 352
нить переменные сессии в базе данных или еще где-то. В этом случае достаточно
бу-
дет переопределить обработчики своими собственными функциями, и вот как оно
делается.
Обзор обработчиков
Всего существует 6 функций, связанных с сессиями, которые PHP вызывает в тот
или
иной момент работы механизма обработки сессий. Им передаются различные пара-
метры, необходимые для работы. Сейчас я перечислю все эти функции вместе с их
описаниями.
bool handler_open(string $save_path, string $session_name)
Функция вызывается, когда вызывается session_start(). Обработчик должен
взять на себя всю работу, связанную с открытием базы данных для группы сессий с
именем $session_name. В параметре $save_path передается то, что было указано
при вызове session_save_path() или же путь к файлам-хранилищам данных сес-
сий по умолчанию. Возможно, если вы используете базу данных, этот параметр
будет
бесполезным.
bool handler_close()
Этот обработчик вызывается, когда данные сессии уже записаны во временное хра-
нилище и его нужно закрыть.
string handler_read(string $sid)
Вызов обработчика происходит, когда нужно прочитать данные сессии с идентифика-
тором $sid из временного хранилища. Функция должна возвращать данные сессии в
специальном формате, который выглядит так:
имя1=значение1;имя2=значение2;имя3=значение3;...;
Здесь имяN задает имя очередной переменной, зарегистрированной в сессии, а
значениеN — результат вызова функции Serialize() для значения этой перемен-
ной. Например, запись может иметь следующий вид:
foo|i:1;count|i:10;
Она говорит о том, что из временного хранилища были прочитаны две целые пере-
менные, первая из которых равна 1, а вторая — 10.
string handler_write(string $sid, string $data)
Этот обработчик предназначен для записи данных сессии с идентификатором $sid во
временное хранилище — например, открытое ранее обработчиком handler_open().
Параметр $data задается в точно таком же формате, который был описан выше.
Фактически, чаще всего действия этой функции сводятся к записи в базу данных
строки $data без каких-либо ее изменений.
bool handler_destroy(string $sid)
Глава 25. Управление сессиями 353
Обработчик вызывается, когда сессия с идентификатором $sid должна быть уничто-
жена.
bool handler_gc(int $maxlifetime)
Данный обработчик — особенный. Он вызывается каждый раз при завершении рабо-
ты сценария. Если пользователь окончательно "покинул" сервер, значит, данные
сес-
сии во временном хранилище можно уничтожить. Этим и должна заниматься функ-
ция handler_gc(). Ей передается в параметрах то время (в секундах), по
прошествии которого PHP принимает решение о необходимости "почистить перыш-
ки", или "собрать мусор" (garbage collection) — т. е., это максимальное время
сущест-
вования сессии.
Как же должна работать рассматриваемая функция? Очень просто. Например, если
мы храним данные сессии в базе данных, мы просто должны удалить из нее все
запи-
си, доступ к которым не осуществлялся более, чем $maxlifetime секунд. Таким об-
разом, "застарелые" временные хранилища будут иногда очищаться.
На самом деле обработчик handler_gc() вызывается не при каждом запуске
сценария, а только изредка. Когда именно — определяется конфигурационным
параметром session.gc_probability. А именно, им задается (в процен-
тах), какова вероятность того, что при очередном запуске сценария будет вы-
бран обработчик "чистки мусора". Сделано это для улучшения производитель-
ности сервера, потому что обычно сборка мусора — довольно ресурсоемкая
задача, особенно если сессий много.
Регистрация обработчиков
Вы, наверное, обратили внимание, что при описании обработчиков я указывал их
имена с префиксом handler. На самом деле, это совсем не является обязательным.
Даже наоборот — вы можете давать такие имена своим обработчикам, какие только
захотите.
Но возникает вопрос: как же тогда PHP их найдет? Вот для этого и существует
функ-
ция регистрации обработчиков, которая говорит интерпретатору, какую функцию он
должен вызывать при наступлении того или иного события.
void session_set_save_handler($open,$close,$read,$write,$destroy,$gc)
Эта функция регистрирует подпрограммы, имена которых переданы в ее параметрах,
как обработчики текущей сессии. Параметр $open содержит имя функции, которая
будет вызвана при инициализации сессии, а $close — функции, вызываемой при ее
закрытии. В $read и $write нужно указать имена обработчиков, соответственно,
для чтения и записи во временное хранилище. Функция с именем, заданным в
Часть IV. Стандартные функции PHP 354
$destroy, будет вызвана при уничтожении сессии. Наконец, обработчик, определяе-
мый параметром $gc, используется как сборщик мусора.
Эту функцию можно вызывать только до инициализации сессии, в противном случае
она просто игнорируется.
Пример: переопределение обработчиков
Давайте напишем пример, который бы иллюстрировал механизм переопределения
обработчиков. Мы будем держать временные хранилища сессий в подкаталоге
sessiondata текущего каталога, и для каждого имени группы сессий создавать от-
дельный каталог.
Код листинга 25.2 довольно велик, но не сложен. Тут уж ничего не поделаешь —
нам
в любом случае приходится задавать все 6 обработчиков, а это выливается в
"объе-
мистые" описания.
Листинг 25.2. Переопределение обработчиков сессии
// Возвращает полное имя файла временного хранилища сессии.
// В случае, если нужно изменить тот каталог, в котором должны
// храниться сессии, достаточно поменять только эту функцию
function ses_fname($key)
{
return "sessiondata/".session_name()."/$key";
}
// Заглушки — эти функции просто ничего не делают
function ses_open($save_path, $ses_name) { return true; }
function ses_close() { return true; }
// Чтение данных из временного хранилища
function ses_read($key)
{
// Получаем имя файла и открываем файл
$fname=ses_fname($key);
$f=@fopen($fname,"rb"); if(!$f) return "";
// Читаем до конца файла
$st=fread($f,filesize($fname));
fclose($f);
return $st;
}
Глава 25. Управление сессиями 355
// Запись данных сессии во временное хранилище
function ses_write($key, $val)
{
$fname=ses_fname($key);
// Сначала создаем все каталоги (в случае, если они уже есть,
// игнорируем сообщения об ошибке)
@mkdir($d=dirname(dirname($fname)),0777);
@mkdir(dirname($fname),0777);
// Создаем файл и записываем в него данные сессии
$f=@fopen($fname,"wb"); if(!$f) return "";
fwrite($f,$val);
fclose($f);
return true;
}
// Вызывается при уничтожении сессии
function ses_destroy ($key)
{
return @unlink(ses_fname($key));
}
// Сборка мусора — ищем все старые файлы и удаляем их
function ses_gc($maxlifetime)
{
$dir=ses_fname(".");
// Получаем доступ к каталогу текущей группы сессии
$d=@opendir($dir); if(!$d) return false;
$DelDir=1; // Признак того, что каталог пуст, и его можно удалить
// Читаем все элементы каталога
while(($e=readdir($d))!==false) {
// Если это "точки", пропускаем их
if($e=="."||$e=="..") continue;
// Файл слишком старый?
if(time()-filemtime($fname="$dir/$e")>=$maxlifetime) {
@unlink($fname);
continue;
}
// Нашли не очень старый файл — значит, каталог точно
Часть IV. Стандартные функции PHP 356
// не будет в результате работы пуст.
$DelDir=0;
}
closedir($d);
// Если все файлы оказались слишком старые и удалены,
// удалить и каталог
if($DelDir) @rmdir($dir);
return true;
}
// Регистрируем наши новые обработчики
session_set_save_handler(
"ses_open", "ses_close",
"ses_read", "ses_write",
"ses_destroy", "ses_gc"
);
// Для примера подключаемся к группе сессий test
session_name("test");
session_start();
session_register("count");
// Дальше как обычно...
$count=@$count+1;
?>
Счетчик
В текущей сессии работы с браузером Вы открыли эту страницу
=$count?> раз(а). Закройте браузер, чтобы обнулить этот счетчик.
Сессии и Cookies
До сих пор я подразумевал, что использование сессий немыслимо без Cookies.
Дейст-
вительно, Cookies представляют собой наиболее элегантное и простое решение
задачи
идентификации каждого подключившегося пользователя, что необходимо для связи
временного хранилища и данных сессии. Но как быть, если пользователи отключили
Cookies в своих браузерах?
Глава 25. Управление сессиями 357
К сожалению, пользователи отключают Cookies гораздо чаще, чем это может
показаться на первый взгляд. Например, всего год назад Всероссийский Клуб
Вебмастеров проводил опрос, в результате которого выяснилось, что количе-
ство пользователей Интернета, отключивших у себя по каким-то соображениям
поддержку Cookies, достигает 20—30%. Что это за соображения? Многие ду-
мают, что Cookies потенциально являются "дырой" в безопасности их компью-
тера. Это совершенно не соответствует действительности, потому что браузе-
ры всегда имеют ограничения на количество и суммарный объем Cookies,
которые могут быть в них установлены. Другие же просто не хотят, чтобы неиз-
вестно кто писал что угодно на их жесткий диск. Правда, это не мешает таким
"перестраховщикам" открывать пришедший по почте исполняемый файл — та-
кой же, как из письма типа "Love letter"…
В общем, вы видите, что для абсолютной уверенности в работоспособности ваших
сценариев на любом браузере нужен механизм, позволяющий отказаться от исполь-
зования Cookies при управлении сессиями. Такой механизм действительно
существует в PHP, и основная его идея состоит в том, чтобы передавать
идентификатор сессии не в Cookies, а каким-нибудь аналогичным путем — например,
в данных запроса GET. Последнее мы сейчас и рассмотрим.
Явное использование
константы SID
В PHP существует одна специальная константа с именем SID. Она всегда содержит
имя группы текущей сессии и ее идентификатор в формате имя=идентификатор.
Вспомните: именно в таком формате данные принимаются, когда они приходят из
Cookies браузера. Таким образом, нам достаточно просто-напросто передать
значение
константы SID в сценарий, чтобы он "подумал", будто бы данные пришли из Cookies.
Вот пример:
Листинг 25.3. Sesget.php: простой пример использования сессий без Cookies
session_name("test");
session_start();
session_register("count");
$count=@$count+1;
?>
Счетчик
В текущей сессии работы с браузером Вы открыли эту страницу
Часть IV. Стандартные функции PHP 358
=$count?> раз(а). Закройте браузер, чтобы обнулить этот счетчик.
>Click here!
Если набрать в браузере адрес вроде такого:
http://www.somehost.ru/sesget.php
то создастся новая сессия с уникальным идентификатором. Разумеется, если сразу
же
нажать кнопку Обновить, счетчик не увеличится, потому что при каждом запуске
будет создаваться новое временное хранилище — у PHP просто нет информации об
идентификаторе пользователя. Теперь обратите внимание на предпоследнюю строчку
листинга 25.3. Видите, как хитро мы передаем в сценарий, запускаемый через
гипер-
ссылку, данные об идентификаторе текущей сессии? Теперь с его точки зрения они
якобы пришли из Cookies…
Все будет работать так, как описано, только в том случае, если в браузере
действительно отключены Cookies. Если же они включены, PHP просто не бу-
дет генерировать константу SID (она будет пустой) и задействует Cookies. Все
вполне логично.
Неявное изменение гиперссылок
Похоже, что вы уже начали думать о том, как же это все-таки неудобно — везде
вставлять участки кода =SID?>, и, пропусти вы их в одном месте, придется
долго
искать ошибку? Что же, законный повод для беспокойства, но, к счастью,
разработ-
чики PHP уберегли нас и от этой напасти.
Вы не поверите, но, если в какой-нибудь гипессылке вы по ошибке пропустите
=SID?>, PHP вставит его за вас автоматически. Причем так, чтобы это никак не
повредило другим параметрам, возможно, уже присутствующим в URL. Если вы в
шоке, то запустите следующий сценарий в браузере, а затем наведите мышь на
гипер-
ссылку и посмотрите в строке состояния, какой адрес имеет ссылка:
Click here!
Click here!
Click here!
Вот адреса этих ссылок с точки зрения браузера:
http://www.somehost.ru/path/to/something.php?PHPSESSID=8114536a920bfb01f
http://www.somehost.ru/path/to/something.html?a=aaa&b=bbb&PHPSESSID=86a20
Глава 25. Управление сессиями 359
http://www.somehost.ru/?PHPSESSID=8114536a920bfb2a
(Я немного урезал идентификаторы сессий, чтобы они уместились на странице этой
книги.) Обратите внимание на второй адрес: он говорит, что идентификатор
коррект-
но вставился в конец обычных параметров страницы. Третий пример заставляет за-
думаться о том, что идентификатор сессии прикрепляется к URL независимо от типа
документа, на который он указывает.
Описанная только что возможность работает лишь в том случае, если в на-
стройках PHP установлен в значение истина параметр
session.use_trans_sid.
Он как раз и включен по умолчанию.
Зачем же тогда нужна константа SID? Да незачем. Это — устаревший прием переда-
чи идентификатора сессии, и я привел его здесь только для того, чтобы
нарисовать
более полную картину, что в действительности происходит, а также показать, на-
сколько иногда PHP может быть услужлив.
Неявное изменение формы
Возможно, прочитав этот заголовок, вы еще более обрадуетесь. Да, PHP умеет не
только изменять гиперссылки, он также и добавляет скрытые поля в формы, которые
формирует сценарий, чтобы передать идентификатор сессии вызываемому документу!
Это ставит последнюю точку над i в вопросе поддержки сессий для пользователей,
которые отключили у себя Cookies.
Напоследок рассмотрим пример сценария, который выводит обыкновенную пустую
форму, и в ней, как по мановению волшебной палочки, появляется дополнительное
скрытое поле с идентификатором сессии.
А вот почти дословно то, что выдается в браузере (Internet Explorer) после
запуска
этого сценария и выбора в меню пункта Просмотр в виде HTML:
Как видим, PHP добавил в форму скрытое поле с нужным именем и значением. Он
также заключил в кавычки значения атрибутов тэга
После выбора в этом поле нужного файла и отправки формы (и загрузки на сервер
того файла, который был указан) PHP определит, что нужно принять файл, и сохра-
нит его во временном каталоге на сервере. Кроме того, в программе создадутся
не-
сколько переменных.
r $MyFile — имя временного файла на машине сервера, который содержит дан-
ные, переданные пользователем. С этим файлом теперь можно вытворять все что
угодно: удалять, копировать, переименовывать, niiaa oaaeyou...
r $MyFile_name — исходное имя файла, которое он имел до своей отправки на
сервер.
r $MyFile_size — размер закачанного файла в байтах.
r $MyFile_type — тип загруженного файла, если браузер смог его определить. К
примеру, image/gif, text/html и т. д.
Как видим, префикс у всех созданных переменных один и тот же — MyFile_. Этот
префикс состоит из имени элемента закачки в форме, к которому присоединен знак
_.
Теперь мы можем, например, скопировать только что полученный файл на новое ме-
сто, при помощи Copy($MyFile,"uploaded.dat") или других средств, проверив
предварительно, не слишком ли он велик, основываясь на значении переменной
$MyFile_size.
Настоятельно рекомендую использовать функцию копирования, а не переиме-
нования/перемещения. Дело в том, что в некоторых операционных системах
временный каталог, в котором PHP хранит только что закачанные файлы, мо-
жет находиться на другом носителе, и в результате операция переименования
завершится с ошибкой. Хотя мы и оставили копию полученного файла во вре-
менном каталоге, можно не заботиться о его удалении в целях экономии мес-
та: PHP сделает это автоматически.
Глава 28. Загрузка файлов на сервер
393
Если процесс окончится неуспешно, вы сможете определить это по отсутствию файла,
имя которого задано в $MyFile, или же по отсутствию самой этой переменной в
про-
грамме.
Пример: фотоальбом
Давайте напишем небольшой сценарий, представляющий собой простейший фото-
альбом с возможностью добавления в него новых фотографий.
Листинг 28.1. Сценарий photo.php: простейший фотоальбом
$ImgDir="img"; // Каталог для хранения изображений
@mkdir($ImgDir,666); // Создаем, если его еще нет
// Проверяем, нажата ли кнопка добавления фотографии
if(@$doUpload) {
// Проверяем, принят ли файл
if(file_exists($File)) {
// Все в порядке — добавляем файл в каталог с фотографиями
// Используем то же имя, что и в системе пользователя
Copy($File,"$ImgDir/".basename($File_name));
}
}
// Теперь считываем в массив наш фотоальбом
$d=opendir($ImgDir); // открываем каталог
$Photos=array(); // изначально альбом пуст
// Перебираем все файлы
while(($e=readdir($d))!==false) {
// Это изображение GIF, JPG или PNG?
if(!ereg("^(.*)\\.(gif|jpg|png)$",$e,$P)) continue;
// Если нет, переходим к следующему файлу,
// иначе обрабатываем этот
$path="$ImgDir/$e"; // адрес
$sz=GetImageSize($path); // размер
$tm=filemtime($path); // время добавления
// Вставляем изображение в массив $Photos
$Photos[$tm] = array(
'time' => filemtime($path), // время добавления
'name' => $e, // имя файла
Часть V. Приемы программирования на PHP
394
'url' => $path, // его URI
'w' => $sz[0], // ширина картинки
'h' => $sz[1], // ее высота
'wh' => $sz[3] // "width=xxx height=yyy"
);
}
// Ключи массива $Photos — время в секундах, когда была добавлена
// та или иная фотография. Сортируем массив: наиболее "свежие"
// фотографии располагаем ближе к его началу.
krsort($Photos);
// Данные для вывода готовы. Дело за малым — оформить страницу.
?>
$Img) {?>
=$Img['wh']?>
alt="Добавлена =date("d.m.Y H:i:s",$Img['time'])?>"
>
}?>
Конечно, этот сценарий далеко не идеален (например, он не поддерживает удаление
фотографий из фотоальбома), но для иллюстрации заявленных возможностей, по-
моему, вполне подходит. Для простоты я совместил две функции (администрирование
альбома и его просмотр) в одной программе. В реальной жизни, конечно, за каждую
из них должен отвечать отдельный сценарий (первый из них, наверное, будет
требо-
вать от пользователя прохождения авторизации, чтобы добавлять фотографии в аль-
бом могли лишь привилегированные пользователи).
Обратите внимание на то, как этот сценарий оформлен. В самом начале нахо-
дится весь код на PHP, который, собственно, и работает с данными фотоаль-
бома. В этом коде в принципе нет никаких указаний на то, как должна быть от-
форматирована страница. Его задача — просто сгенерировать данные.
Наоборот, тот текст, который следует после закрывающей скобки ?>, содержит
Глава 28. Загрузка файлов на сервер
395
минимум кода на PHP. Его главная задача — оформить страницу так, чтобы
она выглядела красиво. У меня нет никаких других стимулов, кроме как эконо-
мии типографской краски, чтобы не разнести данные блоки по разным файлам.
Мы еще вернемся к такому подходу в одной из следующих глав.
Сложные имена полей
Как вы, наверное, помните, элементы формы могут иметь имена, выглядящие, как
эле-
менты массива: A[10], B[1][text] и т. д. До недавнего времени (в третьей версии
PHP)
это касалось только "обычных" полей, но не полей закачки файлов. К счастью, в
PHP
версии 4 все изменилось в лучшую сторону.
Давайте применим указанные возможности в следующем примере формы и опреде-
лим, какие переменные создаст PHP при ее отправке на сервер.
После того как программа script.php примет данные из формы, PHP создаст для
нее следующие переменные:
r ассоциативный массив $File, ключи которого — text, bin и pic, а соответст-
вующие значения — имена временных файлов на сервере, созданных PHP при за-
грузке;
r массив $File_name все с теми же ключами и значениями — именами файлов в
системе пользователя;
r массив $File_type с теми же ключами и значениями — типами соответствую-
щих файлов;
r массив $File_size со значениями — размерами этих файлов.
Мы видим, информация об индексах в именах полей формы попала в ключи соответ-
ствующих массивов и сохранилась в них. Вы можете убедиться в том, что перемен-
ные действительно инициализированы, воспользовавшись вызовом функции
Dump($GLOBALS), код которой приведен в конце главы 11, и в полезности которой
вы
теперь можете убедиться на примере.
Еще раз напоминаю, что PHP версии 3 неправильно работает с подобными
именами полей. Учитывайте это, если собираетесь использовать старый ин-
терпретатор.
Часть V. Приемы программирования на PHP
396
Проблемы со сложными именами
Но не все так восхитительно, как может показаться на первый взгляд. Беда в том,
что
описанный механизм работает замечательно, лишь когда мы задействуем элементы
одномерных массивов в качестве имен полей формы. В случае же многомерных мас-
сивов дела обстоят несколько хуже. Правда, многомерные массивы используются при
закачке значительно реже, но все равно, мой долг — предупредить вас и уберечь
от
возможного недоразумения.
Итак, напишем форму:
При приеме данных такой формы PHP "запутается" и, хотя и создаст массив $File,
но не поместит в него никаких полезных данных. А именно, в моей версии 4.0.3pl1
в
элемент с ключом a вместо имени файла попадает какое-то комплексное (судя по
его
странному виду) число.) Надеюсь, в будущих версиях интерпретатора это досадное
недоразумение будет исправлено.
Но все же существует метод, с помощью которого мы сможем обработать и такие
"неправильные" с точки зрения PHP формы. Я об этом еще не упоминал, но PHP, по-
мимо установки вышеперечисленных переменных, создает также глобальный массив
с именем $HTTP_POST_FILES. Как показывает практика, в этом массиве содержатся
верные данные, какое бы имя не имело поле закачки в форме.
Массив $HTTP_POST_FILES создается не всегда, а только в том случае, если
в настройках PHP задействован параметр track_vars. Так как, судя по доку-
ментации, в PHP версии 4 он включен всегда (чего нельзя сказать о третьей
версии), то беспокоиться не о чем.
Массив $HTTP_POST_FILES используется довольно редко, так что я предоставляю
читателю возможность самостоятельно разобраться, в каком формате хранятся в нем
данные. Это несложно. Вам не потребуется ничего, кроме функции Dump(), которая
уже упоминалась в этой главе, и, конечно, желания экспериментировать.
Глава 29
Модульность программы.
Написание "библиотекаря"
Во всех серьезных языках программирования имеется возможность писать модуль-
ные программы. Иными словами, при определенных навыках вы можете разбить
свою программу на относительно независимые части, каждую из которых реализо-
вать в виде модуля. Особенно это бывает полезно, если над программой работает
сра-
зу коллектив разработчиков (как чаще всего и бывает) — в этом случае остается
лишь продумать связи между модулями, написание которых можно поручить разным
программистам.
Модули обычно также используют другие модули в своей работе, те — третьи, и т.
д.,
до самого низкого уровня. Хорошо написанный модуль подобен новому автомобилю:
его интерфейсные функции — это аналог руля и педаль, а уж что там под капотом —
программиста, подключающего модуль, волновать не должно.
Тем не менее, должен вас огорчить: к сожалению, разработчики PHP не
предусмотре-
ли в языке сколько-нибудь удобной поддержки модульности. Однако не впадайте в
уныние: дело в том, что такую поддержку можно в язык добавить, причем относи-
тельно несложными приемами самого PHP и сравнительно небольшими затратами с
точки зрения быстродействия. Этим мы и займемся в настоящей главе.
Наши требования
Возможно, вы возразите: "Как же нет никакой поддержки модульности?
А инструкция include?" Да, разумеется, уж лучше использовать include, вместо
того чтобы хранить всю программу в одном-единственном файле. Но дело в том, что
применение этой инструкции довольно-таки неудобно по той простой причине, что
поиск подключаемых файлов проводится только в тех каталогах, которые указал ад-
министратор при установке PHP. У многих хостинг-провайдеров мы не можем изме-
нять по своему усмотрению эти каталоги, а указание относительных путей
(например,
../../php/somefile.php) оказывается довольно проблематичным (представьте
только, сколько всего нам придется изменять, если мы захотим расположить нашу
программу в другом месте).
Часть V. Приемы программирования на PHP
398
Возможно, этот разговор о каталогах выглядит с первого взгляда несколько
надуманно, однако люди, уже столкнувшиеся когда-либо с рассматриваемой
проблемой, по достоинству оценят затраченные усилия, особенно если их сце-
нарии состоят из десятков файлов и библиотек.
Помните, что при помощи include или require нельзя один и тот же файл загру-
жать дважды (как это часто бывает, если один модуль вызывает другой, но
програм-
ма об этом "не знает" и еще раз подключает первый — опять же, стандартный слу-
чай). В самом деле, если в этом файле находится, к примеру, описание
какой-нибудь
функции, то при следующем его включении PHP выдаст ошибку: повторное объявле-
ние функции. Конечно, последняя проблема полностью решается подстановкой
include_once вместо include, что работает, кстати, только в PHP версии 4.
Отсюда мы можем сформулировать главные два требования.
r Механизм загрузки модуля должен сам решать, в каком каталоге располагается
модуль, независимо от того, где выполняется сценарий. В любой программе воз-
можность загрузить указанный по имени модуль должна быть легко осуществима.
Мы хотели бы, чтобы это было так же просто, как мы делаем это с обычными
файлами из текущего каталога при помощи include.
r Один и тот же модуль не должен загружаться дважды, даже если программа по-
пытается это выполнить.
К слову сказать, оба требования реализованы, например, в языке Perl.
Как я уже говорил, мы можем написать нужную нам "инструкцию", которая будет
загружать модуль с применением указанных принципов прямо на PHP. Назовем ее
Uses() и оформим в виде функции.
Далее для краткости модулем на PHP я буду называть файл (например, с рас-
ширением phl), содержащий некоторые общеупотребительные функции, кон-
станты и переменные, а также исполняемую часть, которая запускается при
первой (и только первой) загрузке модуля.
Библиотекарь
Ту часть кода, которая будет содержать функцию Uses() (а мы реализуем ее именно
в виде функции) и другие функции, нужные для загрузки модулей, назовем библио-
текарем. Этот библиотекарь, очевидно, сценарию придется загружать первым, а ка-
ким именно образом, мы поговорим чуть позже.
Глава 29. Модульность программы. Написание "библиотекаря"
399
Теперь немного о том, как мы будем реализовывать Uses(). Это довольно несложно.
Помните, я подчеркивал, что поскольку PHP является интерпретатором, то на нем
осуществимы такие приемы, как описание функций внутри функций и многое другое.
Так мы и сделаем: функция Uses() вначале будет проверять, не загружался ли уже
модуль с таким именем, затем искать затребованный модуль в специальных "катало-
гах для модулей", фиксировать во внутреннем массиве факт, что указанный файл
за-
гружен, и, наконец, вызывать include_once для файла с модулем. Кроме того, на
время загрузки текущий каталог будет сменяться на тот, в котором находится
модуль,
чтобы стартовые части всех модулей запускались в "своих" каталогах. Это как раз
та
возможность, которая отсутствует в Perl, и которая оказывается довольно удобной
на
практике.
Раз библиотекарь всегда подключается к программе в первую очередь, разумно
дове-
рить ему выполнение еще некоторых действий.
r Поместим в файл библиотекаря функции, чаще всего необходимые почти каждому
сценарию. Таким образом, мы как бы "расширим" набор встроенных в PHP функ-
ций. Однако помните, что встроенные функции переопределять все же нельзя,
можно лишь создавать новые с уникальными именами.
r Библиотекарь, как никто другой, должен приложить максимум усилий, чтобы сде-
лать сценарии переносимыми с одной платформы на другую. Для нас это будет
заключаться в корректировке некоторых переменных, которые PHP создает перед
выполнением программы. Первым кандидатом на такую правку будет
$SCRIPT_NAME (а также одноименная переменная окружения), которая, как мы
знаем, в Windows-версии PHP содержит не то значение, которое мы ожидаем.
r И еще нам хочется, чтобы на момент загрузки модуля текущий каталог сменялся
на тот, в котором расположен файл модуля. Таким образом, стартовая часть биб-
лиотеки всегда сможет определить, где она находится, — например, при помощи
вызова getcwd().
Вот что у нас получится в результате:
Листинг 29.1. Библиотекарь: librarian.phl
$s) if($s!=".") {
// Признак корневого каталога?
if(!$i && (strlen($s)>1&&$s[1]==":"||$s=="")) $Path=$s;
// Ссылка на родительский каталог?
elseif($s=="..") {
// Если это уже корневой каталог, то куда спускаться?..
if(strlen($Path)>1 && $Path[1]==":") continue;
// Иначе используем dirname()
$p=dirname($Path);
if($p=="/"||$p=="\\"||$p==".") $Path=""; else $Path=$p;
}
// Иначе просто имя очередного каталога
elseif($s!=="") $Path.="/$s";
}
return ($Path!==""?$Path:"/");
}
// Преобразует URL в абсолютный файловый путь.
// Т. е. если адрес начинается со слэша, то результат рассматривается
// по отношению к каталогу DOCUMENT_ROOT, а если нет — то относительно
// dirname($SCRIPT_NAME). Конечно, функция не безупречна (например, она
// не умеет обрабатывать URL, заданные Alias-директивами Apache, но в
// большинстве случаев это и не нужно.
Глава 29. Модульность программы. Написание "библиотекаря"
401
function Url2Path($name)
{ $curUrl=dirname($GLOBALS["SCRIPT_NAME"]);
$url=abs_path(trim($name),$curUrl);
return getenv("DOCUMENT_ROOT").$url;
}
// Превращает все пути в списке $INC в абсолютные, однако делает это
// не каждый раз, а только если массив изменился с момента последнего
// вызова.
function AbsolutizeINC()
{ global $INC;
static $PrevINC=""; // значение $INC при предыдущем входе
// Сначала проверяем — изменился ли $INC. Если да, то преобразуем
// все пути в массиве в относительные, иначе ничего не делаем.
// Нам это нужно только из соображений повышения производительности
// функции.
if($PrevINC!==$INC) {
// Мы не можем использовать foreach, т. к. нам надо
// модифицировать массив
for($i=0; $i$v) global $$k;
// Включаем файл
$ret=include_once($file);
// Пока не вернулись в предыдущий каталог, перевести
// добавленные (возможно?) пути в $INC в абсолютные
AbsolutizeINC();
// Вернуться
chdir($cwd);
return $ret;
}
$LastFound=($LastFound+1)%count($INC);
} while($LastFound!=$l);
// Ничего не вышло — "умираем"...
die("Couldn't find library \"$libname\" at ".join(", ",$INC)."!");
}
// Корректируем некоторые переменные окружения, которые могут иметь
// неверные значение, если PHP установлен не как модуль Apache
@putenv("SCRIPT_NAME=".
$GLOBALS["HTTP_ENV_VARS"]["SCRIPT_NAME"]=
$GLOBALS["SCRIPT_NAME"]=
ereg_Replace("\\?.*","",getenv("REQUEST_URI"))
);
@putenv("SCRIPT_FILENAME".
$GLOBALS["HTTP_ENV_VARS"]["SCRIPT_FILENAME"]=
$GLOBALS["SCRIPT_FILENAME"]=
Url2Path(getenv("SCRIPT_NAME"))
Глава 29. Модульность программы. Написание "библиотекаря"
403
);
// На всякий случай включаем максимальный контроль ошибок
Error_reporting(1+2+4+8);
// ВНИМАНИЕ! После следующего закрывающего тэга
// не должно быть НИКАКИХ ПРОБЕЛОВ! В противном случае
// сценарий, подключающий библиотекаря, будет выводить в самом
// начале своей работы этот пробел, что недопустимо при
// работе с Cookies.
}?>
Обратите внимание на то, что весь код библиотекаря помещен в блок оператора if.
Это сделано специально, чтобы при возможной (ошибочной) повторной загрузке биб-
лиотекаря по include все работало корректно.
Возможно, вы скажете, что то же самое можно было бы сделать и в модулях, и
обойтись вообще без библиотекаря. Однако это приведет к заметной потере
производительности, потому что интерпретатору каждый раз придется загру-
жать и разбирать весь файл модуля, а это — основное время при запуске про-
граммы.
Пожалуй, в приведенном коде есть и еще одно интересное место. Я имею в виду ин-
струкции, помеченные комментарием: "Делаем доступными для модуля все глобаль-
ные переменные". Зачем это нужно? Разве глобальные переменные по определению
не доступны подключаемому модулю? К сожалению, это так, и вот почему. Мы вы-
зываем include_once в теле функции Uses(), а не в глобальном контексте. Неуди-
вительно, что подключенный файл работает не в нем, а в области видимости тела
функции. Указанный цикл перебора всех глобальных переменных и их "глобализа-
ция" с помощью global решает проблему.
Здесь есть еще одна тонкость. Если модуль "захочет" определить какую-либо
новую глобальную переменную, он не сможет сделать это никак иначе, чем
через массив $GLOBALS. Однако изменять имеющиеся переменные напрямую
он все же способен.
Работа с библиотекарем
Рассмотрим пример сценария, использующего библиотекарь в своей работе. Мы бу-
дем предполагать, что все модули размещены в подкаталоге /lib основного
каталога
Часть V. Приемы программирования на PHP
404
с Web-документами (если вы заметили, такой каталог уже есть в путях поиска
моду-
лей по умолчанию, "зашитых" в библиотекаре).
Пока мы будем подключать библиотекаря явно — инструкцией include. Ко-
нечно, это не очень удобно. Очень скоро мы узнаем, как избавиться от указан-
ного недостатка.
Пусть сценарию требуется библиотека files.phl, которую мы написали (или где-то
достали, хотя модули для PHP все еще большая редкость), и которая содержит
неко-
торые функции для работы с файлами.
Кстати, модулю files.phl самому могут понадобиться некоторые модули.
Если это так, нет проблем: достаточно лишь поставить вызов Uses() внутрь
кода библиотеки.
Листинг 29.2. Тестовый сценарий
include "$DOCUMENT_ROOT/lib/librarian.phl"; // подключаем библиотекарь
Uses("files"); // подключаем модуль files.phl
// Все — теперь можно использовать модуль
$Content=ReadAllFile("myfile.txt"); // читаем весь файл myfile.txt
$Hash=ReadKeyValFile("keyval.txt"); // читаем файл формата key=value
// ... и другие функции, которые, возможно, присутствуют в модуле
?>
Как видите, ничего сложного. Давайте теперь посмотрим, как выглядит модуль
files.phl.
Листинг 29.3. Пример модуля files.phl
// Внимание! Так указывается дополнительный каталог для поиска модулей.
// Запись означает, что библиотекарь должен искать модули также и в
// подкаталоге OtherModules/dk текущего каталога
$INC[]="OtherModules/dk";
// Подключение каких-то других модулей, в которых нуждается files.phl
Uses("SomeOtherModule");
Uses("AndOtherModuleToo");
Глава 29. Модульность программы. Написание "библиотекаря"
405
// Константа: символы перевода строки
define("CRLF",getenv("COMSPEC")?"\r\n":"\n");
// Читает все содержимое файла $fname и возвращает его
function ReadAllFile($fname)
{ $f=fopen($fname,"r"); if(!$f) return "";
$Cont=fread($f,1000000); fclose($f);
return $Cont;
}
// Читает файл $fname, строки которого имеют формат
// ключ1=значение1
// Возвращает ассоциативный массив с указанными в файле ключами
function ReadKeyValFile($fname)
{ $Cont=@File($fname); if(!@is_array($Cont)) return array();
$Hash=array();
foreach($Cont as $i=>$st) {
if(!ereg("^([^=]+)=(.*)",$st,$regs)) continue;
$Hash[trim($regs[1])]=trim($regs[2]);
}
return $Hash;
}
?>
Автоматическое подключение
библиотекаря
Из листинга 29.2 можно видеть, что пока нам не удалось полностью избавиться от
указания абсолютного пути к библиотекам. Вот строка, которая мне не нравится:
include "$DOCUMENT_ROOT/lib/librarian.phl"; // подключаем библиотекарь
Действуя привычным способом, нам придется вставлять ее в каждый сценарий, кото-
рый планирует использовать библиотекаря. Этих сценариев может быть довольно
много, так что если мы вдруг захотим изменить lib на, скажем, ../libraries, то
придется править все программы. По закону Мэрфи где-нибудь да ошибетесь — обя-
зательно. А значит, такое решение нам, как дотошным программистам, не подходит.
К счастью, существует еще по крайней мере два способа решить проблему с
абсолют-
ными путями, и который из них выбрать — зависит от ситуации.
Часть V. Приемы программирования на PHP
406
Здесь я хочу оговориться: разумеется, где-то все равно придется задать путь
к библиотекарю, но такое место будет только одно, поэтому в случае нужды
его легко модифицировать.
Способ первый: использование
auto_prepend_file
Как следует из Приложения 2, PHP опирается при выполнении сценариев на специ-
альный файл конфигурации под названием php.ini, в котором хранится большинст-
во его настроек, заданных в виде директив. Кроме того, если PHP установлен как
модуль Apache (а именно так обстоит дело у большинства хостинг-провайдеров),
не-
которые директивы можно также включать прямо в файлы .htaccess, управляющие
работой сервера. Последние могут быть помещены в любой каталог, содержащий
сценарии на PHP. Таким образом, для заданного каталога и всех его подкаталогов
указанные настройки всегда будут действовать.
Помните, что для помещения директивы PHP с каким-нибудь именем NAME в
файл .htaccess ее нужно назвать php_NAME, а значение отделить от имени
не знаком =, как в php.ini, а пробелом. В противном случае Apache будет со-
общать о неизвестной директиве в файле конфигурации.
Среди обрабатываемых интерпретатором директив есть две особенных. Называются
они auto_prepend_file и auto_append_file. В первой задается абсолютный
путь к файлу, содержащему код на PHP, который будет автоматически выполняться
перед запуском любого сценария. Не правда ли, это то, что нам нужно?
Конечно, вставлять директиву auto_prepend_file в глобальный php.ini нет ни-
какого смысла. Ведь у подавляющего большинства хостинг-провайдеров одни и те же
Apache и PHP обслуживают сразу несколько виртуальных хостов, принадлежащих
разным владельцам. А значит, никто не разрешит вам изменять глобальные настрой-
ки интерпретатора. В этом случае модификация файлов .htaccess оказывается
единственно правильным и возможным решением. Правда, для этого нам нужно
знать, какой физический каталог соответствует на нашем сервере корневому для
до-
кументов. Выяснить это можно, например, с помощью такого простого сценария:
Листинг 29.4. Определение физического корневого каталога сервера
echo $DOCUMENT_ROOT;
?>
Глава 29. Модульность программы. Написание "библиотекаря"
407
Пусть, к примеру, у нашего хостинг-провайдера используется каталог
/home/dk/www. Тогда для автоматического подключения библиотекаря ко всем сце-
нариям на PHP нужно добавить в файл .htaccess примерно такую строку:
php_auto_prepend_file /home/dk/www/lib/librarian.phl
Вообще говоря, лучше всего сделать это в файле .htaccess, который нахо-
дится в корневом каталоге сервера, для того чтобы подключение библиотекаря
происходило ко всем сценариям во всех каталогах. Если этого файла не суще-
ствует, необходимо его создать.
Как уже упоминалось, данный способ не подходит для того виртуального сервера
для
Windows, установка которого описана в части II настоящей книги. Изменение
php.ini — тоже не очень удачная идея в силу вышеизложенных рассуждений. Тут
нам на помощь придет второй способ, который мы сейчас и рассмотрим.
Способ второй: установка обработчика Apache
Установка своего обработчика сопряжена с несколько большими сложностями, чем
использование директив auto_prepend_file и auto_append_file. Тем не менее,
он позволяет нам получить чуть больший контроль над сервером, поскольку
перекла-
дывает задачу выбора и запуска нужного сценария на плечи программиста. Это —
установка нового обработчика Apache. Тема настолько важна, что мы, пожалуй, от-
ложим на время нашего библиотекаря (к нему мы еще обязательно вернемся) и зай-
мемся непосредственно обработчиками.
Обработчики Apache
Итак, что же такое обработчик Apache? На самом деле мы постоянно сталкиваемся с
одним из классических примеров обработчика. Да-да, вы уже догадались: это сам
PHP. Если чуть углубиться в теорию, то обработчиком называется сценарий (воз-
можно, встроенный в сам сервер, как это происходит с PHP), который запускается
сервером при попытке пользователя открыть ту или иную страницу определенного
типа.
Каждый обработчик должен иметь уникальный идентификатор — имя обработчика,
который я для краткости буду называть просто именем. Оно может состоять только
из
алфавитно-цифровых символов и знаков подчеркивания. Заметьте, что это имя — не
то же самое, что имя файла сценария, в котором хранится код обработчика. Имя
об-
работчика и является тем, которое нужно указывать серверу в директиве
AddHandler, когда мы хотим связать определенные документы с нашим сценарием.
Часть V. Приемы программирования на PHP
408
Но как же сопоставить идентификатор обработчика тому сценарию, который содер-
жит его код? У сервера Apache для этого есть специальная директива под
названием
Action. Где задается эта директива? В любом файле конфигурации Apache. Конечно,
удобнее всего это делать в файле .htaccess, расположенном в корневом каталоге
виртуального хоста, чтобы изменения распространились сразу на весь сервер. Мы
уже рассматривали такую стратегию выше, только теперь все будет чуточку сложнее.
Вот требуемые директивы. Поместим их, как водится, в главный .htaccess-файл
хоста.
# Сначала связываем имя обработчика с конкретным файлом.
# Знак "?" говорит серверу, что исходный URL запроса следует
# передать сценарию методом GET, т. е. через QUERY_STRING.
Action libhandler "/lib/libhandler.php?"
# Теперь уведомляем сервер, документы какого типа мы желаем
# "пропускать" через наш обработчик.
AddHandler libhandler .html .htm
В этом фрагменте есть два тонких места.
r Путь к сценарию обработчика всегда указывается как абсолютный URL без указа-
ния имени хоста и порта. Мы не можем задать здесь ни путь к файлу, ни даже от-
носительный URL. По той причине, чтобы позволить одному обработчику "пере-
давать эстафету" другому. В самом деле, ведь это и происходит в нашем примере:
Apache сначала определяет, что документ нужно "пропустить" через обработчик
libhandler, а т. к. он представляет собой ни что иное, как сценарий на PHP, то
и
через интерпретатор PHP. В деталях затронутый процесс чуть сложнее, но мы не
будем в него углубляться.
r После URL сценария в директиве Action следует знак ?. Зачем он? Это связано с
механизмом, который использует Apache для того, чтобы определить конечный
обработчик для того или иного документа. Когда пользователь посылает серверу
URL, который, как Apache "знает", подходит под одну из команд Action, к этому
URL слева просто присоединяется второй параметр директивы, и все начинается
сначала — до тех пор, пока не будет найден последний обработчик. Например, ес-
ли пользователь ввел /dir/file.html, то благодаря директиве Action указан-
ный адрес преобразуется в /lib/libhandler.php?/dir/file.html. Это — ни
что иное, как адрес PHP-сценария с параметром, который будет передан програм-
ме, как обычно, через переменную окружения QUERY_STRING.
Теперь сервер знает, что все документы с расширением html и htm нужно обрабаты-
вать при помощи сценария, расположенного по адресу /lib/libhandler.php. Точ-
нее, при каждой попытке открыть страницы с указанными расширениями Apache бу-
дет запускать наш сценарий и в числе обычных переменных окружения отправлять
ему несколько специальных, содержащих первичную информацию о запросе, пере-
данном пользователем. Мы сейчас рассмотрим эти переменные на практике. Если вас
интересует их полный список, попробуйте распечатать массив $GLOBALS или вос-
Глава 29. Модульность программы. Написание "библиотекаря"
409
пользоваться функцией phpinfo(), вставив ее первой и единственной командой об-
работчика libhandler.php.
Вы, возможно, спросите, почему же мы не добавили в список расширений, на
которые будет "реагировать" сценарий, еще одно — php? Давайте посмотрим,
что бы произошло, поступи мы так. Предположим, пользователь хочет загру-
зить страницу /a.php. Apache "видит", что расширение у нее — php, и запус-
кает обработчик с именем /lib/libhandler.php. Точнее, он "сваливает"
всю работу на сценарий libhandler.php. Еще точнее — перенаправляет
сервер по адресу /lib/libhandler.php?a.php! И тут же зацикливается,
потому что у этого сценария расширение — также php. Итак, сервер начинает
вызывать сценарий снова и снова, все удлиняя его URL — до тех пор, пока не
"поймет": что-то неверно, и пора аварийно завершаться, о чем и сообщает в
файлах журнала. О том, как решить эту проблему, рассказано в самом конце
главы.
Ну вот, у нас уже почти все готово. Осталось только написать сам код
обработчика.
Это не так уж и сложно. Но прежде давайте вспомним, зачем мы вообще связались с
обработчиками. Для автоматической загрузки библиотекаря перед выполнением того
или иного сценария, помните? Что же, вот пример (листинг 29.5).
Мы подразумеваем, что обработчик libhandler.php находится в том же са-
мом каталоге, что и библиотекарь с большинством модулей. Это довольно
удобно, поскольку позволяет нам задавать путь к каталогу с модулями лишь в
единственном месте — в директиве Action файла .htaccess, да и то в виде
относительного URL. Оцените, насколько это проще для будущих модифика-
ций сайта.
Листинг 29.5. Обработчик /lib/libhandler.php с подключением библиоте-
каря
// Прежде всего, устанавливаем свои каталоги поиска модулей.
// Это, по нашей договоренности, — текущий в данный момент каталог.
$INC[]=getcwd();
// Проверяем, не пытается ли пользователь запустить обработчик напрямую,
// минуя Apache — например, путем набора в браузере адреса
// /lib/libhandler.php. Так как адрес, введенный пользователем,
// всегда передается в переменной окружения REQUEST_URI, то нужно
// "бить тревогу", если переданная строка адреса встречается
Часть V. Приемы программирования на PHP
410
// в имени файла обработчика (причем в любом регистре символов).
// Мы не забыли отрезать в этой строке часть после ?, потому что
// она будет мешать при сравнении с именем файла.
// К сожалению, похоже, это единственный переносимый между операционными
// системами способ проверки легальности запуска обработчика.
$FileName=strtr(__FILE__,"\\","/");
$ReqName=ereg_Replace("\\?.*","",strtr(getenv("REQUEST_URI"),"\\","/"));
if(eregi(quotemeta($ReqName),$FileName)) {
// Выводим сообщение об ошибке
include "libhandler.err";
// Записываем в журнал данные о пользователе
$f=fopen("libhandler.log","a+");
fputs($f,date("d.m.Y H:i.s")." $REMOTE_ADDR - Access denied\n");
fclose($f);
// Завершаем работу
exit;
}
// Все в порядке — корректируем переменные окружения в соответствии
// с запрошенным пользователем адресом.
@putenv("REQUEST_URI=".
$GLOBALS["HTTP_ENV_VARS"]["REQUEST_URI"]=
$GLOBALS["REQUEST_URI"]=
getenv("QUERY_STRING")
);
@putenv("QUERY_STRING=".
$GLOBALS["HTTP_ENV_VARS"]["QUERY_STRING"]=
$GLOBALS["QUERY_STRING"]=
ereg_Replace("^[^?]*\\?","",getenv("QUERY_STRING"))
);
// Подключаем библиотекарь
include "librarian.phl";
// Здесь можно выполнить еще какие-нибудь действия...
// . . .
// Запускаем тот сценарий, который был запрошен пользователем
chdir(dirname($SCRIPT_FILENAME));
include $SCRIPT_FILENAME;
?>
Глава 29. Модульность программы. Написание "библиотекаря"
411
Ну и, конечно, какая же программа обходится без вывода диагностических сообще-
ний? Наш пример подгружает файл libhandler.err в случае "жульничества" поль-
зователя. Наверное, в нем следует написать что-то типа:
Доступ запрещен!
Доступ запрещен!
Пользователь сделал попытку нелегально вызвать обработчик Apache,
отвечающий за автоматическое подключение библиотекаря. Так как это
свидетельствует о его желании нелегально проникнуть на сервер,
попытка была пресечена. Информация о пользователе записана
в файл журнала.
В результате мы пришли к тому, что теперь все документы с расширениями html и
htm рассматриваются как сценарии на PHP. Они запускаются уже после того, как
подключен библиотекарь, так что могут пользоваться функцией Uses().
Если вы не собираетесь использовать библиотекарь, а хотите применять опи-
санный выше механизм только для того, чтобы включить PHP для файлов с
расширением html, лучше прочитайте конец этой главы. Там описано, как сде-
лать это проще.
Перехват обращений
к несуществующим страницам
Самое интересное, что наш обработчик будет вызываться как для существующих
файлов с расширением html, так и для несуществующих (правда, расположенных в
существующем каталоге). Какой простор это открывает для творчества! Например,
мы можем написать систему новостей или форум, в котором у всех сценариев не бу-
дет ни одного "видимого" параметра. Все данные могут передаваться прямо в имени
файла, например:
/forum/Computers-01-04-01.html
Хотя файла Computers-01-04-01.html нет и в помине, обработчик может пере-
хватить запрос к нему и определить, что речь идет о новостях в разделе
"Компьютеры" за 1 апреля 2001 года. Затем, получив нужную информацию из базы
данных, остается лишь отправить ее клиенту.
Обычно для подобных целей используют специальный модуль Apache —
mod_rewrite. К сожалению, по статистике далеко не все хостинг-провайдеры
Часть V. Приемы программирования на PHP
412
соглашаются устанавливать его на свои серверы. В то же время механизм
ActionAddHandler работает всегда и везде, где установлен Apache.
Надо заметить, что в примере из листинга 29.5 мы никак не перехватываем обраще-
ния к несуществующим страницам. Что происходит, если пользователь все же введет
неправильный адрес? Очевидно, вызов include, стоящий в предпоследней строчке,
завершится неуспешно, а PHP выведет сообщение об ошибке. Наверное, в реальной
программе нужно как-то обрабатывать эту ситуацию, — например, при помощи про-
верки существования запрошенного файла.
Связывание PHP с другим расширением
Как мы знаем, сам PHP представляет собой обычный обработчик. Значит, скажете
вы, чтобы заставить его обрабатывать документы с расширением, отличным от PHP,
нам нужно просто добавить директиву AddHandler для этого расширения в соответ-
ствующий файл .htaccess? Не совсем. Проблема заключается в том, что мы не зна-
ем идентификатора обработчика, он хранится где-то в недрах кода интерпретатора.
Вместо этого мы поступим по-другому: заставим Apache считать, что документы с
нужным нам расширением имеют тот же тип, что и с расширением php.
Что же такое тип документа? Это еще одно понятие, которое использует Apache в
своей работе. Некоторые из этих типов также "понимают" и браузеры. В их числе,
например, text/html, обозначающий HTML-страницу, image/gif, который сигна-
лизирует, что данные являются рисунком GIF,
и т. д. Именно этими типами (а не расширениями страниц!) руководствуются
браузе-
ры, когда решают, в каком формате прислал сервер данные.
Однако есть несколько типов документов, которые никогда не отсылаются браузеру
в
исходном виде. Один из них — application/x-httpd-php. Именно с этим типом
и связан интерпретатор PHP. Если сервер "видит", что пользователь запросил
стра-
ницу, которая имеет тип application/x-httpd-php, он активизирует PHP, а уж тот
берет на себя всю дальнейшую ответственность по запуску сценария и выводу "пра-
вильного" заголовка типа (чаще всего text/html) в браузер.
Как же сервер узнает, какой тип имеет тот или иной документ? Вообще говоря, это
отдельная проблема. Самое простое ее решение — определять тип по расширению
файла. В большинстве случаев это оказывается самым лучшим решением. Програм-
мист может сам задать, какое расширение соответствует тому или иному типу,
доба-
вив в нужный файл .htaccess следующую директиву:
AddType имя_типа расширение1 расширение2 …
А как быть, если многие из наших документов не имеют в принципе никакого
расширения? Например, мы хотим хранить рисунки GIF, JPG и PNG в файлах
без расширения. Разумеется, в этом случае директива AddType нам не помо-
Глава 29. Модульность программы. Написание "библиотекаря"
413
жет. Однако у Apache существует еще одно мощное средство для распознава-
ния типов страниц — это модуль mod_mime_magic (конечно, если он подклю-
чен к той версии сервера, которая установлена у вашего хостинг-провайдера).
В случае, если определение типа на основе директив AddType закончилось
неудачей, этот модуль пытается по нескольким первым байтам файла узнать,
какого же он типа. Например, во всех GIF-файлах первые три байта — симво-
лы G, I и F. Поэтому с вероятностью практически 100% определение типа про-
ходит правильно.
Предположим, что мы хотим связать расширение php4 с PHP для всего сайта. Для
этого запишем в файл .htaccess, расположенный в корневом каталоге сервера, та-
кую директиву:
AddType application/x-httpd-php php4
Теперь для всех файлов с расширением php4 будет выполняться то же, что и для
php.
Кстати говоря, именно такая директива (но для php) записана в главном файле
httpd.conf вашего хостинг-провайдера.
Решение проблемы
зацикливания обработчика
Помните, обработчик из листинга 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 могут использовать функции, предоставляе-
мые библиотекарем.
Глава 30
Код и шаблон
страницы
Что и говорить, конечно, очень удобно, что PHP позволяет комбинировать код про-
граммы с обычным HTML-текстом, но этой возможностью все же не стоит злоупот-
реблять. И особенно в больших сценариях. Это чередование очень плохо смотрится:
сначала код, потом — вставки HTML, а затем — опять код. Кроме того, вашему
HTML-верстальщику будет крайне трудно понять, где же в этом сценарии именно
"его" участки, которые он может править и изменять.
Впрочем, особых проблем здесь нет: я предлагаю отделять почти весь код сценария
от текста, задающего внешний вид страницы. А именно — хранить их в разных фай-
лах. Я уже неоднократно затрагивал такой подход в этой книге, все время
ссылаясь
(не совсем явно) на настоящую главу. Что же, теперь настало время по
достоинству
оценить тот выигрыш, который дает нам отделение кода от шаблона страницы.
Думаете, сейчас мы будем углубляться в "дебри теории", далекой от практики и
вряд
ли вам полезной? Ничего подобного. Я просто расскажу, как можно удобно строить
свои программы, а в конце приведу довольно "внушительный" код шаблонизатора
(так я называю систему управления страницами и шаблонами), который призван сде-
лать работу Web-программиста максимально простой и эффективной.
Некоторые программисты утверждают, что отделению кода от шаблона стра-
ницы уделяют слишком много внимания — чрезмерно много. Если и вы так
думаете, — что же, я не буду с вами спорить и критиковать вашу точку зрения.
Если бы я не занимался этой проблемой столько времени, то, возможно, и сам
бы так считал. Будем честны: отвечает ли проблема отделения кода от шабло-
на страницы тому вниманию и количеству страниц, что я ей здесь уделил? От-
кровенно говоря, не отвечает. В действительности, чтобы полностью расска-
зать о возможных решениях задачи, потребовалось бы написать отдельную
книгу размером в тысячу страниц. Я же ограничусь всего кое-какими рассужде-
ниями и примером простейшего шаблонизатора.
Часть V. Приемы программирования на PHP 416
Идеология
Большинство сценариев пишутся на различных языках программирования без всяко-
го отделения кода от шаблона страницы. Зачем же тогда нам это нужно? Что
застав-
ляет нас искать новые пути в Web-программировании?
Причина всего одна. Это — желание поручить разработку качественного и сложного
сценария сразу нескольким людям, чтобы каждый из них занимался своим делом,
которое, как предполагается, он знает лучше всего. Одна группа людей (назовем
ее
"программисты") занимается тем, что касается взаимодействия программы с пользо-
вателем и обработки данных. Другая же группа (для простоты я буду говорить о
ней
как о "дизайнерах"), наоборот, отвечает лишь за эстетическую часть работы.
Разуме-
ется, программисты и дизайнеры — не единственные категории, которые нужно
сформировать при создании крупных сайтов. Безусловно, требуется еще одно лицо,
которое бы "связывало" и координировало их между собой. Им может быть человек,
не имеющий выдающихся достижений ни в Web-дизайне, ни в Web-
программировании, но в то же время наделенный хорошей интуицией и знаниями.
Если этого человека нет, кому-то все равно придется выполнять его работу
(напри-
мер, одному из программистов), что, конечно же, будет немного противоречить же-
ланиям последнего. В результате работа над проектом затянется и, возможно,
"обрас-
тет" излишними сложностями технического характера.
Я убежден, что нельзя быть одновременно хорошим программистом и выдаю-
щимся дизайнером в указанном только что понимании. Эти две профессии
взаимоисключают друг друга, поскольку требуют совершенно разных складов
мышления. Если у вас нет раздвоения личности, вы без труда определите для
себя, к какой категории людей принадлежите сами.
Зачем нам вообще понадобилось распределять разработку Web-сценариев по не-
скольким направлениям? Отвечаю последовательно. Во-первых, так создаются гораз-
до более качественные программы и Web-страницы. Во-вторых, сроки выполнения
работы значительно сокращаются за счет организации параллельного выполнения
задания. Если вас это все равно не убедило, вспомните о том, что именно так
органи-
зуются практически все крупные Web-студии по всему миру.
Что же получается, если в своих сценариях вы будете смешивать код и оформление
сценария? Фактически, его поддержкой и доработкой не сможет заняться никто,
кро-
ме вас самого. В самом деле: программиста будет раздражать постоянно встречаю-
щиеся вставки HTML-кода, а дизайнера — опасность случайно изменить какую-
нибудь важную функцию программы. Иными словами, такой метод (да и можно ли
назвать его методом?) совершенно не подходит при разработке мало-мальски круп-
ных проектов.
Глава 30. Код и шаблон страницы 417
С горечью отмечаю, что разработчики PHP практически не приблизили нас к
решению проблемы отделения кода от шаблона страницы. Создается впечат-
ление, что они преследовали как раз противоположные цели: максимально уп-
ростить совмещение HTML и PHP за счет снижения функциональности по-
следнего. Когда мы будем разбирать код шаблонизатора ниже в этой главе, вы
увидите, на какие "увертки" нам придется пойти, чтобы обойти все "подводные
камни", невольно расставленные для нас авторами PHP.
Двухуровневая схема
Итак, мы желаем максимально отделить работу программистов и дизайнеров. Давай-
те будем делать это не сразу, а постепенно, детализируя ситуацию. Вначале решим
более простую проблему: разделим код сценария и шаблон его страницы (что я
назы-
ваю двухуровневой схемой построения сценария). Это довольно несложно. Мы уже
поступали так в главе 28, когда писали сценарий простейшего фотоальбома. Теперь
мы поставим задачу более точно.
Шаблон страницы
Пусть нам нужно завести новый раздел сайта — гостевую книгу. Выделим для нее
отдельный каталог на сервере и создадим в нем файл примерно следующего содер-
жания (листинг 30.1). Назовем его шаблоном страницы.
Листинг 30.1. Шаблон: gbook.htm
Гостевая книга
Добавьте свое сообщение:
Гостевая книга:
$Entry) {?>
Имя человека: =$Entry['name']?>
Его комментарий: =$Entry['text']?>
}?>
Часть V. Приемы программирования на PHP 418
Видите, здесь почти нет PHP-кода, за исключением разве что одного-единственного
цикла foreach. Для человека, занимающегося внешним видом вашей гостевой кни-
ги и совершенно не разбирающегося в программировании, это не должно выглядеть,
как непреодолимое препятствие.
В некоторых других языках программирования мы могли бы написать систему, ли-
шенную и указанного недостатка, но обладающую всеми качествами рассматривае-
мой. Честно говоря, существует всего лишь один способ добиться этого:
"замаскиро-
вать" инструкцию foreach специальным псевдотэгом (который, как это ни
удивительно, гораздо лучше воспринимается дизайнерами), чтобы код выглядел при-
мерно так:
Имя человека: $name
Его комментарий: $text
Согласен, для программиста такая замена действительно кажется смешной. Однако
она сильно приближает шаблон нашей страницы к идеалу — практически "чистому"
HTML-коду.
Хочу сразу сказать всем любителям разбивать один шаблон на множество
файлов: их способ чаще всего не оправдывает себя при написании крупных
сценариев. Дело в том, что при такой организации довольно тяжело перестав-
лять подшаблоны внутри страницы. Кроме того, подшаблоны нужно как-то за-
гружать, а поручать эту задачу коду страницы не очень удобно все из тех же
соображений: придется работать и программисту, и верстальщику. Легче всего
это представить на примере все той же гостевой книги: если бы мы выделили
тело цикла foreach в отдельный файл и попытались избавиться от этой ин-
струкции, то пришлось бы переложить задачу циклического вывода данных на
плечи программиста, сообщив ему попутно имя подшаблона. Чувствуете,
сколько лишних зависимостей?..
Надо заметить, что реализовать "прозрачную" замену подобных тэгов на соответст-
вующие инструкции в PHP практически невозможно (во всяком случае, без ущерба
простоте отладки сценария). Это связано с чрезвычайной слабостью этого
интерпре-
татора в вопросе, касающемся "перехвата" и обработки ошибок во время выполнения
кода. К счастью, такая слабость оказывается непреодолимой лишь в подобных
"экзо-
тических" случаях. При написании шаблонизатора она сказывается гораздо меньше.
Глава 30. Код и шаблон страницы 419
Генератор данных
Конечно, это еще далеко не весь сценарий. Вы, наверное, заметили, что сердце
шаб-
лона — цикл foreach вывода записей — использует непонятно откуда взявшуюся
переменную $Book, по контексту — двумерный массив. Кроме того, при отправке
формы тоже ведь нужно предусмотреть некоторые действия (а именно, добавление
записи в книгу).
Мы видим, что где-то должен быть скрыт весь этот код. Он, действительно,
распола-
гается в отдельном файле с именем gbook.php. Отличительная черта этого файла —
то, что в нем нет никакого намека на то, как нужно форматировать результат
работы
сценария. Именно поэтому я называю его генератором данных (листинг 30.2).
Листинг 30.2. Генератор данных: gbook.php
define("GBook","gbook.dat"); // имя файла с данными гостевой книги
// Загружает гостевую книгу с диска. Возвращает содержание книги.
function LoadBook($fname)
{ $f=@fopen("gbook.dat","rb"); if(!$f) return array();
$Book=Unserialize(fread($f,100000)); fclose($f);
return $Book;
}
// Сохраняет содержимое книги на диске.
function SaveBook($fname,$Book)
{ $f=fopen("gbook.dat","wb");
fwrite($f,Serialize($Book));
fclose($f);
}
// Исполняемая часть сценария.
// Сначала — загрузка гостевой книги.
$Book=LoadBook(GBook);
// Обработка формы, если сценарий вызван через нее.
// Если сценарий запущен после нажатия кнопки Добавить...
if(!empty($doAdd)) {
// Добавить в книгу запись пользователя — она у нас хранится
// в массиве $New, см. форму в шаблоне. Запись добавляется,
// как водится, в начало книги.
$Book=array(time()=>$New)+$Book;
Часть V. Приемы программирования на PHP 420
// Записать книгу на диск.
SaveBook(GBook,$Book);
}
// Все. Теперь у нас в $Book хранится содержимое книги в формате:
// array (
// время_добавления => array(
// (или id) name => имя_пользователя,
// text => текст_пользователя
// ),
// . . .
// );
// Вот теперь загружаем шаблон страницы.
include "gbook.htm";
?>
Как видим, исполняемая часть довольно небольшая и, действительно, занимается
лишь подготовкой данных для их последующего вывода в шаблоне. Шаблон рассмат-
ривается этой составляющей как обычный PHP-файл, который она подключает при
помощи инструкции include. Ясно, что весь код шаблона (хотя его и очень мало)
выполнится в том же контексте, что и генератор данных, а значит, ему будет
доступна
переменная $Book.
Логически весь код можно разбить на 3 части. Первая из них — задание конфигура-
ции сценария, в нашем случае она состоит всего лишь в определении одной-
единственной константы GBook, хранящей имя файла гостевой книги. Во второй час-
ти, которую можно назвать "прикладным интерфейсом" гостевой книги, содержатся
описания функций, достаточных для работы с гостевой книгой. Это, конечно, функ-
ции загрузки и сохранения наполнения книги на диске. Наконец, третья часть
генера-
тора данных обрабатывает запросы пользователей на добавление данных в книгу.
Таким образом, для работы нашего сценария нужны три файла: генератор данных,
шаблон книги и файл с записями книги. В принципе, это минимум, если только не
привлекать для хранения записей базу данных (что, безусловно, лучше в больших
программах). Однако в нашем случае проще как раз работать с файлами, поэтому я
на них и остановился.
Обратите внимание: для того чтобы теперь переделать гостевую книгу так,
чтобы она использовала базу данных, а не файл, достаточно изменить всего
лишь 2 функции: LoadBook() и SaveBook(). Ни других частей генератора
данных, ни, тем более, шаблона это не затронет. На самом деле, такой подход
Глава 30. Код и шаблон страницы 421
не является случайностью: он очень тесно связан с трехуровневой схемой по-
строения интерактивных сценариев, о которой мы скоро будем говорить.
Взаимодействие генератора
данных и шаблона
Вернемся опять к тому же генератору данных. В нем мы проверяем, не запущен ли
сценарий книги в ответ на нажатие кнопки Добавить в форме. Тут я хочу кое-что
напомнить. Если вызвать программу без параметров, то пользователю будет просто
выдано содержимое гостевой книги, в противном же случае (то есть при запуске из
формы) осуществится добавление записи. Таким образом, мы "одним махом убиваем
двух зайцев": используем один и тот же шаблон для двух разных страниц, внешне
крайне похожих. Такую практику нужно только приветствовать, не правда ли? Опре-
деляем мы, нужно ли добавлять запись, по состоянию переменной $doAdd. Помните,
именно такое имя имеет submit-кнопка в форме? Когда ее нажимают, сценарию по-
ступает пара "doAdd=Добавить!", чем мы и воспользовались. Итак, если кнопка
нажата, то мы вставляем запись в начало массива $Book и сохраняем его на диске.
Обратите внимание, насколько проста операция добавления записи. Так получилось
вследствие того, что мы предусмотрительно дали полям формы с названием и тек-
стом имена, соответственно, New[name] и New[text], которые PHP преобразовал в
массив. Вообще говоря, придумывание таких имен для полей — задача как раз того
"третьего лица", о котором я говорил выше. Это — работа скорее программистская,
нежели дизайнерская (хотя, безусловно, от удачного планирования названий имен
полей зависит не так уж и мало).
Подчеркиваю, что в самом коде генератора данных gbook.php в принципе не при-
сутствует никаких данных о внешнем виде нашей гостевой книги. В нем нет ни
одной
строчки на HTML. Иными словами, генератору совершенно "все равно", как выгля-
дит книга. Он занимается лишь ее загрузкой и обработкой. Это значит, что в
будущем
для изменения внешнего вида гостевой книги нам не придется править этот код, т.
е.
мы добились некоторого желаемого разделения труда дизайнера и программиста.
С другой стороны, шаблон gbook.htm не делает никаких предположений о том, как
же именно хранится книга на диске и как она обрабатывается. Его дело —
"красиво"
вывести содержимое массива $Book, "и точка". К тому же он почти не содержит ко-
да на PHP (разве что самый минимум, без которого никак не обойтись). А значит,
дизайнеру будет легко изменять внешний вид книги.
Недостатки
У любой медали есть оборотная сторона и, как часто бывает, от ее качества
зависит
довольно много. Имеется она и у двухуровневой схемы построения сценариев.
Давай-
те систематизируем все недостатки и постепенно будем их исправлять.
Часть V. Приемы программирования на PHP 422
1. Что такое для пользователя "гостевая книга"? Конечно же, это прежде всего
страница. А для разработчика сценария? Разумеется, программный код. Получа-
ется, что взгляды пользователя несколько отличаются от воззрений разработчика.
Как разрешить сформулированную неувязку? Для этого нужно посмотреть на на-
шу систему "генератор данных — шаблон" со стороны. Что мы видим? Генератор
данных загружает данные с диска, а затем обращается к шаблону, чтобы тот их
вывел. Но пользователь хочет иметь перед глазами прежде всего шаблон, а не ра-
боту генератора! Мы же заставляем его запускать программу. Возможно, следую-
щее положение и покажется спорным, но на практике оно полностью оправдывает
себя. А именно, предлагается поменять направление обмена данными между шаб-
лоном и генератором данных. Пусть шаблон запрашивает данные у генератора, а
тот их ему предоставляет. Согласитесь, это укладывается даже в замечательно
зарекомендовавшую себя модель обмена "клиент-
сервер": шаблон — это клиент, а генератор данных — сервер.
2. Хотя шаблон двухуровневой схемы и является подчиненным элементом, он все же
вынужден ссылаться на имя генератора данных через атрибут action тэга
. Конечно, это вносит лишь дополнительную неразбериху и является еще
одним стимулом к замене понятий "главный" и "подчиненный".
3. Генератор данных состоит из излишне большого числа логических блоков, свя-
занных лишь односторонне. В самом деле, если мы будем писать систему админи-
стрирования для нашей гостевой книги, нам опять понадобятся функции загрузки
и сохранения данных (то есть, функции LoadBook() и SaveBook()). Поэтому ло-
гично будет выделить их в отдельный файл, который я здесь буду называть ядром
сценария. Ядро — это третий компонент в трехуровневой схеме построения про-
граммы, о которой мы сейчас будем говорить. Разумеется, в сложных системах
ядро может состоять из десятков (и даже сотен) файлов. Вообще говоря, оно также
содержит и сведения о конфигурации (константу GBook), так что часто бывает
удобно выделить эти данные в отдельный файл.
4. Шаблон страницы вмещает в себя весь ее HTML-код. В то же время, в современ-
ном мире подавляющее большинство сайтов организовано так, что их страницы
построены по одной и той же "модели" (например, карта раздела слева, текст
справа, баннер вверху, дополнительная информация cнизу и т. д.). Согласитесь,
что копировать один и тот же шаблон в сотни мест просто неприемлемо для по-
следующего редизайна (который, скорее всего, последует практически сразу, по-
тому что при первой реализации довольно сложно бывает сразу учесть все поже-
лания заказчика). Конечно, мы можем вставить в нужные места шаблона вызовы
инструкции include, загружающей соответствующие блоки страниц. Однако при
детальном рассмотрении оказывается, что это всего лишь некоторая "отсрочка"
неизбежной проблемы редизайна. В самом деле, мы сможем легко менять внеш-
ний вид отдельных блоков, но у нас не получится переставлять их в другом по-
рядке (например, карта — справа, текст — слева) без утомительного изменения
HTML-кода всех страниц.
Глава 30. Код и шаблон страницы 423
r Пожалуй, пока достаточно. Сейчас мы попытаемся решить все эти проблемы,
кроме последней (традиционно являющейся для Web-студий настоящим ящиком
Пандоры), которой мы тоже вскоре займемся, что выльется, как вы увидите, в до-
вольно внушительный объем кода.
Трехуровневая схема
Итак, в отличие от двухуровневой, трехуровневая схема построения сценария
содер-
жит специально выделенный код, или ядро, которое совместно используют все
"гене-
раторы данных". Почему я заключил последний термин в кавычки? Да потому, что
теперь мы будем называть его по-другому, а именно, интерфейсным кодом (или про-
сто интерфейсом, хотя это, возможно, и не совсем корректно) сценария. Генератор
данных — по-прежнему сущность, являющаяся объединением ядра и интерфейса.
Кроме того, при использовании трехуровневой схемы пользователь никогда "не ви-
дит" генератор данных. Он всегда имеет дело только с шаблоном страницы, который
иногда выглядит, как программа. Это происходит при обращении к шаблону (а сле-
довательно, и к генератору данных) из формы в браузере.
Шаблон страницы
Теперь шаблон сам вызывает генератор, который предоставляет ему нужные данные,
а заодно и реагирует на запросы пользователя. Он выполняет это, например, при
по-
мощи все той же инструкции include:
Листинг 30.3. Шаблон: gbook.html
Гостевая книга
Добавьте свое сообщение:
Ваше имя:
. . .
Я не буду приводить текст страницы целиком, т. к. после определения формы он
идентичен листингу 30.1. Итак, мы помещаем инструкцию include самой первой
строчкой шаблона, и на это есть своя причина. Дело в том, что при различных,
ска-
жем так, "аварийных" событиях генератор данных может перенаправить браузер на
другой адрес, не вернув управление в шаблон. Конечно, если бы include размеща-
лась где-нибудь в середине шаблона, мы не смогли бы этого сделать, поскольку
часть
страницы могла быть уже отослана пользователю.
Часть V. Приемы программирования на PHP 424
К слову сказать, при использовании шаблонизатора, описанного ближе к концу
этой главы, мы преодолеваем и этот недостаток. А именно, имеется возмож-
ность вставлять вызов генератора данных в любое удобное место шаблона.
Заметьте, что шаблон имеет расширение HTML и выглядит для пользователя как
обычная HTML-страница. Пользователь может и не подозревать, что в действитель-
ности сценарий написан на PHP. Но, чтобы описанный механизм заработал, нам не-
обходимо связать расширение HTML с обработчиком PHP. Мы уже делали это в гла-
ве 29. Вот какую строчку нужно добавить в файл .htaccess, расположенный в
каталоге (или "надкаталоге") сценария:
AddHandler application/x-httpd-php .html
Мы должны использовать директиву AddHandler, а не AddType, на случай,
если для расширения HTML был ранее установлен другой обработчик. Им мо-
жет быть, например, SSI (Server-Side Includes — Включения на стороне серве-
ра) или даже PHP версии 3. В этом случае директива AddType "не срабатыва-
ет".
Пока применение include является для нас единственным средством обращения к
генератору данных. Я все время повторяю эту фразу — "обращение к генератору
дан-
ных". Вообще говоря, она не совсем верна. В действительности обращение из
шабло-
на происходит лишь к интерфейсу сценария, но не к его ядру. Ядро доступно для
шаблона лишь посредством общения с интерфейсом, и никак не иначе. В свою оче-
редь, ядро также не может "разговаривать" с шаблоном (во всяком случае, не
долж-
но).
Мы видим, что во всех операциях передачи данных неизменно используется "посред-
ник" — интерфейсная часть программы. Это открывает для нас интересные потенци-
альные возможности (которые на практике задействуются довольно редко). А имен-
но, ядро и шаблон могут в принципе "разговаривать на разных языках", тогда
интерфейс будет служить их "переводчиком". Если задуматься, то это и есть
главная
задача интерфейса.
Диаграммы двухуровневой
и трехуровневой моделей
Наверное, пришло время нарисовать схему взаимодействия частей программы при
использовании двухуровневой и трехуровневой модели построения, а также еще раз
подчеркнуть их различия. Стрелками (рис. 30.1 и 30.2) обозначены зависимости,
ко-
торые можно охарактеризовать словами как "предоставляет данные". Пунктирные
стрелки отмечают зависимости, реализуемые достаточно редко. На схемах это не
что
Глава 30. Код и шаблон страницы 425
иное, как переадресация на другую страницу, возможно, выполняемая генератором
данных.
Генератор
данных
Шаблон
страницы
Пользователь
Рис. 30.1. Двухуровневая схема
Мы видим, что в случае двухуровневой схемы связи между компонентами сценария
исключительно циклические (см. рис. 30.1). Каждая часть программы взаимодейст-
вует на равных с другой ее частью.
Легко заметить, что рис. 30.2 гораздо сложнее, чем рис. 30.1. Его
"загруженность"
объясняется тем, что трехуровневая схема более, чем это может показаться с
первого
взгляда, сложна и универсальна по сравнению с двухуровневой. Обратите внимание
на то, что практически все связи стали двусторонними, а циклические — исчезли.
Это
позволяет работать блоком более независимо, чем для случая двухуровневой модели.
А значит, работу над сценарием можно распределить по нескольким исполнителям
более эффективно, — к чему мы и стремились.
Единственный блок программы, который не связан с другими двусторонними
связями, — файл конфигурации системы. Это неудивительно, ведь конфигу-
рация содержит лишь набор определений констант и переменных, которыми
пользуются все остальные блоки схемы. Впрочем, стрелка, ведущая из блока
конфигурации в шаблон страницы, хотя и может существовать без особых по-
следствий, все-таки иногда выглядит несколько нелогично. Рекомендуется так
строить сценарии, чтобы шаблону не требовалась информация о конфигура-
ции. Он должен обращаться только к данным, сгенерированным интерфейсом.
Часть V. Приемы программирования на PHP 426
Генератор данных
Шаблон
страницы
Пользователь
Интерфейсная
часть
программы
Ядро
Конфигу-
рация
Рис. 30.2. Трехуровневая схема
Интерфейс
Как можно заметить из листинга 30.4, интерфейс сценария гостевой книги стал
гораздо
проще, чем это было с генератором данных из листинга 30.2. Файл, в котором
содержится
его код, называется точно так же, как и файл генератора. Это и не удивительно:
"снаружи"
интерфейс выглядит как полноценный генератор данных, а о существовании ядра
шаблон
даже и не "подозревает".
Листинг 30.4. Интерфейс: gbook.php
include "kernel.php"; // Загружаем ядро.
$Book=LoadBook(GBook); // Загрузка гостевой книги.
// Обработка формы, если сценарий запущен через нее.
if(!empty($doAdd)) {
// Добавить в книгу запись пользователя.
$Book=array(time()=>$New)+$Book;
// Записать книгу на диск.
SaveBook(GBook,$Book);
}
// Загрузка шаблона не нужна — теперь, наоборот, шаблон
// вызывает интерфейс.
Глава 30. Код и шаблон страницы 427
?>
Как видим, интерфейс занимается только той работой, для которой он и предназна-
чен: выступает "посредником" между ядром и шаблоном. Самым первым загружается
ядро — файл kernel.php (я люблю так его называть). Дальше осуществляется ис-
ключительно обработка и "расшифровка" входных данных и формирование выход-
ных.
Ядро
Ядро — это самая ответственная, но, на мой взгляд, в то же время и самая
скучная
часть работы программиста. Действительно, оно напрямую не взаимодействует с
шаблоном страницы, а значит, не имеет права "общаться" с пользователем.
Ядро в идеале должно содержать лишь набор функций, которые позволяют исчерпы-
вающим образом работать с объектом программы. В этом смысле идеально его объ-
ектно-ориентированное построение. Об объектно-ориентированном программирова-
нии на PHP будет вкратце рассказано в главе 31, а пока не будем усложнять и без
того
"скользкую" задачу и посмотрим, что представляет собой ядро нашей гостевой
книги
(листинг 30.5).
Листинг 30.5. Ядро: kernel.php
// Загружаем конфигурацию.
include "config.php";
// Загружает гостевую книгу с диска. Возвращает содержимое книги.
function LoadBook($fname)
{ $f=@fopen("gbook.dat","rb");
if(!$f) return array();
$Book=Unserialize(fread($f,100000));
fclose($f);
return $Book;
}
// Сохраняет данные книги на диске.
function SaveBook($fname,$Book)
{ $f=fopen("gbook.dat","wb");
fwrite($f,Serialize($Book));
fclose($f);
}
?>
Часть V. Приемы программирования на PHP 428
Действительно, здесь нет ничего, кроме определений функций и… еще одной инст-
рукции include (вздохните с облегчением — на этот раз последней). Она добавляет
конфигурационные данные нашей книги — всего лишь одну-единственную константу
GBook, определяющую имя файла, в котором гоствевая книга и будет храниться.
"Для порядка" приведу и его (листинг 30.6).
Листинг 30.6. Конфигурация: config.php
define("GBook","gbook.dat"); // имя файла с данными книги
?>
Что же у нас получилось в результате? Мы "растянули" простой сценарий на
целых 5 файлов (если считать еще и .htaccess, то на 6). Что ж, если вы так
думаете, я с вами соглашусь. Тут все дело в том, что для простых сценариев
(а именно такой мы и рассматривали) трехуровневая схема построения оказы-
вается чересчур уж "тяжеловесной". Про такую ситуацию в народе говорят: "из
пушки по воробьям". Что же касается сложных систем, не следует забывать,
что "единственность" ядра может сэкономить нам количество файлов, если у
комплекса много различных интерфейсов (например, разветвленная система
администрирования), не говоря уже о простоте отладки и поддержки. Кроме то-
го, можно полностью разделить работу по написанию ядра и интерфейса меж-
ду несколькими людьми.
Проверка корректности входных данных
До сих пор мы не заботились о том, корректные ли данные заносит посетитель. В
на-
шей ситуации это и не нужно: в книгу кто угодно может добавлять любую информа-
цию. В то же время в реальной жизни, конечно, приходится проверять правильность
введенных пользователем данных.
Например, мы можем ввести в нашу гостевую книгу цензуру, которая будет запре-
щать пользователям употреблять в сообщениях ненормативную лексику. Конечно,
при вводе недопустимого текста он не должен добавиться в гостевую книгу; вместо
этого в браузер пользователя хотелось бы вывести предупреждение. Но как
осущест-
вить желаемую модерацию в соответствии с трехуровневой схемой? И какая часть
программы должна за это отвечать?
На второй вопрос ответить довольно просто. Так как ядро не в состоянии
"общаться"
с шаблоном напрямую, а шаблон не может исполнять сложный код, остается единст-
венный вариант — интерфейс. А что касается того, как выводить сообщение об
ошибке, — вопрос довольно спорный. Мы рассмотрим лишь самое простое решение.
Глава 30. Код и шаблон страницы 429
Интерфейс должен сгенерировать сообщение и передать его шаблону. Последний,
"заметив" сообщение, может вывести текст контрастными буквами, например, вверху
страницы. С этим никаких проблем быть не должно. Пусть интерфейс в случае ошиб-
ки создает переменную $Error и присваивает ей текст ошибки. Вот как может вы-
глядеть шаблон:
. . .
Произошла ошибка: =$Error?>
}?>
. . .
Такой подход, хоть и прост, оказывается немного неудобным для пользовате-
ля. Действительно, ему сообщают, что произошла ошибка, но не говорят, на-
пример, какое именно поле формы он заполнил неправильно. Пользователь
желает, чтобы сообщения об ошибках появлялись напротив неверно введен-
ных данных. К сожалению, без дополнительного программирования в шаблоне
на PHP этого добиться довольно сложно (если вообще возможно). Единствен-
ный имеющийся выход — использовать шаблонизатор и написать для него
фильтр (функцию, занимающуюся финальной обработкой блока страницы
перед ее отправкой), которая будет в автоматическом режиме рядом со всеми
тэгами формы проставлять возможные сообщения об ошибках (а заодно и ат-
рибуты value в самих тэгах, чтобы поля формы сохраняли свои значения ме-
жду вызовами сценария). Эта задача, пожалуй, потребует всей информации о
PHP, заложенной в этой книге, и еще, вероятно, хорошего знания регулярных
выражений Perl. Код, полностью решающий проблему, слишком объемен, что-
бы уместиться на страницах данной книги.
Шаблонизатор
Вот мы и добрались до смысла "этого сладкого слова" — шаблонизатора, которое я
применяю то тут, то там по всему тексту. Возможно, чуть разобравшись в
прилагае-
мом исходном коде, а затем и опробовав программу на практике (наверное, перепи-
сав на свой лад), вы разделите мое убеждение о том, что хороший шаблонизатор
мо-
жет сэкономить студии немало лишних часов работы.
Выше я описал недостатки двухуровневой схемы и показал, как их можно преодолеть
при помощи трехуровневой модели построения сложных сценариев. Но, если вы пом-
ните, одна задача так и осталась нерешенной.
А именно, мы обратили внимание, что даже при использовании трехуровневой схемы
мы не можем легко менять внешний вид многих страниц сразу — без утомительного
изменения шаблонов каждой из них. Если вы не забыли, решение с включаемыми
файлами (в каждом из которых содержится отдельный, общий для всех сценариев
блок страницы) также нам не подходит, потому что оно лишь слегка меняет поста-
Часть V. Приемы программирования на PHP 430
новку проблемы редизайна. Даже используя инструкцию include, мы попадем в
тупик, если захотим изменить положения некоторых блоков на странице.
В общем, при всех достоинствах трехуровневой модели построения сценария ее
необ-
ходимо несколько видоизменить, чтобы добиться хотя бы минимальных удобств. Это
"видоизменение" я и называю шаблонизатором.
Термин "шаблонизатор" произошел от слова "шаблон" и не является стандарт-
ным для технической литературы. В этой книге я применяю его на свой страх и
риск и в основном из соображений краткости: писать везде (а вам — читать)
слова "система управления шаблонами" весьма утомительно.
Сама идея шаблонизатора не является новой в Web-программировании. Скорее даже
наоборот: существуют десятки систем, построенных по описанным ниже принципам.
Большинство из них — коммерческие и часто довольно сложны. В то же время мно-
гие свободно распространяемые системы (во всяком случае, те, с которыми я зна-
ком, — например, Mason, лебедевский Parser и др.) отличаются одним недостатком:
синтаксис их языка излишне сложен, а потому отпугивает. Кроме того, часто для
ос-
воения этих шаблонизаторов требуются навыки не только дизайнера или HTML-
верстальщика, но и программиста. Мы же, напомню в очередной раз, стремимся к
тому, чтобы распределить разработку сценария по возможно большему числу незави-
симых людей, многие из которых не знакомы с программированием.
Высказанные только что суждения являются моей личной позицией в вопросе
шаблонизаторов, а потому, как и все субъективное, они вполне могут несколь-
ко отличаться от действительности. Читателю предлагается самому их прове-
рить после того, как он ознакомится с моделью шаблонизатора, предлагаемого
в этой главе. Нужно заметить, что, конечно, каждая Web-студия считает свою
собственную версию шаблонизатора самой лучшей в мире.
Традиционное построение страниц
Итак, сосредоточим все свое внимание на том, как желательно строить сценарии,
чтобы максимально упростить проблему редизайна, а вместе с ней — добавление
новых страниц в карту сервера. Многие программисты ограничиваются тем, что раз-
бивают свои страницы на 3 логических блока: верхнюю часть (header), центральную
часть (text) и нижний участок страницы (footer). Каждая из этих составляющих
хра-
нится в отдельном файле. Центральный блок (text) является главным: до начала
рабо-
ты он загружает из файла общую для всех страниц верхнюю часть, а в конце
выводит
Глава 30. Код и шаблон страницы 431
нижнюю. Вот как примерно выглядит шаблон страницы при такой структуре сцена-
рия (листинг 30.7):
Листинг 30.7. Традиционное построение шаблона
Здесь идет главный текст страницы,
возможно, включающий данные,
сгенерированные интерфейсом Interface.php
Предполагается, что файлы header.htm и footer.htm хранятся в подкаталоге
/templ корневого каталога сервера и содержат участки страниц, которые должны
быть выведены до и после основного текста страницы. Если сайт небольшой и в нем
используется не так уж много различных шаблонов страниц, данное решение являет-
ся самым простым. В таких ситуациях его применение оправдано. Но нас интересует
оформление больших и сложных сайтов. Предположим, наш ресурс содержит сотни
страниц, построенных по описанной схеме. Давайте взглянем на проблему с этой
по-
зиции.
Сложность перестановки блоков
Первый недостаток увидеть легко: мы не можем ни добавить новый блок в страницу,
ни изменить положения уже имеющихся. Если мы попытаемся это сделать, потребу-
ется менять код сотен страниц сайта.
Необходимо заметить, что в нашем примере вряд ли придется когда-нибудь
изменять порядок следования блоков, раз мы договорились рассматривать
проблему с общих позиций, а не с частных.
"Расщепление" шаблона
Второй недостаток более очевиден для дизайнера: файлы header.htm и
footer.htm, хотя и представляют собой логически один шаблон, все же разделены.
Все мы привыкли к тому, что многие тэги HTML (такие как , и т. д.
)
имеют парные закрывающие, причем расположенные в том же самом файле. Но в
ситуации с разделением шаблона на footer и header мы, наоборот, должны хранить
большинство открывающих тэгов в одном файле, а закрывающие — в другом. В лис-
тинге 30.8 приведен пример верхней части шаблона страницы.
Часть V. Приемы программирования на PHP 432
Листинг 30.8. Файл header.htm
Добрый день.
Карта раздела: . . .
Видите, файл оборвался на открывающем тэге. Теперь взглянем на листинг 30.9:
Листинг 30.9. Файл footer.htm
Он состоит исключительно из одних закрывающих тэгов. Потенциально, добавив в
header.htm новый открывающий тэг, мы можем забыть закрыть его в footer.htm.
Кроме того, такая конструкция несколько противоречит логике: две похожих по
смыслу части шаблона содержатся в разных файлах. Мы уже обсуждали это выше и
пришли к выводу, что данное построение оказывается довольно неудобным.
Сложность смены шаблона у части страниц
Еще один недостаток описанной схемы следует из предыдущего. Каждая страница
должна "знать", где расположены файлы header.htm и footer.htm. Пусть у нас на
сайте есть каталог, в котором "лежат" сотни файлов. Во время разработки
оказалось,
что шаблон для всех файлов в этом каталоге должен отличаться от шаблона всех
ос-
тальных страниц (которых также немало). Значит, требуется создать еще одну пару
header- и footer-файлов, назвав их, например, header1.htm и footer1.htm. Это в
общем-то не представляет особой проблемы, сложность в другом: придется заменять
ссылки во всех файлах каталога. Можно, конечно, сделать это посредством
глобаль-
ных поиска и замены при помощи какого-нибудь текстового редактора (например,
HomeSite фирмы Allaire), но, согласитесь, это решение выглядит как явно
"лобовое".
Кроме того, если мы имеем доступ к сайту только с использованием FTP, нам при-
дется "скачивать" все страницы, редактировать их, а затем опять копировать на
сер-
вер. Естественно, для крупных информационных сайтов такие "накладные расходы"
просто неприемлемы.
Возможно единственное решение этой проблемы — заставить страницы "наследо-
вать" ссылку на шаблон каталога (или его родительского каталога), в котором они
Глава 30. Код и шаблон страницы 433
находятся. Таким образом, поправив эту ссылку в информации о каталоге, мы авто-
матически изменим шаблон и у всех страниц в нем.
Для сравнительно небольших систем все же существует путь, обходящий, хоть
и не очень удачно, рассматриваемую проблему. А именно, можно для каждого
раздела сайта использовать отдельную пару header- и footer-файлов. В дейст-
вительности же эти файлы будут представлять собой лишь символические
ссылки на "настоящие" шаблоны. Правда, эта схема работает лишь в системах
Unix. Кроме того, она нисколько не упрощает ситуацию, когда разработчики
решили перенести часть страниц из одного раздела в другой (сменив при этом
их шаблоны).
Что такое шаблонизатор?
Итак, мы вновь столкнулись с множеством трудноразрешимых накладок (возможно,
выглядящих для многих с первого взгляда как надуманные). Когда же они закончат-
ся, спросите вы? Отвечаю: прямо сейчас.
Давайте взглянем "в корень" описанных выше сложностей. Почему они вообще воз-
никают в этой задаче? Нетрудно догадаться: опять же из-за излишних зависимостей
данных. Помните, эти зависимости привели нас в свое время к необходимости пере-
хода от двухуровневой схемы построения сценариев к трехуровневой? Теперь они
подводят нас к идее шаблонизатора.
Вспомним, что мы сделали тогда, чтобы убрать зависимости. Мы поменяли местами
"поставщика" и "исполнителя". Идея выполнить обратную перестановку кажется аб-
сурдной, т. к. мы опять придем к тому, с чего начали. Конечно, мы не будем так
де-
лать. Зададимся отвлеченным вопросом: что предпринимает общество, когда перед
ним возникает чересчур большое количество нерешенных и необъяснимых задач?
Оно придумывает себе богов. В программировании — то же самое. Раз мы не можем
больше сослаться ни на генератор данных, ни на шаблон, значит, настало время
реа-
лизовать нечто третье, "бога", управляющего всей системой в совокупности и
распре-
деляющего обязанности. Вы, наверное, догадались, что я снова имею в виду шабло-
низатор.
Итак, шаблонизатор — это программный код, держащий "под контролем" все файлы
на нашем сайте. Ни одно обращение к странице, ни один запуск сценария не может
пройти без его непосредственного участия. В то же время шаблонизатор
"маскирует"
себя, создавая у пользователя впечатление, будто бы его и нет. Этим он похож на
ге-
нератор данных в трехуровневой модели построения сценариев.
Часть V. Приемы программирования на PHP 434
Теперь вы почувствовали, почему я применил здесь аналогию с богом? Ведь
бог как раз удовлетворяет тем описаниям, которые даны в предыдущем абза-
це!
Впрочем, идеология "вездесущего" кода не является для нас новой: нечто похожее
мы
уже рассматривали в главе 29, правда, с целью гарантированного подключения биб-
лиотекаря ко всем сценариям сайта. В рамках реализуемой "религии" мы применим
точно такой же подход, только вместо библиотекаря будет подключаться и
запускать-
ся шаблонизатор.
Описание шаблонизатора
Сформулируем, что должен уметь делать наш будущий шаблонизатор. Конечно, все,
что мы реализуем, будет лишь самым основным, что мы хотели бы получить от этой
системы. В то же время в описанную концепцию чрезвычайно легко добавлять новые
возможности (так уж она разрабатывалась). Для этого практически не придется
пере-
писывать имеющийся код, останется лишь вставить то, что нам нужно.
Вставка страниц в единый шаблон
Раньше главный текстовый блок страницы (text) запрашивал подключения к себе
двух частей шаблона — footer и header. Но, раз мы в очередной раз поменяли
места-
ми "поставщика" данных и "исполнителя", посмотрим, нельзя ли пойти дальше. Да-
вайте поиграем в такую словесную игру: "обработаем" первое предложение этого
аб-
заца, переставив в нем понятия, соответствующие "исполнителю" и "поставщику".
Получим буквально: шаблон запрашивает подключение к себе главного текстового
блока страницы. Эврика, это и есть главная задача шаблонизатора!
Не хотите ли взглянуть с этой новой позиции на шаблон страницы? Тогда изучите
листинг 30.10.
Листинг 30.10. Свежий взгляд на шаблон страницы: /templ/main.tmpl
=Blk("Title"title>
Добрый день.
Карта раздела: . . .
=Blk("Text")?>
Глава 30. Код и шаблон страницы 435
Не обращайте пока внимания на команду . Ее смысл
поясняется немного ниже.
Мы видим, что ненужное и опасное "расщепление" шаблона на два файла ушло в
прошлое, а мы опять вернулись к простой модели. Будем хранить этот шаблон в
фай-
ле /templ/main.tmpl.
Но позвольте, откуда же возьмется блок с именем Text, который выводится в сере-
дине этого шаблона? Вот задачу по его получению и возьмет на себя шаблонизатор.
Предположим, пользователь обратился по адресу /news/weekly/today.html. Шаб-
лонизатор, как я уже упоминал, "перехватит" это обращение и "возьмет" текстовый
блок из файла today.html, расположенного в каталоге /news/weekly. Затем он
передаст управление шаблону, который вставит этот текст в нужное место страницы
и
отправит последнюю браузеру.
Множественность блоков
Шаблонизатор, который вставляет все содержимое запрошенного файла в фиксиро-
ванный шаблон, совершенно бесполезен в реальной практике. Это означает, что мы
должны "научить" его:
r определять имя шаблона индивидуально для каждой страницы;
r позволять хранить в документе несколько блоков информации, а не только глав-
ный текст файла.
Последняя задача более важна, так что начнем с нее. Мы привыкли к тому, что
любая
страница сценария выполняется последовательно и представляет собой единый
HTML-документ. Теперь нам придется отказаться от этого стереотипа (в общем-то,
ведущего в тупик). Итак, любая страница сайта — это всего лишь набор блоков,
ко-
торые будут вставлены в шаблон.
Блок — участок текста, имеющий имя (не обязательно уникальное), посредством ко-
торого можно ссылаться на этот текст. Мы уже видели, как это происходит в про-
стейшем случае (см. листинг 30.10). Функция шаблонизатора Blk() возвращает
текст (или содержимое, или тело) блока, имя которого указанно в ее параметрах.
Содержимое блока может быть задано многократно, при этом последующее опреде-
ление "затирает" текст предыдущего. Чуть ниже мы увидим, насколько данное каче-
ство оказывается полезным на практике.
Как же определять новые блоки в файле страницы? Для этого существует конструк-
ция . Пример ее использования приведен в листинге 30.11.
Листинг 30.11. Файл данных страницы: /phil/index.html
Часть V. Приемы программирования на PHP 436
Конфликт индуцирует смысл жизни. Объект деятельности, пренебрегая
деталями, методологически рефлектирует себя через постсовременный
класс эквивалентности, открывая новые горизонты. Закон внешнего мира
может быть получен из опыта.
Философия, конечно, порождена временем. Информация, как следует из
вышесказанного, непредвзято подчеркивает принцип восприятия, отрицая
очевидное.
Из листинга 30.11 следует, что мы можем задавать содержимое блока двумя разными
способами. Самый простой — указать текст непосредственно вторым параметром
функции Block(), как это сделано для блока Title. Второй способ незаменим для
блоков, тела которых состоят из большого количества строк. А именно, мы можем
опустить второй параметр функции Block(), в этом случае весь текст, который
рас-
положен до начала следующего блока либо до конца файла, будет восприниматься
как тело. Я буду называть такие блоки многострочными. Особенностью многостроч-
ных блоков в том шаблонизаторе, который мы с вами сейчас напишем, является то,
что из их содержимого удаляются начальные и концевые пробельные символы, в том
числе символы перевода строки. В результате та пустая строка, которая
присутствует
в листинге, не попадет в шаблон — она будет удалена.
Текст, не принадлежащий ни одному из блоков, игнорируется. Например, мы
могли бы написать какие-нибудь комментарии сразу после первой строки лис-
тинга 30.11, и они были бы пропущены.
Наверное, вы уже догадались, как мы будем задавать имя шаблона для той или иной
страницы. Название шаблона — не что иное, как содержимое блока Template, кото-
рый воспринимается шаблонизатором как специальный. Но, конечно, мы не собира-
емся определять этот блок в каждой странице — иначе чем этот способ лучше ис-
пользования участков header и footer? Посмотрим, что предлагает нам
шаблонизатор.
Наследование блоков
Наверное, вы думаете, что страница /phil/index.html, которая генерируется лис-
тингом 30.11, состоит только из трех блоков — Title, Text и Cite. Это не так.
Страница, без сомнения, включает перечисленные блоки, но она также состоит и из
всех блоков, которые заданы для каталогов /phil и /. Каталоги ведь ничем не
хуже
файлов. Соответственно, каждый каталог также может иметь собственный набор бло-
ков, которые будут унаследованы всеми файлами в нем, а также файлами его подка-
талогов.
Глава 30. Код и шаблон страницы 437
Предположим, что для каталога /phil определяется блок Title, содержащий, ска-
жем, строку Weekly. В то же время файл index.html также определяет блок Title.
Что произойдет в этом случае? А произойдет следующее: в шаблоне будет доступно
только тело последнего блока. Иными словами, тот блок, который определяется в
файле, заменит собой свое старое значение из каталога.
Нетрудно теперь догадаться, как происходит процесс сбора блоков для конкретной
запрошенной страницы. Вначале шаблонизатор получает все блоки корневого катало-
га, затем обрабатывает блоки подкаталога, причем уже имеющиеся одноименные
блоки перезаписываются. Описанный процесс продолжается до тех пор, пока не
будет
достигнут файл, который запрошен пользователем. Такая организация шаблонизато-
ра позволяет нам задавать для всех блоков значения по умолчанию. Эти значения
будут использованы шаблоном в случае, если те или иные блоки не
"переопределяют-
ся" в файле страницы. Чтобы задать тело по умолчанию для блока, достаточно
доба-
вить его к блокам корневого каталога сайта.
Мы знаем, что блоки файла хранятся в самом этом файле. Где же находятся блоки
каталога? Конечно, в специальном файле, расположенном в этом каталоге.
Хранить блоки каталогов в отдельном централизованном хранилище (наподо-
бие Реестра Windows) было бы большим просчетом. Этим мы перечеркнули
бы принцип минимизации зависимостей данных, о котором так много сказано в
этой главе.
Я предлагаю использовать в качестве такого файла .htaccess. Чтобы Apache не
"ругался" на не непонятные ему директивы , мы снабдим их симво-
лами комментария # в начале строки. Конечно, таким способом мы не сможем зада-
вать многострочные блоки для каталогов, но, как показывает практика, в
большинст-
ве случаев это и не нужно. В листинге 30.12 показан пример файла .htaccess,
расположенного в корневом каталоге сервера и задающего значения блоков по умол-
чанию.
Листинг 30.12. Блоки для корневого каталога: /.htaccess
#
#
#
#
# Связываем имя обработчика с конкретным файлом.
Action templhandler "/php/TemplateHandler.php?"
# Документы этого типа мы желаем "пропускать" через наш обработчик.
AddHandler templhandler .html .htm
Часть V. Приемы программирования на PHP 438
Обратите внимание на то, что в приведенном файле конфигурации задаются также и
некоторые директивы Apache, которые заставляют сервер запускать программу шаб-
лонизатора каждый раз, когда пользователь обращается к HTML-документу. Мы уже
знакомы с этими директивами: в главе 29 они использовались для того, чтобы
обес-
печить подключение библиотекаря к каждому сценарию сервера.
Наверное, вы уже заметили, что блочный файл, который обрабатывается шаб-
лонизатором, представляет собой ни что иное, как код на PHP с вызовами
управляющих функций типа Block(). Этим мы достигаем множества преиму-
ществ, самое главное из которых — значительное ускорение работы шаблони-
затора по сравнению со способом "ручного" разбора файлов. Кроме того, от-
ладочные качества сценария при таком подходе ничего не теряют: файлы
блоков загружаются с помощью include, а значит, случись там ошибка, PHP
исправно покажет имя файла и номер строки, где это произошло. Правда,
остается единственный недостаток: несколько некрасивый синтаксис
определения блоков, естественный лишь для программиста, но не для
дизайнера. Что же, всегда приходится идти на какие-то жертвы…
Внимательно взгляните на определение блока Template. Как уже упоминалось, этот
блок содержит имя шаблона, который будет задействоваться при отображении стра-
ницы. То, что блоки из родительских каталогов наследуются файлами, позволяет
нам
задать Template в одном-единственном месте, автоматически распространив его
действие на все файлы в каталоге. Не правда ли, это как раз то, чего мы так
долго
добивались?
Шаблонизатор также обрабатывает специальным образом еще один блок. Его назва-
ние — Output. Тело именно этого блока выводится в браузер пользователя, когда
вся
страница уже обработана. Обычно блок Output вставляют только в шаблон страни-
цы, потому что использование его в любом другом месте оказывается бессмыслен-
ным (все равно он переопределится в шаблоне).
Автоматическая генерация названий
Если пользователь находится на сайте "Книжный магазин" в разделе "Философия" на
заинтересовавшей его странице "Современность", то, конечно, в заголовке окна
брау-
зера ему бы хотелось видеть что-то вроде "Книжный магазин | Философия | Совре-
менность", а не просто "Современность". Мы уже договорились хранить название
страницы в блоке Title. Но, конечно, мы бы не хотели записывать в каждой
страни-
це название полностью, потому что:
r в будущем мы можем перенести страницу в другой раздел;
r мы, возможно, захотим сменить разделитель | на /;
r это нарушает концепцию минимальной избыточности данных, что, как мы уже
неоднократно убеждались, не приводит ни к чему хорошему.
Глава 30. Код и шаблон страницы 439
Специально для решения такого рода задач в нашем шаблонизаторе предусмотрим
механизм, который я далее буду называть автоматической склейкой тел блоков. Вот
как он работает. Если при обработке очередного блока шаблонизатор видит, что
его
тело начинается с подстроки [Клей], он определяет, что текст должен быть
"присты-
кован" к предыдущему телу одноименного блока, но не должен заменить его. В
каче-
стве "строки-клея" выступает значение блока с именем Клей. Это позволяет нам в
будущем изменить символ "склейки" лишь в одном месте, чтобы это затронуло весь
сайт. В случае, если указана пустая пара квадратных скобок [] (то есть имя
блока
было опущено), используется тело блока с именем DefaultGlue (см. листинг 30.12),
а если и он отсутствует — то |.
Теперь при загрузке страницы /phil/index.html из листинга 30.11, пользователь
увидит ее полное название, составленное из блоков Title всех "надкаталогов" и
са-
мого файла страницы. Мы добиваемся этого, определив блок Title следующим об-
разом:
Поддержка механизма поиска
включаемых файлов
В шаблонизаторе есть одна полезная функция. Называется она Load() и занимается
тем, что загружает указанный в параметрах файл, который как предполагается,
также
имеет блочную структуру. Имя этого файла можно задавать относительно текущего
каталога (в котором расположен код, вызвавший Load()), либо же в абсолютном
формате (относительно корневого каталога сервера).
С помощью данной функции мы можем разбивать сложные шаблоны на части. На-
пример, так можно было бы поступить с блоком, занимающимся формированием
карты текущего раздела, особенно если существует несколько шаблонов, отображаю-
щих эту карту. Функцию Load() можно вызывать в любом месте страницы или даже
из файла .htaccess. Блоки, генерируемые ей, будут вставлены непосредственно пе-
ред тем блоком, в котором она была вызвана.
На примере использования библиотекаря мы уже убедились, насколько утомитель-
ным может быть указание абсолютных путей к файлам. Поэтому функция Load()
умеет сама искать включаемые файлы по серверу. Она делает это всякий раз, когда
ей задан относительный путь к файлу. Поиск ведется на основе списка так
называе-
мых каталогов для поиска шаблонизатора. Этот список можно пополнять с помощью
вызова Inc(), как это сделано, например, в листинге 30.12. Функция Inc()
доволь-
но интеллектуальна: даже если ей передан относительный путь к каталогу, она
пере-
водит его в абсолютный. Так что при использовании Load() из файла, расположен-
ного в другом каталоге, не происходит никаких недоразумений.
Часть V. Приемы программирования на PHP 440
Фильтры блоков
После того, как тело блока вычислено, шаблонизатор производит его
дополнительную
обработку. Делается это с помощью специальных функций-фильтров. Например,
"склеивание" названий осуществляется именно таким фильтром. Система устроена
таким образом, что позволяет добавлять и удалять фильтры прямо в процессе
работы.
Для этого программисту достаточно лишь написать код функции-фильтра, а затем
добавить имя этой функции в специальную таблицу фильтров (см. исходный код
шаблонизатора).
В той версии шаблонизатора, которую мы сейчас рассматриваем, имеется и еще один
"стандартный" фильтр. Его задача — удалить из тел блоков все начальные символы
табуляции, сколько бы их ни было. Это позволяет программистам и дизайнерам сво-
бодно делать отступы в HTML-коде документов, не думая о том, что символы
табуля-
ции будут увидены пользователем при просмотре кода страницы. Впрочем, возмож-
но, это и излишество (в конце концов, кому какое дело, как выглядит исходный
код
страницы).
Ради интереса вы можете написать фильтр, который превращает все символы
перевода строки в пробелы. Таким образом, исходный код страницы, которую
получит пользователь, будет представлять собой одну длинную строку. Код
этого фильтра занимает буквально одну строку на PHP!
Поддержка трехуровневой схемы
разработки сценариев
Несомненно, наш шаблонизатор будет поддерживать трехуровневую схему разработ-
ки сценариев. Иначе и быть не могло: мы не должны удалять из системы то, что
пре-
красно работает. Наверное, вы уже заметили, что в телах блоков мы можем
свободно
применять операторы PHP, а это требование является главным для любой схемы.
Чтобы не "засорять" каталоги сайта сценариями — интерфейсами и генераторами
данных — предлагается разместить все, что не относится к HTML-файлам и блокам,
в отдельном (недоступном извне) каталоге. Им может быть, например, тот самый
каталог, где располагаются различные модули. Ведь что такое ядро сценария, как
не
обычная библиотека, предоставляющая функции для всеобщего использования?! Взя-
тие на вооружение такой техники также снимает с нас заботу об указании полного
пути к файлам ядра, поскольку они находятся в общедоступном каталоге модулей, а
значит, могут быть включены при помощи Uses().
С загрузкой интерфейсов посредством Uses() все обстоит несколько сложнее.
Вполне может возникнуть ситуация, когда один и тот же интерфейс требуется в
разных местах шаблона страницы для выполнения различных действий. Функ-
Глава 30. Код и шаблон страницы 441
ция же Uses() всегда загружает файл лишь однажды, следя за тем, чтобы в
следующий раз ее вызов был просто проигнорирован. Так что она нам не со-
всем подходит. В качестве альтернативы предлагается добавить в код библио-
текаря еще одну функцию (назвав ее, например, UsesMulti()), которая могла
бы загружать указанный файл несколько раз. Единственное отличие ее кода от
кода Uses() состоит в том, что она использует инструкцию include, а не
include_once. Написание этой функции предоставляю читателю.
Вот и подошло к концу описание нашего шаблонизатора. Надеюсь, я ничего не упус-
тил. Впрочем, если вдруг в приведенном ниже коде вы обнаружите еще какую-нибудь
возможность, которую я здесь забыл описать, ничего страшного, наверное, не
случит-
ся….
Обработчик Apache для шаблонизатора
Так как шаблонизатор должен запускаться при обращении к любой странице на сер-
вере, для него придется написать обработчик. Я привожу здесь его код без
дополни-
тельных пояснений, поскольку он практически полностью аналогичен тому коду, ко-
торый мы рассматривали в главе 29.
Листинг 30.13. Обработчик шаблонизатора: TemplateHandler.php
// Проверяем, не пытается ли пользователь запустить обработчик
// напрямую, минуя Apache.
$FileName=strtr(__FILE__,"\\","/");
$ReqName=ereg_Replace("\\?.*","",strtr(getenv("REQUEST_URI"),"\\","/"));
if(eregi(quotemeta($ReqName),$FileName)) {
// Выводим сообщение об ошибке.
include "TemplateHandler.err";
// Записываем в журнал данные о пользователе.
$f=fopen("TemplateHandler.log","a+");
fputs($f,date("d.m.Y H:i.s")." $REMOTE_ADDR — Access denied\n");
fclose($f);
// Завершаем работу.
exit;
}
// Все в порядке — корректируем переменные окружения в соответствии
// с запрошенным пользователем адресом.
@putenv("REQUEST_URI=".
$GLOBALS["HTTP_ENV_VARS"]["REQUEST_URI"]=
Часть V. Приемы программирования на PHP 442
$GLOBALS["REQUEST_URI"]=
getenv("QUERY_STRING")
);
@putenv("QUERY_STRING=".
$GLOBALS["HTTP_ENV_VARS"]["QUERY_STRING"]=
$GLOBALS["QUERY_STRING"]=
ereg_Replace("^[^?]*\\?","",getenv("QUERY_STRING"))
);
// Подключаем библиотекаря.
$INC[]=getcwd();
include "Librarian.phl";
// Переходим в каталог со страницей.
chdir(dirname($SCRIPT_FILENAME));
// Загружаем шаблонизатор.
Uses("Template");
// Выводим содержимое главного блока страницы.
echo RunUrl($SCRIPT_NAME);
?>
Главный модуль шаблонизатора
Основной код шаблонизатора, который и выполняет всю работу, помещен в библио-
теку Template.phl. Она содержит все функции, которые могут потребоваться в
шаблонах и блочных страницах. Главная функция модуля — RunUrl() — "запуска-
ет" страницу, путь к которой (относительно корневого каталога сервера)
передается в
параметрах. Результат работы этой функции — содержимое блока Output, порож-
денного страницей.
В листинге 30.14 приводится полный код шаблонизатора с комментариями.
Листинг 30.14. Модуль шаблонизатора: Template.phl
// Константы, задающие некоторые значения по умолчанию
define("DefGlue"," | "); // символ склейки по умолчанию
define("Htaccess_Name",".htaccess"); // имя .htaccess-файла
// Имена "стандартных" блоков
define("BlkTemplate","template"); // шаблон страницы
Глава 30. Код и шаблон страницы 443
define("BlkOutput","output"); // этот блок выводится в браузер
define("BlkDefGlue","defaultglue"); // символ для "склейки" по умолчанию
// Рабочие переменные
$GLOBALS["BLOCK"]=array(); // массив тел всех блоков
$GLOBALS["BLOCK_INC"]=array(); // аналог $INC библиотекаря
$GLOBALS["CURBLOCK_URL"]=false; // URL текущего обрабатываемого файла
$GLOBALS["bSingleLine"]=0; // обрабатываемый файл — .htaccess?
// В следующем массиве перечислены имена функций-фильтров,
// которые будут вызваны для каждого блока, когда получено его
// содержимое. Вы, возможно, захотите добавить сюда и другие
// фильтры (например, исполняющие роль простейшего макропроцессора,
// заменяющего одни тэги на другие). Формат функций:
// void FilterFunc(string $BlkName, string &$Value, string $BlkUrl)
$GLOBALS["BLOCKFILTERS"]=array(
"_FBlkTabs",
"_FBlkGlue"
//*** Здесь могут располагаться имена ваших функций-фильтров.
);
// Возвращает тело блока по его имени. Регистр символов не учитывается.
function Blk($name)
{ return @$GLOBALS["BLOCK"][strtolower($name)];
}
// Добавляет указанный URL в список путей поиска. При этом путь
// автоматически преобразуется в абсолютный URL (за текущий каталог
// принимается каталог текущего обрабатываемого файла).
function Inc($url)
{ global $CURBLOCK_URL,$SCRIPT_NAME;
$CurUrl=$CURBLOCK_URL; if(!$CurUrl) $CurUrl=$SCRIPT_NAME;
if($url[0]!="/") $url=abs_path($url,dirname($CurUrl));
$GLOBALS["BLOCK_INC"][]=$url;
}
// Устанавливает имя текущего блока и, возможно, его значение.
// Все данные, выведенные после вызова этой функции, будут принадлежать
// телу блока $name. Если задан параметр $value, тело сразу
// устанавливается равным $value, а весь вывод просто проигноруется.
Часть V. Приемы программирования на PHP 444
// Это удобно для коротких однострочных блоков, особенно расположенных
// в файлах .htaccess. Из того, что было выведено программой в
// стандартный поток, будут удалены начальные и концевые пробелы,
// а также вызовутся все функции-фильтры. Окончанием вывода,
// принадлежащего указанному блоку, считается конец файла либо начало
// другого блока (то есть еще один вызов Block()).
function Block($name=false, $value=false)
{ global $BLOCK,$bSingleLine,$CURBLOCK_URL;
// Объявляем некоторые флаги состояния
static $Handled=false; // в прошлый раз вывод был перехвачен
static $CurBlock=false; // имя текущего обрабатываемого блока
// Если имя блока задано, перевести его в нижний регистр
if($name!==false) $name=strtolower(trim($name));
// Вывод был перехвачен. Значит, что до этого вызова уже
// была запущена функция Block(). Теперь блок, который
// она обрабатывала, закончился, и его надо добавить в массив
// блоков (или же проигнорировать этот вывод).
if($Handled) {
// Имя предыдущего блока было задано?
if($CurBlock!==false) {
// Добавляем в массив блоков.
$BLOCK[$CurBlock]=trim(ob_get_contents());
// Если блок однострочный (из файла .htaccess), то
// удаляем все строки, кроме первой.
if(@$bSingleLine)
$BLOCK[$CurBlock]=ereg_Replace("[\r\n].*","",$BLOCK[$CurBlock]);
// Запускаем фильтры
_ProcessContent($CurBlock,$BLOCK[$CurBlock],$CURBLOCK_URL);
}
// Завершаем перехват потока вывода
ob_end_clean(); $Handled=0;
}
// Если имя блока задано (а это происходит практически всегда),
// значит, функция была вызвана нормальным образом, а не только для
// того, чтобы завершить вывод последнего блока (см. функцию Load()).
if($name!==false) {
// Перехватываем поток вывода
ob_start(); $Handled=1;
// Тело явно не задано, значит, нужно его получить путем
Глава 30. Код и шаблон страницы 445
// перехвата выходного потока. Фактически, сейчас мы просто
// говорим системе, что текущий блок — $name, и что как только
// она встретит другой блок или конец файла, следует принять
// выведенные данные и записать их в массив.
if($value===false) {
$CurBlock=$name;
} else {
// Тело задано явно. Записать блок в массив, но все равно
// перехватить выходной поток (чтобы потом его проигнорировать).
_ProcessContent($name,$value,$CURBLOCK_URL);
$BLOCK[$name]=$value;
$CurBlock=false;
}
}
}
// Загружает файл с URL $name и добавляет блоки, которые в нем
// находились, к списку существующих блоков. Параметр $name может
// задавать относительный URL, в этом случае производится его
// поиск в глобальном массиве $INC (том же самом, который использует
// библиотекарь). Если в качестве $name задано не имя файла, а имя
// каталога, то анализируется файл .htaccess, расположенный
// в этом каталоге. На момент загрузки файла текущий каталог
// изменяется на тот, в котором расположен файл.
function Load($name)
{ global $BLOCK,$bSingleLine,$CURBLOCK_URL,$BLOCK_INC;
// Перевести все пути в $INC в абсолютные
AbsolutizeINC();
// Если путь относительный, ищем по $BLOCK_INC
$fname=false;
if($name[0]!='/') {
// Перебираем все каталоги включения
foreach($BLOCK_INC as $v) {
$fname=Url2Path("$v/$name"); // Определяем имя файла
if(file_exists($fname)) { $name="$v/$name"; break; }
}
// Если не нашли, $fname остается равной false
} else {
// Абсолютный URL — перевести его в имя файла
Часть V. Приемы программирования на PHP 446
$fname=Url2Path($name);
}
// Обрабатываем файл, имя которого вычислено по URL.
// Сначала проверяем, существует ли такой файл.
if($fname===false || !file_exists($fname))
die("Couldn't open \"$name\"!");
// Это каталог — значит, используем .htaccess
$Single=false;
if(@is_dir($fname)) {
$name.="/".Htaccess_Name;
$fname.="/".Htaccess_Name;
$Single=1;
}
// Если файла до сих пор не существует (это может случиться, когда
// мы предписали использовать .htaccess, а его в каталоге нет),
// "мирно" выходим. Ничего страшного, если в каталоге нет .htaccess'а.
if(!file_exists($fname)) return;
// Запускаем файл. Для этого сначала запоминаем текущее состояние
// и каталог, затем загружаем блоки файла (просто выполняем файл),
// а в конце восстанавливаем состояние.
$PrevSingle=$bSingleLine; $bSingleLine=@$Single;
$SaveDir=getcwd(); chdir(dirname($fname));
$SaveCBU=$CURBLOCK_URL; $CURBLOCK_URL=$name;
// Возможно, в файле присутствуют начальные пробелы или другие
// нежелательные символы (например, в .htaccess это может
// быть знак комментария). Все они включатся в блок с
// именем _PreBlockText (его вряд ли целесообразно использовать).
Block("_PreBlockText");
// Делаем доступными все глобальные переменные.
foreach($GLOBALS as $k=>$v) if(!@Isset($$k)) global $$k;
// Запускаем файл.
include $fname;
// Сигнализируем, что блоки закончились (достигнут конец файла).
// При этом чаще всего будет осуществлена запись данных последнего
// блока файла в массив.
Block();
chdir($SaveDir);
$CURBLOCK_URL=$SaveCBU;
$bSingleLine=$PrevSingle;
Глава 30. Код и шаблон страницы 447
}
// Главная функция шаблонизатора. Обрабатывает указанный файл $url
// и возвращает тело блока Output. В выходной поток ничего не печатается
// (за исключением предупреждений, если они возникли).
function RunUrl($url)
{ global $BLOCK;
// Собираем все блоки.
_CollectBlocks($url);
// Находим и запускаем главный шаблон. Мы делаем это в последнюю
// очередь, чтобы ему были доступны все блоки, из которых состоит
// страница. Шаблон — обычный блочный файл. В нем обязательно должен
// присутствовать блок Output.
$tmpl=@$BLOCK[BlkTemplate];
if(!$tmpl) {
die("Cannot find the template for $url ".
"(have you defined ".BlkTemplate." block?)");
}
Load($tmpl);
// Возвращаем блок Output.
if(!isSet($BLOCK[BlkOutput])) {
die("No output from template $tmpl ".
"(have you defined ".BlkOutput." block?)");
}
return $BLOCK[BlkOutput];
}
// Эта функция предназначена для внутреннего использования. Она собирает
// блоки из файла, соответствующего указанному $url, в том числе и блоки
// из всех .htaccess-файлов "надкаталогов".
function _CollectBlocks($url)
{ global $BLOCK;
$url=abs_path($url,dirname($GLOBALS["SCRIPT_NAME"]));
// Если путь — не /, то обратиться к "надкаталогу".
if(strlen($url)>1) _CollectBlocks(dirname($url));
// Загрузить блоки самого файла.
Load($url);
}
Часть V. Приемы программирования на PHP 448
// Запускает все фильтры для блока.
function _ProcessContent($name,&$cont,$url)
{ foreach($GLOBALS["BLOCKFILTERS"] as $F)
$F($name,$cont,$url);
}
// "Склеивание" блоков.
// Если тело блока начинается с [name], то оно не просто
// записывается в массив блоков, а "пристыковывается" к значению,
// уже там находящемуся, причем в качестве символа-соединителя
// выступает тело блока с именем name. Если строка name не задана
// (то есть указаны []), используется блок с именем DefaultGlue,
// а если этого блока нет, то соединитель по умолчанию — " | ".
function _FBlkGlue($name,&$cont,$url)
{ global $BLOCK;
if(ereg("^\\[([^]])*]",$cont,$P)) {
$c=substr($cont,strlen($P[0])); // тело блока после [name]
$n=$P[1]; // имя соединителя
// Есть с чем "склеивать"?
if(!empty($BLOCK[$name])) {
$glue=@$BLOCK[$n];
if(!Isset($glue)) $glue=@$BLOCK[BlkDefGlue];
if(!Isset($glue)) $glue=DefGlue;
$cont=$BLOCK[$name].$glue.$c;
}
// "Склеивать" нечего — просто присваиваем.
else $cont=$c;
}
}
// Удаление начальных символов табуляции из тела блока.
// Теперь можно выравнивать HTML-код в документах с помощью табуляции.
// Это оказывается чрезвычайно удобным, если мы используем тэги,
// например, в таком контексте:
// < ?foreach($Book as $k=>$v) {? >
//
// < ?=$Book['name']? >
// < ?=$Book['text']? >
Глава 30. Код и шаблон страницы 449
//
// < ?}? >
function _FBlkTabs($name,&$cont,$url)
{ // используем регулярное выражение в формате PCRE, т. к. это —
// единственный приемлемый способ решения задачи
$cont=preg_replace("/^\t+/m","",$cont);
}
?>
"Перехват" выходного потока
В коде листинга 30.14 есть всего лишь несколько вызовов стандартных функций,
ко-
торые мы еще не рассматривали в этой книге. Я имею в виду функции с префиксами
ob_ (от Output Buffering — Буферизация вывода). Их задача — "перехватить" тот
текст, который выводится операторами echo, а также участками, расположенными
вне PHP-тэгов и ?>, и направить его в строковую переменную для дальнейшей
обработки.
Эти чрезвычайно полезные функции впервые введены в PHP версии 4. Нужно за-
метить, что без них вряд ли можно написать более-менее удобный шаблонизатор.
Я привожу здесь их описания в том виде, который принят в этой книге.
void ob_start()
Вызов данной функции говорит PHP, что необходимо начать "перехват" стандартного
выходного потока программы. Иными словами, весь текст, который выводится опе-
раторами echo или расположен вне участков кода PHP, будет накапливаться в
специ-
альном буфере, а не отправится в браузер. В любой момент времени мы можем полу-
чить все содержимое этого буфера, вызвав функцию ob_get_contents(). В
шаблонизаторе мы вызываем ob_start() каждый раз, когда встречается начало
нового блока.
string ob_get_contents()
Функция возвращает текущее содержимое буфера, который заполняется операторами
вывода при включенном режиме буферизации. Именно ob_get_contents() обеспе-
чивает в нашем шаблонизаторе возможность накопления текста блоков. Она вызыва-
ется (а возвращенные данные записываются в массив) каждый раз, когда заканчива-
ется очередной блок (вернее, перед началом следующего блока), а также при
достижении конца файла.
Часть V. Приемы программирования на PHP 450
В случае, если буферизация выходного потока не была включена, функция
возвращает false. Это свойство можно использовать для проверки того, ус-
тановлен ли буфер вывода, или же данные сразу направляются в браузер.
void ob_end_clean()
Вызов данной функции завершает буферизацию выходного потока. При этом все со-
держимое буфера, которое было накоплено с момента последнего вызова
ob_start(), теряется (не попадает в браузер). Конечно, если текст вывода нужен,
необходимо сначала получить его при помощи ob_get_content(). Именно так и
происходит в шаблонизаторе. Вызов функции ob_end_clean() с последующим
ob_start() — единственный способ очистить внутренний буфер PHP.
void ob_end_flush()
Эта функция практически полностью эквивалентна ob_end_clean(), за исключени-
ем того, что данные, накопленные в буфере, немедленно выводятся в браузер
пользо-
вателя. Ее применение оправдано, если мы хотим отправлять данные страницы кли-
енту, параллельно записывая их в переменную для дальнейшей обработки.
Стек буферов
Необходимо сделать несколько замечаний насчет функций "перехвата" выходного
потока программы. Что получится, если больше одного раза подряд вызвать
ob_start()? Хотя об этом не написано ни слова в официальной документации, ри-
скну взять на себя ответственность и заявить, что, в общем-то, ничего
нежелательно-
го не произойдет. Последующие операторы вывода будут работать с тем буфером,
который был установлен самым последним вызовом. При этом функция
ob_end_clean() не завершит буферизацию, а просто установит в активное состоя-
ние "предыдущий" буфер (разумеется, сохранив его предыдущее содержимое). Легче
всего понять этот механизм на примере:
Листинг 30.15. Пример "перехвата" выходного потока
ob_start(); // устанавливаем перехват в буфер 1
echo "1"// попадет в 1-й буфер
ob_start(); // откладываем на время буфер 1 и активизируем второй
echo "2"; // текст попадет в буфер 2
$A[2]=ob_get_contents(); // текст во втором буфере
ob_end_clean(); // отключает буфер 2 и активизируем первый
echo "1"; // попадет опять в буфер 1
$A[1]=ob_get_contents(); // текст в первом буфере
Глава 30. Код и шаблон страницы 451
ob_end_clean(); // т. к. это последний буфер, буферизация отключается
// Распечатываем значения буферов, которые мы сохранили в массиве
foreach($A as $i=>$t) echo "$i: $t ";
// Выводится:
// 2: 2
// 1: 11
?>
Мы видим, что схема буферизации выходного потока чем-то похожа на стек: всегда
используется тот буфер, который был активизирован последним. У такой схемы до-
вольно много положительных черт, но есть и одна отрицательная. А именно, если
какая-то логическая часть программы использует буферизацию выходного потока, но
по случайности "забудет" вызвать ob_end_clean() перед своим завершением, ос-
тавшаяся программа останется "в недоумении", что же произошло. К сожалению, в
PHP мы никак не сможем обойти это ограничение, так что призываю вас быть осо-
бенно внимательными.
Проблемы с отладкой
В последней версии PHP на момент написания этих строк имелось небольшое неудоб-
ство, которое может превратить отладку программ, использующих буферизацию, в
сущий ад. Дело в том, что при включенной буферизации все предупреждения, в нор-
мальном состоянии генерируемые PHP, записываются в буфер, а потому (если про-
грамма не отправляет буфер в браузер) могут потеряться. К счастью, это касается
лишь предупреждений, которые не завершают работу сценария немедленно. Фаталь-
ные ошибки отправляются в браузер почти всегда. Неприятность как раз и состоит
в
этом "почти". Даже фатальные ошибки останутся программистом незамеченными,
если он вызывает функцию ob_start() вложенным образом, — т. е. более одного
раза, как это было описано в предыдущем абзаце. Например, если в листинге 30.15
после присваивания текста элементу массива $A[2] вставить вызов несуществующей
функции, программа сразу же выдаст пользователю текущее содержимое буфера но-
мер 1, а затем, "не сказав ни слова", завершится. Это и понятно: ведь сообщение
об
ошибке попало во второй буфер, а значит, было проигнорировано! Почему разработ-
чики PHP, вопреки общеизвестной практике, не разделили стандартный выходной
поток и поток ошибок, остается неясным.
Если вы заметили, шаблонизатор всегда использует не более одного буфера
"перехва-
та" в каждый момент времени. Это сделано именно из соображений упрощения от-
ладки сценариев. И все же, если нефатальное предупреждение было сгенерировано в
момент обработки блока, который по каким-то причинам не входит в шаблон страни-
цы, оно останется незамеченным программистом. Впрочем, наверное, в этом нет ни-
чего страшного: раз блок не выводится, значит, нам все равно, правильно он
отрабо-
тан или нет….
Часть V. Приемы программирования на PHP 452
Глава 31
Объектно-ориентированное
программирование на PHP
В последние 10 лет идея объектно-ориентированного программирования (ООП),
кардинально новая идеология написания программ, все более занимает умы про-
граммистов. И это неудивительно. В самом деле, сейчас происходит (а точнее, уже
произошло, особенно после выхода стандарта на С++ от 98-го года и изобретения
таких языков, как Java и Delphi) примерно то же, что произошло в начале 80-х
годов
при появлении идеи структурного программирования.
Объектно-ориентированные программы более просты и мобильны, их легче модифи-
цировать и сопровождать, чем их "традиционных" собратьев. Кроме того, похоже,
сама идея объектной ориентированности при грамотном ее использовании позволяет
программе быть даже более защищенной от различного рода ошибок, чем это заду-
мывал программист в момент работы над ней. Однако ничего не дается даром: сами
идеи ООП довольно трудны для восприятия "с нуля", поэтому до сих пор очень
боль-
шое количество программ (различные системы Unix, Apache, Perl, да и сам PHP)
все
еще пишутся на старом добром "объектно-неориентированном" Си. Что ж, очень
жаль. Ощущение жалости усиливается, если посмотреть на исходные тексты этих
программ, поражающие своей многословностью...
PHP, как и большинство современных языков, обеспечивает некоторую поддержку
ООП. Конечно, эта поддержка далеко не полна: например, нет множественного на-
следования и сокрытия данных, довольно примитивен и сам механизм наследования
и полиморфизма. Правда, в четвертой версии PHP наметился кое-какой прогресс:
появились ссылочные переменные, но их использование все еще несколько затрудни-
тельно из-за неудобного синтаксиса. Однако это все же лучше, чем ничего.
В этой главе я кратко изложу основные идеи ООП, подкрепляя их иллюстрациями
программ на PHP. Конечно, данная глава ни в коей мере не претендует на звание
учебника по ООП. Интересующимся читателям рекомендую изучить любой из мону-
ментальных трудов Бьерна Страуструпа, изобретателя языка C++.
Классы и объекты
Ключевым понятием ООП является класс. Класс — это просто тип переменной. Ну,
не совсем просто... На самом деле переменная класса (далее будем ее называть
объ-
Часть V. Приемы программирования на PHP 454
ектом класса) является в некотором смысле автономной сущностью. Обычно такой
объект имеет набор свойств и операций (или методов), которые могут быть с ним
проведены. Например, мы можем рассматривать тип int как класс. Тогда перемен-
ная этого "класса" будет обладать одним свойством (ее целым значением), а также
набором методов (сложение, вычитание, инкремент и т. д.).
В языке C++ мы могли бы, действительно, объявить тип int именно таким образом.
Однако в PHP дело обстоит немного хуже: мы не имеем права переопределять стан-
дартные операции (сложение, вычитание и т. д.) для объектов. Например, если бы
мы
захотели добавить в язык комплексные числа, в C++ это можно было сделать без
осо-
бых затруднений (и класс комплексных чисел по использованию практически не от-
личался бы от встроенного типа int), однако в PHP у нас такое добавление не
удаст-
ся. Альтернативное решение состоит в том, чтобы везде вместо + и других
операций
использовать вызовы соответствующих функций — например, Add(), которые бы
являлись методами класса.
Но обо всем по порядку. Давайте посмотрим, как создать класс в PHP. Это
довольно
несложно:
class MyName {
описания свойств
. . .
определения методов
}
Замечу, что здесь не создается объекта класса, а только определяется новый тип.
Чтобы создать объект класса MyName, в PHP нужно воспользоваться специальным
оператором new:
$Obj = new MyName;
Вот теперь в программе существует объект $Obj, который "ведет себя" так же, как
и
все остальные объекты класса MyName.
Свойства объекта
Но давайте пока не будем создавать объектов, а вернемся опять к классу. Сначала
(честно говоря, можно и не только в начале, но и в любом другом месте описания)
должны следовать описания свойств класса. Свойство — это просто своеобразная
переменная внутри объекта класса, в которой может храниться какое-то значение.
Например, в классе таблицы MySQL, которым мы вскоре займемся, имя таблицы
задано в виде свойства $TableName.
То есть, грубо говоря, каждый объект-таблица содержит в себе свою собственную
переменную $TableName и имеет над ней полный контроль. Какие именно свойства
будет иметь любой объект заданного класса, указывается при создании этого
класса.
Глава 31. Объектно-ориентированное программирование на PHP 455
Мы можем представить несколько объектов одного и того же типа как братьев-
близнецов: у них все одинаково с "физиологической" точки зрения (одни и те
же имена свойств), но на самом деле это совершенно разные "люди" — у них
разные взгляды, различный образ жизни (свойства содержат разные значе-
ния).
Объект класса может напрямую обращаться к своим свойствам, считывать их или
записывать. Еще раз: каждый объект одного и того же класса имеет свой собствен-
ный набор значений свойств, и они не пересекаются. Таким образом, объект класса
со
стороны представляется контейнером, хранящим свои свойства.
Объявление свойств задается при помощи ключевого слова var:
var $pName1, $pname2, ...;
Мы видим, что каждое свойство должно иметь уникальное имя в классе. Инструкций
var может быть несколько, и они могут встречаться в любом месте описания класса,
а не только в его начале.
Займемся теперь вопросом о том, как нам из программы получить доступ к
какому-то
свойству определенного объекта (например, объекта $Obj, который мы только что
создали). Это делается очень просто при помощи операции ->:
// Выводим в браузер значение свойства Name1 объекта $Obj
echo $Obj->Name1;
// Присваиваем значение свойству
$Obj->Name2="PHP Four";
Если какое-то свойство (например, с именем SubObj) объекта само является объек-
том (что вполне допустимо), нужно использовать две "стрелочки":
// Выводим значение свойства Property объекта-свойства
// $SubObj объекта $Obj
echo $Obj->SubObj->Property;
Такой синтаксис был придуман из того расчета, чтобы быть максимально простым.
Добавлю, что указание объекта $Obj перед стрелкой обязательно по той причине,
что
каждый объект имеет свой собственный набор свойств. Поэтому-то они и не пересе-
каются при хранении, а при доступе нужно уточнить объект, свойство которого за-
прашивается.
Впрочем, в данном простом примере объект ничем не лучше обычного ассоциативно-
го массива — ведь мы просто используем его как контейнер для хранения свойств.
Поэтому давайте поговорим о более существенном отличии — методах класса.
Часть V. Приемы программирования на PHP 456
Методы
Основная идея ООП — инкапсуляция — базируется на объединении данных (свойств)
объекта с функциями, которые эти данные обрабатывают. В самом деле, почему это
мы привыкли разграничивать информацию и методы ее обработки? Разве, в конце
концов, эти методы сами не являются информацией? Зачем же разделять нераздели-
мые сущности?..
Фактически, свойства хранят в себе состояние объекта в данный момент времени,
тогда как методы (функции обработки) являются чем-то вроде механизма посылки
запроса экземпляру класса (объекту). Например, в классе таблицы MySQL, которую
мы с вами вскоре напишем, может быть довольно большой набор методов. Самый
простой из них — Drop(), заставляющий таблицу очистить и удалить себя из базы
данных. Вызов этого метода из программы происходит примерно так:
$Obj->Drop(); // таблица $Obj удаляет сама себя!
Конечно, у методов, как и у обычных функций, могут быть параметры.
К примеру, метод Add() того же класса (добавление новой записи в таблицу)
прини-
мает только один параметр — ассоциативный массив, содержащий данные, а метод
Select() (получить все записи, удовлетворяющие запросу) использует три парамет-
ра — логическое выражение запроса, максимальное количество получаемых записей
и правила сортировки результата. Он возвращает массив с результирующими запи-
сями.
Класс таблицы MySQL
Пожалуй, я слишком далеко заглянул в будущее. Вернемся назад к основам. Чтобы
определить метод внутри класса, используется следующий синтаксис:
сlass MyClass {
. . .
function Method(параметры)
{ . . .
}
. . .
}
Давайте будем потихоньку набрасывать план нашего класса MySQL-таблицы. Во
первых, зададимся вопросом: зачем нам вообще это нужно? Почему бы не пользо-
ваться обычными функциями для работы с MySQL? Ответ не вполне очевиден, по-
этому оставим его на потом. А пока будем считать, что такой класс нам необходим
(а
он действительно необходим, т. к. значительно упрощает работу с базой данных).
Во-вторых, сформулируем правило: обращаться к какой-то таблице MySQL только
посредством нашего класса, а точнее, объекта этого класса, связанного с
таблицей.
Как же его связать? Очевидно, объект должен содержать имя таблицы, к которой он
Глава 31. Объектно-ориентированное программирование на PHP 457
"привязан". Так как в программе могут использоваться одновременно несколько
таб-
лиц и несколько объектов, то, наверное, логично это самое имя хранить в виде
свой-
ства.
Что бы еще хотелось знать об объекте-таблице? Конечно, имена и типы ее полей.
По-
местим их в свойство-массив. Наконец, в процессе работы наверняка иногда будут
возникать ошибки. Чтобы как-то сигнализировать о них, предлагаю в класс-таблицу
ввести еще одно свойство — Error. Оно будет равно нулю, если предыдущая опера-
ция (например, добавление записи) прошла успешно, и тексту ошибки — в против-
ном случае.
Вот что у нас пока получилось:
class MysqlTable {
var $TableName; // Имя таблицы в базе данных
var $Fields; // Массив полей. Ключ — имя поля, значение — его тип
var $Error; // Индикатор ошибки
. . .
}
Согласитесь, это почти все данные, которые должны храниться в объекте-таблице.
Все
остальное (например, записи) находится в базе данных. Нам нужно научиться
каким-то
образом легко извлекать и добавлять (а также удалять, подсчитывать и обновлять)
эти
записи путем простых запросов к объекту-таблице. Для этого я предлагаю написать
соот-
ветствующие методы (листинг 31.1).
Пока мы не будем расписывать код методов. Взамен просто обозначим его
словом "команды" в тексте программы. Вообще говоря, такой способ проекти-
рования, когда сначала решают, какие методы нам нужны, а потом начинают
продумывать их код, довольно типичен для ООП.
Листинг 31.1. Эскиз класса таблицы
class MysqlTable {
var $TableName; // Имя таблицы в базе данных
var $Fields; // Массив полей. Ключ — имя поля, значение — его тип
var $Error; // Индикатор ошибки
// Добавляет в таблицу запись $Rec. $Rec должна представлять из себя
// обычный ассоциативный массив. В будущем мы придем к тому, что
// массив $Rec будет представлен даже древовидной структурой,
// т. е. будет иметь подмассивы.
// Как вы понимаете, непосредственной поддержки этого в MySQL нет,
// но мы "ее" реализуем.
Часть V. Приемы программирования на PHP 458
function Add($Rec) { команды; }
// Возвращает массив записей (ключ — id записи, значение —
// ассоциативный массив, в точности такой же, какой был помещен
// некогда в таблицу при помощи Add), удовлетворяющих выражению
// $Expr. Возвращаются только первые $Num (или менее) записей.
// Сортировка осуществляется в соответствии с критерием $Order.
function Select($Expr,$Num=1e10,$Order="id desc") { команды; }
// Удаляет из таблицы все записи, удовлетворяющие выражению $Expr.
function Delete($Expr) { команды; }
// Удаляет из таблицы все записи (например, при помощи вызова
// Delete("1=1") и удаляет саму таблицу из базы данных. Этот
// метод довольно опасен!
function Drop() { команды; }
}
Пока, пожалуй, хватит. Я не буду здесь углубляться в то, как устроен каждый из
на-
званных методов. Этим мы займемся в свое время. А пока обратите внимание на то,
что мы попытались определить все операции, которые вообще применимы к таблице
MySQL (на самом деле, это далеко не полный их перечень, но пока нам и такого
ко-
личества вполне достаточно). Это очень важно, потому что потом, когда будем ис-
пользовать объекты класса MysqlTable, мы сможем вообще забыть, что такое
MySQL и язык запросов SQL, или поручить разработку программы, обращающейся к
MysqlTable, человеку, не разбирающемуся в SQL.
Вообще говоря, это один из самых главных приемов ООП (структурного программи-
рования — в меньшей степени) — постоянно размышлять, как бы нам сделать так,
чтобы потом можно было побольше "забыть". Работает принцип: если вы используете
какой-то класс и не догадываетесь, как он реализован, причем это вам нисколько
не
мешает, значит, класс хорош.
И наоборот. Впрочем, совсем абстрагироваться от SQL нам все же не удастся —
все-
таки нужно знать правила составления выражений для выборки и удаления записей,
для их сортировки и т. д. Но это уже не SQL, а что-то гораздо более простое и
интуи-
тивно понятное.
Доступ объекта
к своим свойствам
Как ни странно, но при изучении ООП "с нуля" программисты, привыкшие к струк-
турному программированию, часто с трудом понимают, каким образом объект может
добраться до своих собственных свойств. Рассмотрим, например, такую программу:
$Obj1=new Mysqltable;
Глава 31. Объектно-ориентированное программирование на PHP 459
$Obj2=new MysqlTable;
. . .
echo $Obj1->TableName, " ", $Obj2->TableName;
Здесь никаких проблем не возникает — ясно, что выводятся свойства разных объек-
тов — мы же сами указали их до стрелки. Однако давайте посмотрим, что будет,
если
вызвать какой-нибудь метод одного из объектов:
$Obj1->Drop();
Как видите, при вызове метода так же, как и при доступе к свойству, нужно
указать
объект, который должен "откликнуться на запрос". Действительно, этой командой
мы
удаляем из базы данных таблицу $Obj1, а не $Obj2. Рассмотрим теперь тело метода
Drop():
class MysqlTable {
function Drop()
{ сюда интерпретатор попадет, когда вызовется Drop() для
какого-то объекта
}
}
По логике, Drop() — функция. Эта функция, конечно, едина для всех объектов
клас-
са MysqlTable. Но как же метод Drop() узнает, для какого объекта он был вызван?
Ведь мы не можем Drop() для $Obj1 сделать одним, а для $Obj2 — другим, иначе
нарушился бы весь смысл нашей объектной ориентированности. В том-то вся и соль,
что два различных объекта-таблицы являются объектами одного и того же класса...
Оказывается, для доступа к свойствам (и методам, т. к. один метод вполне может
вы-
зывать другой) внутри метода используется специальная предопределенная перемен-
ная $this, содержащая тот объект, для которого был вызван метод. Теперь мы мо-
жем определить Drop() внутри класса так:
function Drop()
{ // сначала удаляем все записи из таблицы
$this->Delete("1=1"); // всегда истинное выражение
// а затем удаляем саму таблицу
mysql_query("drop table ".$this->TableName);
}
Если мы вызвали Drop() как $Obj1->Drop(), то $this будет являться тем же объ-
ектом, что и $Obj1 (это будет ссылка на $Obj1), а если бы мы вызвали $Obj2-
>Drop(), то $this был бы равен $Obj2. То есть метод всегда знает, для какого
объекта он был вызван. Это настолько важно, что я повторю еще раз: метод всегда
знает, для какого объекта он был вызван.
Использование ссылок говорит о том, что $this — не просто копия объекта-хозяина,
это и есть хозяин. Например, если бы в $Obj1->Drop() мы захотели изменить ка-
Часть V. Приемы программирования на PHP 460
кое-то свойство $this, оно поменялось бы и у $Obj1, но не у $Obj2 или других
объ-
ектов.
В синтаксисе PHP есть один просчет: запись вида
$ArrayOfObjects["obj"]->DoIt();
считается синтаксически некорректной. Вместо нее применяйте следующие две ко-
манды:
$obj=&$ArrayOfObjects["obj"]; $obj->DoIt();
Не забудьте про & сразу после оператора присваивания (то есть создавайте
ссылку на элемент массива), иначе метод DoIt() будет вызван не для самого
объекта, присутствующего в массиве, а для его копии, полученной в $obj!
Инициализация объекта.
Конструкторы
До сих пор мы не особенно задумывались, каким образом были созданы объекты
$Obj1 и $Obj2 и к какой таблице они прикреплены. Однако вполне очевидно, что
эти
объекты не должны существовать сами по себе — это просто не имеет смысла. По-
этому нам, наравне с уже описанными методами, придется написать еще один — а
именно, метод, который бы:
r "привязывал" только что созданный объект-таблицу к таблице в MySQL;
r сбрасывал индикатор ошибок;
r заполнял свойство Fields;
r делал другую работу по инициализации объекта.
Назовем это метод, например, Init():
class MysqlTable {
. . .
// Привязывает объект-таблицу к таблице с именем $TblName
function Init($TblName)
{ $this->TableName=$TblName;
$this->Error=0;
получаем и заполняем $this->Fields
}
}
. . .
$Obj=new MysqlTable; $Obj->Init("test");
Глава 31. Объектно-ориентированное программирование на PHP 461
А вдруг между вызовами new и Init() случайно произойдет обращение к таблице?
Или кто-то по ошибке забудет вызвать Init() для созданного объекта (что обяза-
тельно случится, дайте только время)? Это приведет к непредсказуемым
последстви-
ям. Поэтому, как и положено в ООП, мы можем завести метод вместо Init(), кото-
рый будет вызываться автоматически сразу же после инструкции new и проводить
работы по инициализации объекта. Он называется конструктором, или инициализа-
тором. Чтобы PHP мог понять, что конструктор следует вызывать автоматически,
ему
(конструктору) нужно дать то же имя, что и имя класса. В нашем примере это
будет
выглядеть так:
class MysqlTable {
function MysqlTable($TblName)
{ команды, ранее описанные в Init();
}
}
$Obj=new MysqlTable("test"); // создаем и сразу же инициализируем объект
Обратите внимание на синтаксис передачи параметров конструктору. Если бы мы
случайно пропустили параметр test, PHP выдал бы сообщение об ошибке. Таким
образом, теперь в программе потенциально не могут быть созданы объекты-таблицы,
ни к чему не привязанные.
Деструктор
По аналогии с конструкторами обычно рассматриваются деструкторы. Деструк-
тор — тоже специальный метод объекта, который вызывается при уничтожении это-
го объекта (например, после завершения программы). Деструкторы обычно выпол-
няют служебную работу — закрывают файлы, записывают протоколы работы,
разрывают соединения, "форматируют винчестер" — в общем, освобождают ресур-
сы. К сожалению, из-за "щедрости" PHP на выделение памяти, которая никогда не
будет освобождена, деструкторы в нем не поддерживаются. Так что, если вам нужно
выполнить нечто необычное после того, как вы перестали использовать какой-то
объ-
ект, определите в нем метод, который будет это делать, и вызовите его явно.
Наследование
Создание самодостаточных объектов — довольно неплохая идея. Однако это далеко
не единственная возможность ООП. Сейчас мы займемся наследованием — одним из
основных понятий ООП.
Итак, пусть у нас есть некоторый класс A с определенными свойствами и методами.
Но то, что этот класс делает, нас не совсем устраивает — например, пусть он
выпол-
няет большинство функций, по сути нам необходимых, но не реализует некоторых
других. Зададимся целью создать новый класс B, как бы "расширяющий" возможно-
Часть V. Приемы программирования на PHP 462
сти класса A, добавляющий ему несколько новых свойств и методов. Сделать это
можно двумя принципиально различными способами. Первый выглядит примерно
так:
class A {
function TestA() { ... }
function Test() { ... }
}
class B {
var $a; // объект класса A
function B(параметры_для_A, другие_параметры)
{ $a=new A(параметры_для_A);
инициализируем другие поля B
}
function TestB() { ... }
function Test() { ... }
}
Поясню: в этой реализации объект класса B содержит в своем составе подобъект
класса A в качестве свойства. Это свойство — лишь "частичка" объекта класса B,
не
более того. Подобъект не "знает", что он в действительности не самостоятелен, а
со-
держится в классе B, поэтому не может предпринимать никаких действий, специфич-
ных для этого класса.
Но вспомним, что мы хотели получить расширение возможностей класса A, а не не-
что, содержащее объекты A. Что означает "расширение"? Лишь одно: мы бы хотели,
чтобы везде, где допустима работа с объектами класса A, была допустима и работа
с
объектами класса B. Но в нашем примере это совсем не так.
r Мы не видим явно, что класс B лишь расширяет возможности A, а не является от-
дельной сущностью.
r Мы должны обращаться к "части A" класса B через $obj->a->TestA(), а к чле-
нам самого класса B как $obj->TestB(). Последнее может быть довольно утоми-
тельным, если, как это часто бывает, в B будет использоваться очень много мето-
дов из A и гораздо меньше — из B. Кроме того, это заставляет нас постоянно
помнить о внутреннем устройстве класса B.
Впрочем, такой способ расширения также иногда находит применение. Мы погово-
рим об этом чуть позже. А пока рассмотрим, что же представляет собой
наследование
(или расширение возможностей) классов.
class B extends A {
function B(параметры_для_A, другие_параметры)
{ $this->A(параметры_для_A);
инициализируем другие поля B
Глава 31. Объектно-ориентированное программирование на PHP 463
}
function TestB() { ... }
function Test() { ... }
}
Ключевое слово extends говорит о том, что создаваемый класс является лишь "рас-
ширением" класса A, и не более того. То есть B содержит те же самые свойства и
ме-
тоды, что и A, но, помимо них и еще некоторые дополнительные, "свои".
Теперь "часть A" находится прямо внутри класса B и может быть легко доступна,
на-
равне с методами и свойствами самого класса B. Например, для объекта $obj
класса
B допустимы выражения $obj->TestA() и $obj->TestB(). Итак, мы видим, что,
действительно, класс B является воплощением идеи "расширение функциональности
класса A". Обратите также внимание: мы можем теперь забыть, что B унаследовал
от
A некоторые свойства или методы — снаружи все выглядит так, будто класс B
реали-
зует их самостоятельно.
Немного о терминологии: принято класс A называть базовым, а класс B — про-
изводным от A. Иногда базовый класс также называют суперклассом, а произ-
водный — подкласcом.
Зачем может понадобиться наследование? Например, мы написали класс Mysql-
таблицы и хотели бы дополнительно иметь класс Guestbook (гостевая книга). Оче-
видно, в классе Guestbook будет много методов, которые нужны для того же, что и
методы из MysqlTable, поэтому было бы разумным сделать его производным от
MysqlTable:
class Guestbook extends MysqlTable {
. . .
методы и свойства, которых нет в MysqlTable
и которые относятся к гостевой книге
}
Многие языки программирования поддерживают множественное наследование (то
есть такое, когда, скажем, класс B наследует члены не одного, а сразу
нескольких
классов — например, A и Z). К сожалению, в PHP таких возможностей нет.
Полиморфизм
Полиморфизм (многоформенность) — это, я бы сказал, одно из интересных следст-
вий идеи наследования. В общих словах, полиморфность класса — это его способ-
ность использовать функции производных от него классов, даже если на момент оп-
ределения еще неизвестно, какой именно класс будет включать его в качестве
базового и, тем самым, становиться от него производным.
Часть V. Приемы программирования на PHP 464
Вернемся к нашему предыдущему примеру с классами A и B.
class A {
// Выводит, функция какого класса была вызвана
function Test() { echo "Test from A\n"; }
// Тестовая функция — просто переадресует на Test()
function Call() { Test(); }
}
class B extends A {
// Функция Test() для класса B
function Test() { echo "Test from B\n"; }
}
$a=new A();
$b=new B();
Давайте рассмотрим следующие команды:
$a->Call(); // напечатается "Test from A"
$b->Test(); // напечатается "Test from B"
$b->Call(); // Внимание! Напечатается "Test from B"!
Обратите внимание на последнюю строчку: вопреки ожиданиям, вызывается не
функция Test() из класса A, а функция из класса B! Складывается впечатление,
что
Test() из B просто переопределила функцию Test() из A. Так оно на самом деле и
есть. Функция, переопределяемая в производном классе, называется виртуальной.
Механизм виртуальных функций позволяет нам, например, "подсовывать" функциям,
ожидающим объект одного класса, объект другого, производного, класса. Еще один
классический пример — класс, воплощающий собой свойства геометрической фигу-
ры, и несколько производных от него классов — квадрат, круг, треугольник и т. д.
Базовый класс имеет виртуальную функцию Draw(), которая заставляет объект нари-
совать самого себя. Все производные классы-фигуры, разумеется, переопределяют
эту
функцию (ведь каждую фигуру нужно рисовать по-особому). Также у нас есть массив
фигур, причем мы не знаем, каких именно. Зато, используя полиморфизм, мы можем,
не задумываясь, перебрать все элементы массива и вызвать для каждого из них
метод
Draw() — фигура сама "решит", какого она типа и как ее рисовать.
В нашем классе MysqlTable, который мы еще только-только наметили, идея поли-
морфизма найдет свое применение. И вот зачем. Мы проектируем класс так, чтобы
другие классы, которые он будет использовать, подключали его к себе как
производ-
ный. Тем самым они наследуют все свойства MysqlTable и добавляют некоторые
свои. Например, класс Guestbook, реализующий гостевую книгу, может быть произ-
водным от MysqlTable и "расширять" его некоторыми дополнительными функция-
ми — например, проверкой орфографии во введенном сообщении или же контролем,
имеет ли право тот или иной пользователь писать в книгу (или он "отключен" за
ис-
пользование ненормативной лексики). Кроме того, прежде чем помещать данные в
Глава 31. Объектно-ориентированное программирование на PHP 465
MySQL-таблицу, наверное, разумным будет их немного "почистить" — убрать лиш-
ние пробелы, HTML-тэги и т. д. Конечно, такой корректировке должны быть подвер-
жены все поля книги. Поэтому класс MysqlTable перед помещением очередной за-
писи в таблицу будет вызывать виртуальную функцию PreModify(), передавая ей в
параметрах запись, которая должна быть откорректирована. Естественно, в классе
Guestbook эта функция должна переопределяться — так, чтобы выполнять требуе-
мые действия по коррекции записи перед ее занесением в таблицу. Конечно, класс
MysqlTable не "знает", как именно будет переопределена PreModify() в производ-
ном от него классе, поэтому сам он содержит функцию PreModify(), не делающую
ничего (то есть с пустым телом).
Думаю, если вы слышите об ООП впервые, это объяснение будет для вас как
китайская грамота. В то же время знатоки сочтут его слишком простым, чтобы
быть достойным этой книги. К сожалению, так получается всегда, когда пыта-
ешься сжатым языком рассказать о чем-то нетривиальном. А я тем временем
еще раз настоятельно рекомендую вам прочитать учебник по ООП, коим ни в
коей мере не является эта книга.
Полноценный класс таблицы MySQL
Я ранее обещал, что в каждой главе части V книги обязательно будет
присутствовать
пример нетривиального кода на PHP, который (или идеи из которого) вы сможете
использовать в своих программах. На этот раз "исходник" оказался особенно боль-
шим, но это с лихвой оправдывается его функциональностью. Сейчас мы с вами раз-
работаем полноценный класс, который существенно облегчает работу с таблицей
MySQL, в значительной степени абстрагируя программиста не только от специфики
этой СУБД, но и вообще от сложностей SQL-запросов. С помощью этого класса даже
начинающий программист сможет построить форум, гостевую книгу, да и вообще
любую программу, которая требует структурированного хранилища данных большого
объема. Правда, для того, чтобы извлекать максимальную выгоду из использования
класса, придется разобраться в механизме наследования, вкратце описанном чуть
выше. Впрочем, класс прекрасно работает и сам по себе. Вот его некоторые
отличи-
тельные черты.
r Кодирование и декодирование данных производится автоматически. Программи-
сту не нужно заботиться о том, чтобы ставить слэши перед апострофами и други-
ми специальными символами. Все, что от него требуется, — передать той или
иной функции массив, представляющий собой запись.
r Таблица является с точки зрения программиста набором записей совершенно про-
извольной структуры (с произвольным числом полей). При создании таблицы ука-
зываются лишь ее несущие поля, по которым можно в будущем вести поиск, сор-
тировку и т. д. Все остальные поля перед помещением записи в таблицу
Часть V. Приемы программирования на PHP 466
подвергаются сериализации, а при чтении из таблицы — восстановлению, "про-
зрачно" для вызывающей программы.
r В то же время имеется возможность добавления/удаления несущих столбцов
"на лету", т. е. без какого бы то ни было специального запроса пользователя.
Дос-
таточно изменить список несущих полей при создании/открытии таблицы. Класс
сам определяет, что именно изменилось, и применяет соответствующие действия
по корректировке (вызывает нужные команды SQL).
r Поддерживается одно автоинкрементное поле с именем id, которое автоматиче-
ски проставляется у записи при ее добавлении в таблицу. Указывать его в списке
несущих полей не надо.
r Имеется набор стандартных операций, которые можно производить с таблицей: ее
создание и удаление, вставка новой записи, обновление записи, удаление записей,
выборка указанного числа записей с сортировкой. Кроме того, поддерживаются
дополнительные операции, такие как подсчет числа записей, удовлетворяющих
запросу, и получение всех уникальных значений в указанном столбце таблицы.
r Для каждой таблицы можно хранить один дополнительный блок информации лю-
бой структуры (например, это может быть даже многомерный ассоциативный
массив). Выборка и запись этого блока осуществляются методами GetInfo() и
SetInfo(). Блок информации нельзя получить никак иначе, кроме как посредст-
вом этих двух функций (он "не виден" даже для функции выборки).
r Для убыстрения работы программист может назначить для тех или иных столбцов
таблицы режим индексирования (при использовании индекса MySQL тратит зна-
чительно меньше времени на поиск данных). Индексы, как и несущие поля, встав-
ляются и удаляются автоматически при изменении параметров вызова конструк-
тора. Помните, что хотя они и убыстряют работу, но зато занимают на диске
довольно много места.
У этого класса есть один небольшой недостаток, который заставляет применять его
аккуратно. Так как количества и размеры полей при вставке могут быть любыми, то
злоумышленник может быстро "забить" таблицу разного рода "мусором". Например,
если таблица используется как хранилище для гостевой книги, то он может видоиз-
менить форму отправки сообщения и вставить туда какое-нибудь текстовое поле,
предварительно поместив в него пару мегабайтов текста. Чтобы избежать этой
потен-
циальной "дыры" в защите, рекомендуется перед вставкой записи в таблицу прове-
рять, какой объем она занимает в сериализованном виде, и в случае превышения
оп-
ределенного числа байтов выводить предупреждение и завершать сценарий по die().
Думаю, читатель сам без труда добавит такую возможность в свои сценарии или же
прямо в класс MysqlTable.
Согласитесь, не так уж и мало для каких-то четырехсот строчек кода.….. Листинг
31.2 представляет собой исходный текст библиотеки, реализующей наш класс. Она
предполагает, что соединение с MySQL уже открыто и выбрана верная текущая база
данных.
Глава 31. Объектно-ориентированное программирование на PHP 467
Листинг 31.2. Полноценный класс MySQL-таблицы
// MysqlTable — "прозрачная работа" с таблицей MySQL.
// Класс MysqlTable обычно делают базовым для какого-нибудь
// другого класса (например, CGuestBook), и переопределяют
// нужные функции.
// Поле для хранения сериализованных полей (снаружи "не видно")
define("DataField","__data__");
//******************* Вспомогательные функции *******************
// Если переменная пуста, инициализирует ее
function Def0(&$st,$def) { if(!isSet($st)||$st=="") $st=$def; }
// Подготавливает строку двоичных данных для помещения в таблицу.
function Apostrophs(&$st)
{ $st=str_replace(chr(0),"\\0",$st);
$st=ereg_replace("\\\\","\\\\",$st);
$st=ereg_replace("'","\\'",$st);
return $st;
}
// Упаковывает объект и превращает его в строку.
function SqlPack(&$obj)
{ $s=Serialize($obj); return Apostrophs($s); }
// Распаковывает строку и создает объект.
function SqlUnpack(&$st) { return Unserialize($st); }
//****************************************************************
//*** Далее идет описание класса таблицы.
// Каждая запись таблицы, помимо тех полей, которые указаны в
// конструкторе, будет иметь еще два поля — id (уникальный
// идентификатор записи) и __data__ (упакованный массив
// всех остальных полей). Кроме того, в запись можно вводить
// произвольные поля — они тоже будут сохраняться, но по
// ним нельзя будет вести поиск (предложение "select"),
// потому что эти поля будут автоматически сериализованы при
// добавлении/изменении записи и распакованы при извлечении.
class MysqlTable {
//*** Внутренние переменные
var $TableName; // имя таблицы
var $UniqVars; // список уникальных полей (имя=1, имя=1...)
var $Index; // для этих полей построены индексы (имя=1, имя=1...)
Часть V. Приемы программирования на PHP 468
var $Fields; // все физические поля таблицы (имя=тип, имя=тип...)
var $Error; // текст последней ошибки ("", если нет)
var $JustCreated; // 1, если таблица была создана, а не загружена
//*** Внутренние функции
// Упаковывает поля массива в строку, за исключением тех, которые
// сами являются непосредственными полями в базе данных.
function _PackFields(&$Hash)
{ $Data=array();
foreach($Hash as $k=>$v) if($k!=DataField)
if(!isSet($this->Fields[$k])) $Data[$k]=$v;
return Serialize($Data);
}
// Виртуальная функция производного класса вызывается ПЕРЕД любым
// занесением данных в таблицу (добавлением и обновлением). То есть
// она предназначена для "прозрачной" автоматической генерации некоторых
// полей записи (например, времени ее изменения) в производном классе
// перед ее сохранением.
// Можно, к примеру, в таблице держать какую-нибудь дату в формате
// SDN, а "делать вид", что она хранится в обычном представлении
// "дд.мм.гггг".
// Если эта функция возвратит 0, то операция закончится с ошибкой.
function PreModify(&$Rec) { return 1; }
// Виртуальная функция вызывается ПОСЛЕ выборки записи из таблицы, а
// также в конце модификации записи. То есть она предназначена для
// "прозрачной" модификации только что полученной из таблицы записи.
// Возвращаясь к предыдущему примеру, мы можем при извлечении записи
// из таблицы STM-поле преобразовать в "дд.мм.гггг", и "никто ничего
// не заметит".
function PostSelect(&$Rec) { return; }
// Возвращает имя таблицы
function GetTableName() { return $this->TableName; }
// Возвращает результат запроса select. В дальнейшем этот результат
// (дескриптор) будет, скорее всего, обработан при помощи GetResult().
// $Expr — выражение SQL, по которому будет идти выборка
// $Order — правила сортировки (по умолчанию — по убыванию id)
function TableSelectQuery($Expr="",$Order="id desc")
{ $this->Error="";
if(!$Expr) $Expr="1=1";
$r=mysql_query("select * from ".$this->TableName.
Глава 31. Объектно-ориентированное программирование на PHP 469
" where ($Expr) and (id>1) order by $Order");
if(!$r) { $this->Error=mysql_error(); return; }
return $r;
}
function SelectQuery($Expr="",$Order="id desc")
{ return $this->TableSelectQuery($Expr,$Order); }
// Возвращает результат предыдущего запроса select (точнее, очередную
// найденную запись) в виде распакованного (!) массива. Если
// SelectQuery() нашла несколько записей, то, последовательно вызывая
// GetResult(), можно считать их все. Метод делает всю "черную" работу
// по сериализации. Еще раз: если у результата несколько строк, то метод
// возвращает очередную. Если строки кончились, возвращает "".
// Чаще всего в вызове этой функции (и функции SelectQuery) нет
// необходимости — можно воспользоваться методом Select(), который по
// запросу сразу возвращает массив со всеми обработанными результатами!
function TableGetResult($r)
{ $this->Error="";
// Выбираем очередную строку в виде массива
if($r) $Result=mysql_fetch_array($r);
else $this->Error=mysql_error();
if(!@is_array($Result)) return;
// Перебираем все поля таблицы и записываем их в массив $Hash
$Hash=array();
foreach($this->Fields as $k=>$i)
if(isSet($Result[$k])) $Hash[$k]=$Result[$k];
// Распаковываем поле с данными
$Hash+=SqlUnpack($Hash[DataField]); unSet($Hash[DataField]);
$this->PostSelect($Hash);
// Все сделано
return $Hash;
}
function GetResult($r) { return $this->TableGetResult($r); }
// Примечание: мы используем две функции, из которых GetResult()
// просто является синонимом для TableGetResult(), чтобы позволить
// производному классу вызывать функции MysqlTable, даже если они
// переопределены в нем. К сожалению, в PHP это единственный метод
// добиться цели.
// Аналог mysql_num_rows()
function GetNumRows($r) { return mysql_num_rows($r); }
Часть V. Приемы программирования на PHP 470
// Аналог mysql_data_seek(). После вызова этой функции указатель на
// дескриптор $r "перескочит" на найденную запись номер $to, после
// чего GetResult() ее и возвратит.
function DataSeek($r,$to) { return mysql_data_seek($r,$to); }
// Создает или загружает таблицу по имени $Name.
// $Fields — список полей базы. Именно по ним в дальнейшем можно
// будет вести поиск и строить индекс. Кроме того, в запись можно будет
// добавлять ЛЮБЫЕ другие переменные, но они будут сериализованы, а
// потом восстановлены. Формат списка: массив с ключами — именами
// переменных и значениями — их типами. Если $Fields — не массив, то
// считается, что таблица открывается такой, какой она есть. В противном
// случае производится проверка: не добавились или не удалились ли какие-
// то поля или индексы и, если это так, то выполняется соответствующая
// модификация таблицы (кстати, это процесс довольно длительный).
// ВНИМАНИЕ: если в таблице было какое-то поле, которое сериализуется, то
// в будущем при добавлении этого поля к $Fields оно НЕ будет
// автоматически переведено в ранг несущих, т. е. попросту
// пропадет (и наоборот).
// РЕКОМЕНДАЦИЯ: перечисляйте в $Fields те поля, для которых вы ТОЧНО
// уверены, что они будут всегда присутствовать в базе, а также те,
// по которым нужно будет вести поиск, строить индекс и использовать
// distinct.
// $Index — по каким полям нужно строить индекс. Индекс несколько
// увеличивает размер базы, но зато вырастает скорость поиска по ней
// (точнее, по тем полям, для которых используется индекс). Ключи — имена
// столбцов, значения — "размер" индекса (0, если по умолчанию, что чаще
// всего наиболее разумно)
function MysqlTable($Name,$Fields="",$Index="")
{ $this->TableName=$Name; $this->Error="";
if(is_array($Fields)) {
foreach($Fields as $k=>$v)
if(!eregi("not null",$v)) $Fields[$k]=$v." not null";
$Fields=array("id"=>"int auto_increment primary key")
+$Fields+array(DataField=>"mediumblob");
}
Def0($Index,array());
// Считываем из таблицы поле с ее параметрами
$this->Fields=array(DataField=>"mediumblob");
Глава 31. Объектно-ориентированное программирование на PHP 471
$Data=$this->TableGetResult(
mysql_query("select ".DataField." from $Name where id=1")
);
// Если таблица существует, то запрос окончится успешно.
// В этом случае нужно проверить, не изменилась ли таблица с момента
// последнего обращения, и если это так, то подкорректировать ее.
if(@is_array($Data)) {
if(!is_array($Fields)) {
$this->Error="Couldn't create table: no fields specified";
return;
}
Def0($Data["Fields"],array());
Def0($Data["Index"],array());
//** Возможно, что-то изменилось. Тогда выполняем alter table.
//1. Добавились поля?
$Lst=array();
foreach($Fields as $k=>$v) {
if(!isSet($Data["Fields"][$k])) $Lst[]="add $k $v";
else if($Data["Fields"][$k]!=$v) $Lst[]="change $k $k $v";
}
//2. Удалились поля?
foreach($Data["Fields"] as $k=>$v)
if(!isSet($Fields[$k])) $Lst[]="drop $k";
//3. Добавились индексы?
foreach($Index as $k=>$v) if(!isSet($Data["Index"][$k]))
$Lst[]="add index index_$k ($k".($v!=0?" ($v)":"").")";
//4. Удалились индексы?
foreach($Data["Index"] as $k=>$v)
if(!isSet($Index[$k])) $Lst[]="drop index index_$k";
if(count($Lst)) {
PrintDump($Lst);
if(!mysql_query("alter table $Name ".implode($Lst,","))) {
$this->Error=mysql_error();
return;
}
$Changed=1;
}
$this->JustCreated=0;
} else {
Часть V. Приемы программирования на PHP 472
// Необходимо создать таблицу.
// BugFix by DM: При создании новой таблицы необходимо очистить
// переменную Error, иначе в ней остается ошибка от попытки
// чтения полей.
$this->Error="";
$Lst=array();
foreach($Fields as $k=>$v) $Lst[]="$k $v";
foreach($Index as $k=>$v)
$Lst[]="index index_$k ($k".($v!=0?" ($v)":"").")";
if(!mysql_query("create table $Name (".implode($Lst,",").")")) {
$this->Error=mysql_error();
return;
}
$this->JustCreated=1;
}
// Сохраняем информацию о таблице, если она поменялась
if(!empty($Changed)||$this->JustCreated) {
$Data["Fields"]=$Fields;
$Data["Index"]=$Index;
Def0($Data["Info"],array()); // Информации не было — делаем пустой
$Data=SqlPack($Data);
if($this->JustCreated) {
$Result=mysql_query("insert into $Name(id,".DataField.")
values(1,'$Data')");
} else {
$Result=mysql_query("update $Name set ".DataField.
"='$Data' where id=1");
}
if(!$Result) { $this->Error=mysql_error(); return; }
}
$this->Fields=$Fields;
$this->Index=$Index;
}
// Записывает в таблицу информацию, общую для всей таблицы. Эта
// информация может быть получена потом только при помощи метода
// GetInfo(), и никак иначе. Например, если таблица используется для
// гостевой книги, мы можем сюда записывать какие-нибудь параметры этой
// книги — скажем, имя и пароль владельца. $Inf может быть чем угодно —
Глава 31. Объектно-ориентированное программирование на PHP 473
// даже массивом.
function TableSetInfo($Inf)
{ $this->Error="";
// Читаем информационную запись
$r=mysql_query("select ".DataField." from ".
$this->TableName." where id=1");
if(!($Data=$this->GetResult($r))) return;
// Устанавливаем поле Info
$Data["Info"]=$Inf;
$Data=SqlPack($Data);
// Сохраняем результат
if(!mysql_query("update ".$this->TableName.
" set ".DataField."='$Data' where id=1"))
{ $this->Error=mysql_error(); return; }
return 1;
}
function SetInfo($Inf) { return $this->TableSetInfo(&$Inf); }
// Возвращает информацию о таблице, ранее занесенную в нее при помощи
// SetInfo. Если информация не была занесена, возвращает пустой массив.
function TableGetInfo()
{ $this->Error="";
// Читаем информационную запись
$r=mysql_query("select * from ".$this->TableName." where id=1");
// Если что-то не в порядке, GetResult установит поле Error у объекта
if(!($Data=$this->GetResult($r))) return array();
if(!@is_array($Data["Info"])) $Data["Info"]=array();
return $Data["Info"];
}
function GetInfo() { return $this->TableGetInfo(); }
// Уничтожает таблицу. Осторожно! Таблица удаляется без всяких
// предупреждений!!!
function TableDrop()
{ $this->Error="";
if(!mysql_query("drop table ".$this->TableName)) {
$this->Error=mysql_error();
return 0;
}
return 1;
}
Часть V. Приемы программирования на PHP 474
function Drop() { return $this->TableDrop(); }
// Добавляет запись $Rec (обычно это ассоциативный массив с некоторыми
// установленными полями) в таблицу. Автоматически у нее проставляется
// id, а также проверяется, уникальны ли у записи те поля, которые должны
// быть уникальными (указываются в конструкторе). Возвращает 1 в случае
// успеха, при этом в $Rec содержится окончательно сформированная
// запись.
function TableAdd(&$Rec)
{ $this->Error="";
if(!$this->PreModify($Rec)) return 0;
// Иначе все в порядке. Добавляем запись.
$Rec[DataField]=$this->_PackFields($Rec);
// Составляем список имен полей и их значений
$LNames=$LVals=array();
foreach($this->Fields as $name=>$type) {
$LNames[]=$name;
$LVals[]="'".Apostrophs($Rec[$name])."'";
}
$LNames=implode($LNames,",");
$LVals=implode($LVals,",");
unSet($Rec[DataField]);
// Добавляем
if(!mysql_query("insert into ".$this->TableName.
"($LNames) values($LVals)"))
{ $this->Error=mysql_error(); return 0; }
$Rec["id"]=mysql_insert_id();
$this->PostSelect($Rec);
return 1;
}
function Add(&$Rec) { return $this->TableAdd(&$Rec); }
// Удаляет из таблицы записи, удовлетворяющие выражению $Expr.
// Например: $Tbl->Delete("(id=$id) or (id=0)");
function TableDelete($Expr)
{ $this->Error="";
if(!mysql_query("delete from ".$this->TableName.
" where ($Expr) and (id>1)"))
{ $this->Error=mysql_error(); return 0; }
return 1;
Глава 31. Объектно-ориентированное программирование на PHP 475
}
function Delete($Expr) { return $this->TableDelete($Expr); }
// Возвращает массив записей (ключ — id, значение — запись). В массив
// будет занесено не более $Num записей. Для каждой записи
// вызывается PostSelect()!
function TableSelect($Expr="",$Num=100000,$Order="id desc")
{ $this->Error="";
// Выполнить запрос
$r=$this->SelectQuery($Expr,$Order); if(!$r) return 0;
// Цикл по найденным записям
for($i=0,$Found=array(); $i<$Num&&($Rec=$this->GetResult($r)); $i++)
$Found[$Rec["id"]]=$Rec;
return $Found;
}
function Select($Expr="",$Num=100000,$Order="id desc")
{ return $this->TableSelect($Expr,$Num,$Order); }
// Обновляет запись в таблице, при этом запись $Upd изменяется и
// становится фактически такой, как она будет выглядеть после обновления.
// То есть к ней могут добавиться новые поля из таблицы. Если записи с
// таким id нет (когда $id не указан в параметрах, его значение берется
// равным $Upd["id"]), то генерируется ошибка!
// Возможно, в записи $Upd не задан идентификатор id (это бывает, если
// мы только что получили данные из формы). В этом случае можно этот
// идентификатор передать через $id.
// Итак, при обновлении id НЕ МЕНЯЕТСЯ по определению (в отличие от
// ДОБАВЛЕНИЯ, когда id всегда проставляется)!
function TableUpdate(&$Upd,$id=0)
{ $this->Error="";
// Если задан $id, то устанавливаем в записи этот идентификатор
if($id) $Upd["id"]=$id;
// Загружаем старую запись. Она должна быть одна.
$r=$this->SelectQuery("id=".$Upd["id"]);
$Rec=$this->GetResult($r);
// Если не удалось, значит, неверное обновление — записи
// еще не существует
if(!$Rec) { $this->Error="NotExists"; return 0; }
// Иначе все в порядке — добавляем. Сначала обновляем
// поля и упаковываем переменные
Часть V. Приемы программирования на PHP 476
$Rec=$Upd+$Rec; $Upd=$Rec;
if(!$this->PreModify($Rec)) return 0;
$Rec[DataField]=$this->_PackFields($Rec);
// Затем составляем список полей для обновления
$Lst=array();
foreach($this->Fields as $name=>$type)
$Lst[]="$name='".Apostrophs($Rec[$name])."'";
$Lst=implode($Lst,",");
// Выполняем запрос
if(!mysql_query("update ".$this->TableName.
" set $Lst where id=".$Rec["id"]))
{ $this->Error=mysql_error(); return 0; }
$this->PostSelect($Rec);
return 1;
}
function Update(&$Upd,$id=0) { return $this->TableUpdate(&$Upd,$id); }
// Возвращает число записей, удовлетворяющих выражению $Expr.
// Если $Expr не задано, возвращает число ВСЕХ записей.
function TableGetCount($Expr="")
{ $this->Error="";
if(!$Expr) $Expr="1=1";
$r=mysql_query("select count(if(($Expr) and (id>1),1,NULL)) from ".
$this->TableName);
if(!$r) { $this->Error=mysql_error(); return 0; }
$a=mysql_fetch_array($r);
return $a[0];
}
function GetCount($Expr="") { return $this->TableGetCount($Expr); }
// Возвращает СПИСОК всех уникальных значений поля $field
// в таблице, удовлетворяющих тому же условию $Expr.
// ВНИМАНИЕ: эта функция работает лишь тогда, когда поле $field
// присутствовало среди полей $Fields при вызове конструктора.
// В противном случае генерируется ошибка.
// Рекомендуется при создании таблицы для поля $field создать индекс.
function TableGetDistinct($field,$Expr="")
{ $this->Error="";
if(!$Expr) $Expr="1=1";
$r=mysql_query("select distinct $field from ".
Глава 31. Объектно-ориентированное программирование на PHP 477
$this->TableName." where ($Expr) and (id>1)");
// distinct НЕ работает вместе с order by! Почему — неясно...
if(!$r) { $this->Error=mysql_error(); return 0; }
for($Arr=array(),$i=0,$n=mysql_num_rows($r); $i<$n; $i++)
$Arr[]=mysql_result($r,$i,0);
return $Arr;
}
function GetDistinct($field,$Expr="")
{ return $this->TableGetDistinct($field,$Expr); }
}; // Конец класса
?>
А вот пример применения этого класса (листинг 31.3). Делает он следующее:
откры-
вает таблицу в некоторой базе данных (если таблицы с таким именем не существует,
создает ее) и добавляет одну пробную запись.
Листинг 31.3. Пример использования класса MysqlTable
include "librarian.phl"; // подключаем библиотекарь
Uses("MysqlTable"); // подключаем модуль с классом таблицы
// Устанавливаем соединение с базой данных
mysql_connect("localhost");
mysql_select_db("test");
// Открываем таблицу
$t=new MysqlTable("test",array("t"=>"int"));
// Добавляем запись
$d=array("t"=>time());
$t->Add($d);
// Работаем с блоком информации
$Inf=$t->GetInfo();
$Inf["a"]=@$Inf["a"]+1;
$Inf["b"]=@$Inf["b"]+10;
echo $Inf["a"]," ",$Inf["b"]," ";
$t->SetInfo($Inf);
// Выбираем все записи и выводим их
$d=$t->Select();
foreach($d as $id=>$Data) {
echo "$id: ".$Data['t']." ";
Часть V. Приемы программирования на PHP 478
}
?>
Попробуйте запустить этот сценарий (естественно, сделав так, чтобы ему был
досту-
пен библиотекарь), а затем понажимать кнопку Обновить в браузере. Вы должны
увидеть, что информация действительно накапливается в базе данных.
Копирование объектов
Так уж устроен PHP, что в нем все переменные, в том числе и объекты (а что
такое
объект, как не переменная определенного класса?), всегда рассматриваются как
про-
стой набор значений и копируются целиком. Например, если у нас есть громадный
массив $A и мы выполняем оператор $B=$A, то все содержимое $A будет скопировано
в $B один-в-один. Возможно, это как раз то, что и требуется, но вот с объектами
сложных классов все обстоит совсем иначе. Предположим, например, что мы выпол-
нили команды:
$Obj1=new MysqlTable("test");
$Obj2=$Obj1;
$Obj1->Drop();
Объект-таблица $Obj1 благополучно уничтожится и пометит в своих свойствах, что
он
уничтожен, и больше использоваться не должен, но вот $Obj2 об этом и не
"догадается".
$Obj2 по-прежнему будет "считать", что он — "единственный и неповторимый"
объект,
привязанный к существующей таблице test, и будет честно пытаться выполнить с
ней
какие-то операции по запросам.
Этого, к сожалению, нельзя избежать в PHP. А именно, мы не можем никак контро-
лировать процесс копирования объектов. И в этом — безусловная слабость PHP. Так
что будьте особенно бдительны.
Ссылки и интерфейсы
Как мы знаем, в PHP оператор присваивания всегда копирует значения переменных,
какой бы сложной структуры они ни были. Это же, напомню, происходит и с объек-
тами. Что тогда получится, если мы скопируем, например, объект класса
MysqlTable? Вообще говоря, ничего хорошего. Произойдет дублирование всех
свойств и методов объекта. Фактически, мы получим сразу две независимые
"обертки" для одной и той же таблицы MySQL. Таким образом, изменения, внесен-
ные в первый объект, никак не повлияют на второй, и наоборот.
Я специально проектировал класс MysqlTable так, что даже после копирования объ-
ектов этого типа не происходило никаких фатальных недоразумений описанного вы-
ше рода. Однако так можно сделать далеко не всегда. Представьте, например, что
Глава 31. Объектно-ориентированное программирование на PHP 479
нам приходится очень часто использовать функцию GetInfo() и довольно редко —
SetInfo(). Так как GetInfo() при каждом запросе обращается к MySQL, мы мо-
жем получить здесь ощутимый проигрыш в быстродействии. Очевидное решение за-
ключается в промежуточном хранении данных, возвращаемых нашим "обычным"
методом GetInfo() в специальном свойстве объекта. Действительно, зачем загру-
жать сервер лишней работой по чтению одних и тех же данных, когда можно хранить
их в программе и сразу же использовать? Это свойство будет инициализироваться
при конструировании объекта класса MysqlTable и обновляться каждый раз при
обращении к методу SetInfo().
То есть наше свойство будет представлять собой аналог "зеркала" записи в
таблице MySQL, по аналогии с "зеркалами" сайтов в Интернете. Класс
MysqlTable должен следить за тем, чтобы оно всегда содержало актуальные
данные — те же самые, что и в реальной таблице.
Но, к сожалению, описанная схема не может быть реализована в PHP напрямую, и
именно по причине обязательного полного копирования переменных. Вот пример,
который породит ошибку:
$t1=new MysqlTable("MyTable");
. . .
function DoIt($t)
{ $t->SetInfo("This is the new info!");
}
. . .
$t=new MysqlTable("MyTableName");
$t->SetInfo("Data");
DoIt($t);
$Inf=$t->GetInfo(); // в $Inf будет строка Data!
Впрочем, в приведенном только что фрагменте это недоразумение можно легко пре-
одолеть, передав функции ссылку на объект:
function DoIt(&$t)
{ $t->SetInfo("This is the new info!");
}
Я намеренно привел здесь пример, когда ограничение на копирование объектов все
же можно обойти относительно безболезненно. Настало время описать неразрешимую
(во всяком случае, похожим методом) задачу. Но прежде обратите внимание, что в
нашем примере объект передается "вглубь" кода (внутрь функции), а не "наружу"
(из
функции). Вот как раз в последнем случае и будет возникать неразрешимая
проблема.
Часть V. Приемы программирования на PHP 480
Но обо всем по порядку. Чтобы чуть сгустить краски и не вдаваться в абстрактные
рассуждения, давайте предположим, что наш класс MysqlTable вообще не допускает
копирования его объектов, а при случайном выполнении такого копирования
работает
совершенно неправильно. Нужно заметить, что это не так уж и далеко от истины,
особенно если мы используем MysqlTable не напрямую, а как базовый для какого-то
другого типа (например, для класса форума).
Мы знаем, что в таком случае объекты этого класса можно передать без побочных
эффектов внутрь функций по ссылке. Сейчас мы остановимся на обратном процессе.
Итак, пусть мы написали более-менее универсальный модуль, в котором есть
единст-
венная интерфейсная функция OpenTable(), создающая новую таблицу в базе дан-
ных и, соответственно, новый объект класса MysqlTable. Специфика этой функции в
том, что в случае, если таблица существует, новый объект не создается, а
возвращает-
ся уже имеющийся. Иными словами, для двух вызовов функции с одинаковыми па-
раметрами должен быть возвращен один и тот же объект, а не две его копии.
Возможно, вы спросите: зачем нам вообще такая функция, когда можно вос-
пользоваться оператором new напрямую? Тогда еще раз перечитайте предпо-
следнюю фразу предыдущего абзаца: "В случае, если таблица уже существует,
новый объект не создается". В то же время оператор new всегда создает но-
вый объект, что нам, конечно, не подходит. Ведь мы договорились никогда не
иметь в программе двух разных объектов, связанных с одной и той же табли-
цей.
Легко сказать — "возвращает уже существующий объект", но несколько сложнее —
реализовать это. Рассмотрим два различных способа, с помощью которых мы можем
достичь цели.
Как следует из законов Мэрфи, "у любой сложной задачи всегда имеется одно
простое, красивое и легкое для понимания… неправильное решение". В нашем
случае это будет возврат из функции объекта класса MysqlTable "обычным"
способом, подразумевающим копирование. Но ведь, по имеющейся между на-
ми договоренности, объекты этого класса нельзя копировать!
Возврат ссылки на объект
Первый прием связан с новой возможностью PHP версии 4 — ссылочными перемен-
ными. Помните, в части III этой книги мы говорили, что функция может возвращать
ссылку на переменную (объект), а не только копию переменной?.. В нашем случае
это
оказывается довольно удобно. Вот как могла бы выглядеть функция OpenTable() и
использование для нее ссылок (листинг 31.4):
Глава 31. Объектно-ориентированное программирование на PHP 481
Листинг 31.4. Использование ссылок
// Массив всех уже открытых таблиц. Ключи — имена таблиц, значения —
// соответствующие объекты.
$Tables=array();
. . .
// Функция OpenTable() возвращает ссылку на объект, соответствующий
// таблице MySQL с заданным именем. Копии объектов не создаются.
function &OpenTable($name,$Fields="")
{ global $Tables;
if(!Isset($Tables[$name]))
$Tables[$name]=new MysqlTable($name,$Fields);
return $Tables[$name];
}
. . .
// Вот так мы должны использовать эту функцию.
$Tbl1=&OpenTable("MyTable"); // создает новый объект
$Tbl2=&OpenTable("OtherTable"); // создает объект
$TblEqualsTo1=&OpenTable("MyTable"); // возвращает имеющийся объект!
// Теперь $Tbl1 и $TblEqualsTo1 ссылаются на один и тот же объект.
// То есть изменение $Tbl1 тут же отразится на $TblEqualsTo1,
// и наоборот.
Опытный программист сразу же заметит в подходе предыдущего примера два значи-
тельных недостатка. Оба они связаны с несовершенством механизма управления
ссылками в PHP.
r Если пропустить перед вызовом функции оператор & (взятие ссылки), то функция
вернет не ссылку на объект, а копию этого объекта. При этом программа не вы-
даст никакого предупреждения и, скорее всего, будет даже работать верно — до
тех пор, пока для копии объекта не будет вызван метод, ради которого мы и хоте-
ли избежать копирования. Вообразите себе муки программиста, отлаживающего
такую программу, которая отказалась правильно работать по этой причине — ведь
& может быть пропущен очень далеко от того места, где возникла ошибка!
r У неопытного программиста, использующего ваш класс, может возникнуть иску-
шение скопировать $Tbl1 в новую переменную "обычным" образом — при по-
мощи оператора =. Или же он может по ошибке пропустить &, когда объявляет
функцию со ссылочным параметром.
Мы видим, что два указанных недостатка приводят к тому, что программу
становится
очень трудно отлаживать. А такие программы, как показал многолетний опыт про-
граммирования, не только никуда не годятся — они приносят разработчику лишь
огорчения, сокращая его век.
Часть V. Приемы программирования на PHP 482
Есть ли альтернатива ссылкам? Оказывается, есть. Правда, она сопряжена с
больши-
ми сложностями при разработке классов, но зато полностью лишена недостатков,
описанных выше. Это — фактическое отделение набора методов, отвечающих за
взаимодействие с объектом класса (то есть интерфейса класса) от его реализации.
Возврат интерфейса
Поговорим немного о том, что же собой представляют интерфейсы в объектно-
ориентированном программировании. Это понятие довольно сложное, и о нем напи-
сано множество томов. Я, разумеется, не собираюсь их здесь пересказывать,
потому
что эта книга — о PHP, а не об идеологии ООП.
Интерфейсы — главная "изюминка" практически всех сложных объектно-
ориентированных систем (например, COM+, CORBA) и одно из основных понятий
такого языка, как Java. Язык C++ также во всем поддерживает эту идеологию. Что
же
может дать нам PHP в этом отношении? К сожалению, довольно немного. И все-таки
даже этого хватает, чтобы избавиться от недостатков, присущих ссылкам в PHP —
во
всяком случае, для нашей задачи.
Психологи утверждают, что яркие ассоциации запоминаются особенно хорошо. Что
ж, проверим. Помните, когда мы были маленькими детьми, всем нам рассказывали
сказки. Почему бы не заняться этим вновь? Как считаете, а?.. Ну и прекрасно
(хотя я,
право, не могу знать наверняка, что вы ответили). В скобках я буду давать
коммента-
рий, ведущий параллельную линию повествования. Итак, закроем глаза и представим
себе большого кита (объект большого и сложного класса, например, MysqlTable),
лениво плавающего по просторам океана (расположенного в оперативной памяти).
Мы не настолько смелы, чтобы приблизиться к этому киту на достаточно близкое
расстояние и дотронуться до него (не хотим использовать свойства или методы
объекта напрямую). Если уж быть честными, мы даже не видим этого кита (не мо-
жем напрямую использовать в программе этот объект) — он слишком далеко (на
него нет ссылок), и уж подавно не можем его сдвинуть с места (скопировать
объект
в другую переменную). Но зато, как мы знаем, его постоянно сопровождают рыбы-
прилипалы (объекты-интерфейсы), маленькие и юркие (имеющие код небольшого
размера), которые иногда заплывают достаточно далеко, чтобы мы могли с ними
взаимодействовать. Этих рыб нам удалось выдрессировать, так что теперь они
могут
передавать киту любые наши приказы (передавать запросы на обслуживание) —
разумеется, из тех, что сами понимают (для которых имеются соответствующие
методы). Конечно, к киту могут "приклеиваться" рыбы-прилипалы различных видов
и по-разному дрессированные (объект может иметь несколько разных интерфей-
сов). Важно то, что мы не можем взаимодействовать с китом никак иначе, кроме
как
посредством этих рыб-прилипал (не можем напрямую использовать объект). При
этом мы имеем право совершенно свободно разводить прилипал в неволе (копиро-
вать объекты-интерфейсы), ведь киту (главному объекту) нет до этого ровным сче-
том никакого дела (в PHP объект "не знает", сколько у него интерфейсов и как
они
используются).
Глава 31. Объектно-ориентированное программирование на PHP 483
Вроде бы понятно, не правда ли? А теперь давайте уберем все, кроме слов-
связок, но оставим курсив. Вот что у нас получится. "Представим себе объект
большого и сложного класса, например, MysqlTable, расположенный в опе-
ративной памяти. Мы не хотим использовать свойства или методы объекта на-
прямую. Если уж быть честными, мы даже не можем напрямую использовать в
программе этот объект — на него нет ссылок, и уж подавно не способны ско-
пировать объект в другую переменную. Но зато, как мы знаем, его постоянно
"сопровождают" объекты-интерфейсы, имеющие код небольшого размера. Эти
интерфейсы могут передавать запросы на обслуживание — разумеется, из тех,
для которых имеют соответствующие методы. Конечно, объект может иметь
несколько разных интерфейсов. Важно то, что мы не можем напрямую исполь-
зовать объект. При этом мы имеем право копировать объекты-интерфейсы —
главному объекту нет до этого ровным счетом никакого дела. В PHP объект "не
знает", сколько у него интерфейсов и как они используются".
Итак, основная идея такова: отделим интерфейс MysqlTable от его реализации, т.
е.
напишем класс IMysql, с которым и будем всегда работать. Этот класс должен со-
держать все те методы, которые поддерживаются MysqlTable, только заниматься
они будут ни чем иным, как просто переадресацией вызовов на "настоящие" объекты.
А последние, в свою очередь, хранятся в глобальном массиве объектов, на
элементы
которого должно ссылаться одно из свойств IMysql. Реализуем эту стратегию для
упрощенной версии MysqlTable, имеющей только метод Drop() и конструктор
(листинг 31.5):
Листинг 31.5. Упрощенный интерфейс к таблице MySQL
// Массив объектов-таблиц, созданных в программе
$GLOBALS["Tables"]=array(); // вначале массив пуст
// Реализация класса. Это — обычный класс без каких-либо особенностей.
// Давайте предположим, что объекты этого класса недопустимо
// копировать обычным способом.
class MysqlTable {
// . . .
function MysqlTable($name) { echo "MysqlTable($name) "; }
function Drop() { echo "Drop() "; }
}
// Класс-интерфейс
class IMysql {
var $id; // идентификатор реализации таблицы (MysqlTable) в $Tables
// Открывает таблицу с именем $name. Если эта таблица уже была
// открыта ранее, то ничего не делает и просто становится ее
// синонимом, иначе создает экземпляр объекта.
Часть V. Приемы программирования на PHP 484
function IMysql($name)
{ global $Tables;
$this->id=$name;
// Если объект для таблицы $name еще не создан, создать его
if(!isset($Tables[$name])) $Tables[$name]=new MysqlTable($name);
// Иначе объект уже существует и ничего делать не надо
}
// Уничтожает таблицу. Переадресуем вызов реализации
function Drop() { $obj=&$GLOBALS['Tables'][$this->id]; $obj->Drop(); }
}
// Демонстрация работы с интерфейсом
$m=new IMysql("TestTable"); // объект создается
$m=new IMysql("TestTable"); // новый объект не создается!
$m->Drop(); // очищается единственный объект
Откровенно говоря, мы реализовали здесь не совсем то, что в объектно-
ориентированном проектировании принято называть "интерфейсом". По опре-
делению интерфейс не может иметь конструктора, класс же IMysql его имеет.
Так что слово "интерфейс" здесь, мягко говоря, не подходит, но я буду назы-
вать класс IMysql именно так — для краткости. Думаю, в этом нет ничего
страшного — такова уж специфика PHP, и это самое простое, что можно было
бы предложить. В самом деле, не писать же на PHP специальные "классы-
фабрики", занимающиеся исключительно созданием объектов, как это принято
в ООП…
Таким образом, как при копировании, так и при создании объекта-таблицы, который
был уже ранее создан в программе, новый экземпляр объекта не создается. Иными
словами, мы можем иметь сколько угодно объектов класса IMysql, ссылающихся на
одну и ту же таблицу, и при изменении одного из них это "почувствуют" и все ос-
тальные. Нужно только грамотно реализовать все переадресующие функции.
И еще насчет класса-реализации: лучше всего дать ему какое-нибудь некрасивое
имя
(например, __MysqlTableImpl__), чтобы какой-нибудь неопытный пользователь
случайно не стал к нему обращаться напрямую, а не через IMysql.
Хочу заметить, что в настоящих объектно-ориентированных языках нет причин при-
бегать к столь странным ухищрениям, потому что в них есть такое понятие, как
ука-
затель. В этих языках подобъект класса-интерфейса IMysql содержится прямо внут-
ри объекта MysqlTable, и указатель на него можно получить либо посредством
явных преобразований типов, либо с помощью специальных функций для "отпочко-
вывания" интерфейса. Например, в COM+ эти функции часто называют
QueryInterface(). Здесь же у нас вышло нечто вроде примитивной поддержки
Глава 31. Объектно-ориентированное программирование на PHP 485
указателей (ведь объект класса IMysql именно указывает на "хозяина" типа
MysqlTable, но не содержит его в себе!), которых в PHP нет.
Правда, получилось все это несколько неказисто (уж очень некрасивы и одинаковы
функции переадресации...), зато механизм действительно работает и решает все
по-
ставленные задачи.
Глава 32
Почтовые
шаблоны
В главе 20 мы уже обсуждали задачу создания универсальной функции для рассылки
писем из PHP-сценария. Если вы помните, мы хотели назвать ее PostMail() и "нау-
чить" перекодировать письма в нужную кодировку перед их отсылкой, а также вы-
полнять функции небольшого шаблонизатора. В этой главе мы детально рассмотрим,
как может быть устроена такая функция.
Мини-шаблонизатор
Конечно, пользователю будет приятно, если письмо (пусть даже и сгенерированное
программой) будет адресовано ему лично. Например, в поле From содержится фами-
лия и имя клиента, а первые строки текста звучат как-нибудь вроде: "Уважаемый
ФИО!". Так что нам придется формировать текст письма "на лету" — проставлять в
нем нужное имя, фамилию, тему и т. д. по общему шаблону.
В идеале такой шаблон должен ничем не отличаться от небольшого PHP-сценария с
тэгами и ?> и возможностью использования команды echo или print, не говоря
уж о всех остальных инструкциях. Но вот беда: как нам этот самый шаблон
"развер-
нуть", превратить в письмо-строку, которую потом мы будем посылать по почте?
Пусть, например, у нас есть следующий шаблон письма (разделителем заголовков и
тела письма служит маркер ~StartOfMail, обрабатываемый функцией
PostMail()):
To: "=$Name?>" <=$email?>>
Subject: =$Subject?>
~StartOfMail
Дорогой =$Name?>!
Только что Вы подписались на наш лист рассылки.
Пожалуйста, подтвердите свое желание получать новости нашего сайта.
Если бы мы писали сценарии на PHP версии 3, задача обработки такого шаблона бы-
ла бы практически невыполнимой. К счастью, при использовании PHP версии 4 все
проще: в нем имеются функции "перехвата" стандартного выходного потока (о них
мы уже говорили в главе 30 ).
Глава 32. Почтовые шаблоны 487
Давайте начнем проектирование функции PostMail() с написания своеобразного
"мини-шаблонизатора" — функции, которая умеет "разворачивать" шаблоны наподо-
бие приведенного выше, возвращая окончательный текст. Назовем ее, к примеру,
ExpandTemplate() (листинг 32.1). Думаю, будет целесообразно вынести данную
функцию в отдельную библиотеку, потому что она достаточно универсальна для это-
го.
Листинг 32.1. Функции обработки шаблонов: Minitemplate.phl
// Эта функция используется для внутренних целей. Она возвращает
// "развернутый" шаблон $templ. Перед обработкой создаются переменные,
// имена которых содержатся в ключах массива $Vars, а значения — в
// соответствующих значениях массива. Если $Vars===false, то вместо
// него используется массив $GLOBALS (то есть делаются доступными все
// глобальные переменные). Значение параметра $ReadFile "истина"
// указывает, что в $templ хранится не содержимое шаблона, а имя файла,
// из которого его можно получить.
// Замечание: параметр $Vars передается по ссылке, т. к. для
// массивов передача ссылки работает значительно быстрее, чем
// копирование.
function _RunTemplate($tmpl, $ReadFile, &$Vars)
{ // Перехватываем стандартный поток вывода
ob_start();
// Если $Vars опущен, использовать вместо него $GLOBALS. Мы
// используем ссылки для убыстрения работы, чтобы PHP не пришлось
// копировать значения, чем экономим время.
if($Vars===false) $Vars=&$GLOBALS;
// Делаем доступными коду шаблона все переменные. Также создаем
// ссылки из соображений производительности.
foreach($Vars as $k=>$v) $$k=&$Vars[$k];
// Включаем файл по include, либо же запускаем eval().
if($ReadFile) { include $tmpl; }
else eval("?>$tmpl;");
// Получаем содержимое буфера и закрываем его
$MTResult=ob_get_contents();
ob_end_clean();
// Возвращаем развернутый шаблон
return $MTResult;
}
// Функция "разворачивает" шаблон, тело которого расположено
Часть V. Приемы программирования на PHP 488
// в файле $fname. Перед запуском переменные из $Vars делаются
// доступными шаблону (если этот параметр не опущен).
function ExpandFile($fname,$Vars=false)
{ return _RunTemplate($fname,true,$Vars);
}
// Функция "разворачивает" тело шаблона, явно заданное в $tmpl.
// Рекомендуется везде, где можно, применять ExpandFile() вместо
// данной функции, потому что это упрощает отладку.
function ExpandTemplate($tmpl,$Vars=false)
{ return _RunTemplate($tmpl,false,$Vars);
}
?>
Зачем нам две различных функции для "раскрытия" шаблона —
ExpandTemplate() и ExpandFile()? Почему бы не использовать всегда
ExpandTemplate(), предварительно загружая тело шаблона с помощью
функций чтения файлов? Все дело в тонкостях обработки ошибочных ситуаций
в PHP. А именно, в случае ошибки внутри файла, загружаемого по include,
PHP сообщит нам имя этого файла. Если же ошибка произойдет в eval(),
выведется только номер строки, что сильно затруднит отладку. Поэтому реко-
мендуется везде, где это допустимо, вызывать функцию ExpandFile().
Отправка и перекодирование писем
Приступим ко второй части нашей задачи — напишем функцию PostMail(), кото-
рая будет отправлять письмо адресату, преобразовав его предварительно в нужную
кодировку. Вот какие возможности она будет обеспечивать:
r вставку заголовка From в письмо, если он еще не присутствует в сообщении;
r преобразование письма в нужную кодировку кириллицы;
r вставку соответствующего значения в заголовок Content-type, чтобы письмо
было "понятно" любой почтовой программе;
r поддержку функций мини-шаблонизатора, который мы уже написали.
В листинге 32.2 приведен исходный код функции. Как обычно, мы помещаем функ-
цию в отдельный модуль библиотекаря (библиотекарь описан в главе 29). Этот мо-
дуль будет использовать возможности, предоставляемые библиотекой
Minitemplate.phl.
Глава 32. Почтовые шаблоны 489
Листинг 32.2. Функция PostMail(): Mail.phl
Uses("Minitemplate");
// Кодировка по умолчанию для исходного текста.
define("DefaultCode","w");
// Функция возвращает строку $st, переведенную из кодировки
// $from в кодировку $to. Возможные значения этих параметров:
// w[indows] — windows-1251
// k[oi8-r] — koi8-r
// m[ac] — x-mac-cyrillic
// i[so] — iso-8859-5
// t[ranslit] — translit ("английскими" буквами — "русские" слова)
// Замечание: квадратными скобками помечены необязательные символы.
// параметр $from не может равняться "t", потому что трудно
// восстанавливать текст из транслита (хотя эта задача и разрешима).
// Функция полезна и сама по себе, но все-таки чаще всего ее
// применяют для работы с почтой. Именно поэтому я включаю
// ее в этот модуль.
function EncodeString($st,$to,$from=DefaultCode)
{ // Оставляем только первые буквы названий кодировок
$from=strtolower(substr($from,0,1));
$to =strtolower(substr($to,0,1));
// Пытаемся воспользоваться встроенной в PHP функцией
if($to!="t") return convert_cyr_string($st,$from,$to);
// Иначе нужно преобразовать строку в Translit, что придется
// делать "вручную" — при помощи strtr().
// Сначала заменяем "односимвольные" фонемы.
$st=strtr($st,"абвгдеёзийклмнопрстуфхъыэ",
"abvgdeeziyklmnoprstufh'ie");
$st=strtr($st,"АБВГДЕЁЗИЙКЛМНОПРСТУФХЪЫЭ",
"ABVGDEEZIYKLMNOPRSTUFH'IE");
// Затем — "многосимвольные".
$st=strtr($st,array(
"ж"=>"zh", "ц"=>"ts", "ч"=>"ch", "ш"=>"sh",
"щ"=>"shch","ь"=>"", "ю"=>"yu", "я"=>"ya",
"Ж"=>"ZH", "Ц"=>"TS", "Ч"=>"CH", "Ш"=>"SH",
Часть V. Приемы программирования на PHP 490
"Щ"=>"SHCH","Ь"=>"", "Ю"=>"YU", "Я"=>"YA"
));
// Возвращаем результат.
return $st;
}
// Значения параметра Content-tyep charset в зависимости от
// односимвольного названия кодировки.
global $CoderCharset;
$CoderCharset["w"]="windows-1251";
$CoderCharset["i"]="iso-8859-5";
$CoderCharset["k"]="koi8-r";
$CoderCharset["m"]="x-mac-cyrillic";
$CoderCharset["t"]="koi8-r";
// Разделитель тела и заголовков (таких как From: и т. д.) в письме.
define("MailDivider","~StartOfMail");
// Посылает письмо $msg по заданному адресу $to, перед этим
// преобразовав его в кодировку $encTo. Проставляет поле
// charset и правильно обрабатывает имя получателя (если
// в теле письма уже указано "To: Вася", то в результате
// получается "To: Вася "). Если работа происходит
// в Win32, то письмо не посылается, а создается отладочный файл,
// в котором будет содержаться текст письма.
// Письмо должно состоять из заголовков и тела, разделенных
// маркером ~StartOfMail.
function SendMail($to,$msg,$encTo=DefaultCode,$encFrom=DefaultCode)
{ global $CoderCharset;
// Перекодируем
$msg=EncodeString($msg,$encTo,$encFrom); // тело письма
$head=""; // заголовки
// Если есть заголовки, выделяем их.
if(strpos($msg,MailDivider)!==false) {
$regs=split(MailDivider."\r?\n?",$msg,2); // тело и заголовки
$head=trim($regs[0]);
$msg=$regs[1];
}
Глава 32. Почтовые шаблоны 491
// Работаем с заголовками. Разбиваем их на строки.
if($head) $Lines=split("[\r\n]+",$head); else $Lines=array();
$HasContType=0; // число найденных заголовков Content-type
$chs="charset=$CoderCharset[$encTo]";
$subject="";
for($i=0; $i";
$l="";
}
// Проверяем заголовок Subject. В некоторых верcиях PHP
// передача пустого второго параметра в функцию mail()
// приводит к нежелательным последствиям. Указывая в заголовке
// значение Subject из письма, мы решаем проблему.
if(eregi("^subject:([^\r\n]*)",$l,$regs)) {
$subject=trim($regs[1]);
}
}
// Нет заголовка Content-type — добавляем его в конец.
if(!$HasContType) $Lines[]="Content-type: text/plain; $chs";
// Соединяем строки опять вместе.
$head=ereg_Replace("\n\n+","\n",join("\n",$Lines));
// Посылаем письмо.
$Result=@mail($to,$subject,$msg,$head)!=0;
Часть V. Приемы программирования на PHP 492
// В Windows параллельно ведем журнал писем (для отладки).
if(getenv("COMSPEC")) {
if(!@is_dir("debug")) mkdir("debug",0755);
$f=fopen("debug/_debug_mail.txt","a+");
fputs($f,"> to: $to\n");
fputs($f,"$head\n--------\n");
fputs($f,"$msg\n-----------------------------------------\n\n");
fclose($f);
}
return $Result;
}
// Функция PostMail() "разворачивает" шаблон $msg, делая доступным для
// него переменные из массива $Vars (см. описание функций
// ExpandTemplate() и ExpandFile()). Затем она переводит результирующий
// текст в кодировку, заданную в $encTo (сам текст при этом
// рассматривается в кодировке $encFrom), и посылает его по электронной
// почте по адресу $to. Если строка $msg начинается с префикса
// file:, за которым следует имя файла, то шаблон письма загружается из
// этого файла при помощи ExpandFile(). В противном случае в качестве
// шаблона рассматривается сам параметр $msg.
function PostMail($to,$msg,$encTo=DefaultCode,
$Vars=false,$encFrom=DefaultCode)
{ if(eregi("^file:(.*)(\n|\$)",$msg,$P))
$Text=ExpandFile(trim($P[1]),$Vars);
else
$Text=ExpandTemplate($msg,$Vars);
// Посылаем письмо.
return SendMail($to,$Text,$encTo,$encFrom);
}
?>
Отличительной особенностью функции EncodeString() (а также всех остальных
почтовых функций) является то, что она умеет перекодировать текст в транслит.
Термин "транслит" (сокращение от "транслитерация") означает такую кодиров-
ку кириллицы, при которой все "русские" буквы контекстно заменяются на за-
писанные в соответствии с английской транскрипцией. Например, vot stroka,
Глава 32. Почтовые шаблоны 493
zapisannaya translitom. Эта кодировка особенно полезна для пользователей
Unix, которые забыли установить у себя "русскую" таблицу символов.
Пример
Напоследок рассмотрим пример применения описанных выше функций. Предполо-
жим, в некотором текстовом файле хранится список подписчиков, каждая строка ко-
торого оформлена в следующем формате:
Имя_подписчика|адрес|timestamp_подписки|кодировка_письма
Напишем сценарий, который будет посылать каждому подписчику из этой простей-
шей базы данных "личное" письмо с самыми последними новостями сайта. Предпо-
ложим для простоты, что эти новости в программе уже сохранены в массиве $News.
Для начала создадим шаблон письма (листинг 32.3):
Листинг 32.3. Шаблон "личного" письма: mail.txt
Content-type: text/plain
From: Система рассылки
To: =$User['name']?>.
Subject: Свежие новости
Content-type: text/plain
~StartOfMail
Уважаемый =$User['name']?>!
Вы подписались на наш лист рассылки =date("d.m.Y",$User['time'])?>.
Предлагаем Вашему вниманию последние новости.
---------------------------------------------------------------
$v) {?>
=WordWrap($v,60)?>.
}?>
Как видим, шаблон практически ничем не отличается от небольшого сценария на
PHP. Он получает данные из переменных $User (данные пользователя) и $News
(блоки новостей), которые должны устанавливаться запускающей программой. Вско-
ре мы рассмотрим процедуру более подробно, а пока обратите внимание на некото-
рые моменты при написании этого шаблона.
r Мы указали заголовок Content-type сразу в двух местах шаблона — в начале и
конце. В силу рассуждений, приведенных в главе 20, это необходимо для того,
чтобы помочь некоторым "недогадливым" почтовым программам в определении
кодировки письма.
Часть V. Приемы программирования на PHP 494
r Заметьте, что в конце заголовка To стоит точка. Зачем она нужна? Дело в том,
что закрывающий тэг PHP ?>, если он занимает последние символы строки, нико-
гда не генерирует знака перевода строки \n. Это, видимо, сделано для того,
чтобы
уменьшить количество пустых строк в страницах, которые создает интерпретатор.
В нашем случае отсутствие разделителя может сильно помешать, если не поста-
вить после тэга ?> какой-нибудь знак. Вообще-то, лучше здесь использовать про-
бел, но в листинге он был бы совершенно незаметен, — вот почему я и выбрал
точку.
r Наконец, чтобы каждая строка новостей, которые получит пользователь, была не
длиннее 60 символов, мы задействуем встроенную в PHP функцию WordWrap(). Под-
робнее о ней можно прочитать в главе 12 настоящей книги.
В листинге 32.4 приведен код, который, собственно, и занимается рассылкой писем.
Листинг 32.4. Код рассылки писем
// Подключаем библиотекаря "прямым" способом.
include "$DOCUMENT_ROOT/php/Librarian.phl";
// Подключаем модуль с функцией PostMail()
Uses("Mail");
// . . .
// Здесь мы должны генерировать массив $News,
// содержащий блоки последних новостей.
// . . .
// Открываем базу данных с подписчиками. Ее формат был
// рассмотрен нами ранее.
$F=File("db.txt");
foreach($F as $s) {
$User=explode("|",trim($s));
// Для удобства создаем для каждого значения ключи.
$User=array(
"name" => $User[0],
"email" => $User[1],
"time" => $User[2],
"encode" => $User[3]
);
// Посылаем письмо по шаблону из файла mail.txt
// очередному пользователю, переводя его в желаемую кодировку.
Глава 32. Почтовые шаблоны 495
PostMail($User['email'],"file:mail.txt",$User['encode']);
}
?>
Этот код довольно красноречиво показывает, что работать с нашей новой функцией
PostMail() очень просто. Большая его часть занимается не отправкой писем, а
раз-
бором записей в базе данных. Так как переменные $User и $News — глобальные, то
не нужно предпринимать никаких дополнительных действий, чтобы использовать их
в шаблоне письма.
На этом мы завершим рассмотрение возможностей PHP по отправке электронной
почты и разбору шаблонов писем. Я не затронул здесь тему, касающуюся включения
в письма так называемых attachment'ов (или "вложенных файлов"), потому что в
формате писем, содержащих "вложения", довольно легко запутаться. Любознатель-
ный читатель всегда сможет добавить в модуль Mail.phl функции, позволяющие
удобно работать с "вложениями". Для того чтобы разобраться с форматом таких пи-
сем, можно даже не искать соответствующую документацию: достаточно просто по-
смотреть на исходный текст письма, сгенерированного какой-нибудь почтовой про-
граммой, и уловить закономерности размещения заголовков и блоков текста.
Глава 33
Разные советы
В этой небольшой завершающей главе сведены воедино некоторые советы и приемы
программирования сценариев, которым не было удалено достаточного внимания в
остальных главах книги.
Разделенные вычисления
Большинство хостинг-провайдеров ставят ограничения на то время, в течение
которо-
го могут выполняться сценарии пользователя. Иными словами, если выполнение про-
граммы занимает более определенного времени (например, 10 секунд), она прерыва-
ется принудительным образом. Минимальный квант времени задается в файле
конфигурации php.ini. Как правило, его хватает для большинства программ, но все
же существуют Web-приложения, требующие длительной работы.
Одним из таких приложений является автоматически генерируемая карта сервера.
Она может представлять собой обычный сценарий на PHP, который рекурсивно обхо-
дит все каталоги сервера и собирает информацию о файлах, которые в них
находятся.
Конечно, если сайт велик, кванта времени, отведенного хостинг-провайдером,
может
и не хватить. Кроме того, не очень вежливо заставлять пользователя ждать
загрузки
страницы карты сервера дольше нескольких секунд.
Как же быть, если описанный сценарий нужен для вашего сайта? Для этого следует
формировать карту не при каждом запросе, а лишь изредка, — ведь новые страницы
добавляются на сервер довольно редко. Гораздо реже, чем, например, их загружают
пользователи. Кроме того, наверное, пользователь не будет особенно недоволен,
если
изменение на карте сервера проявится не сразу же, а спустя некоторое время —
на-
пример, час. Главное для него, чтобы карта была всегда перед глазами, а значит,
ото-
бражалась быстро.
Мы можем хранить уже "просчитанную" карту сервера в файле, быстро выдавая его
пользователю при запросе. Но даже если мы собираемся обновлять этот файл всего
лишь один раз в час (при очередном запросе карты пользователем), мы
наталкиваем-
ся на проблему нехватки кванта времени, выделенного хостинг-провайдером.
Чтобы решить и эту проблему, придется разбить построение большой карты на мно-
жество мелких этапов, каждый из которых занимает, скажем, не более 2-х секунд.
Каждый такой этап должен запускаться при очередном обращении пользователя к
Глава 33. Разные советы 497
карте сервера, но уже после того, как содержимое временного файла с "просчитан-
ной" картой будет отправлено пользователю. Таким образом, мы постепенно будем
накапливать сведения и, как только весь сайт обработан, перестроим карту во
вре-
менном файле. В ближайший час будет отображаться именно она.
Напишем функцию 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.
Часть V. Приемы программирования на PHP 498
$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)
{ // для диагностики выводим имя файла
Часть V. Приемы программирования на PHP 500
print ">$fname ";
// в качестве примера — просто добавляем имя файла в массив
$Result[]="$fname: ".filesize($fname)." ";
}
// Если WalkSite() вернула false, значит, процесс закончился.
if(!WalkSite($DOCUMENT_ROOT,"Walk","map",0,$Result)) {
// В качестве примера просто выводим содержимое массива,
// сформированного вызовами функции Walk(). Реальный код
// должен был бы вырабатывать HTML-представление карты,
// данные которой накоплены в $Result.
print " ";
print join(" \n",$Result);
} else {
// для примера заставляем страницу обновить саму себя,
// имитируя многократные посещения пользователей.
print " ";
}
?>
В этом сценарии функции WalkSite() передается 0 как значение размера кванта
времени, в течение которого можно собирать данные о сайте. Это означает, что
фай-
лы будут обрабатываться по одному при каждом запросе.
В реальном коде карты сервера, конечно, это не так — нужно указывать приемлемый
промежуток времени, чтобы в него "уложилась" обработка сразу нескольких страниц.
Чем меньше будет этот промежуток, тем менее заметным для пользователя станет
замедление, связанное с работой сценария, но тем значительнее будут "накладные
расходы", вызванные работой функций сериализации. Так что тут нужно выбирать
некоторый "средний" вариант. Проще всего это сделать опытным путем — например,
так, чтобы примерно за час при известной посещаемости успевала перестроиться
вся
карта сервера.
Функция WalkSite() из листинга 33.2 работает с файлами, устанавливая на
них рекомендательные блокировки. Этот процесс хоть и позволяет обойти про-
блемы с разделением доступа к файлам, немного сложен для понимания. Он
подробно описан в главе 15 части IV.
Использование самопереадресации
Термин самопереадресация (или, в английском варианте, self-redirect) означает
свой-
ство сценария подавать в браузер клиента запрос, заставляющий его (браузер)
заново
Глава 33. Разные советы 501
выполнить и загрузить этот сценарий с сервера. Звучит, как языческое заклинание,
не
правда ли? Пожалуй, с первого взгляда не совсем ясно, зачем же может
понадобиться
эта хваленая самопереадресация в Web-программировании.
Рассмотрим пример. Предположим, у нас имеется сценарий — гостевая книга напо-
добие той, эскиз которой мы рассматривали в главе 30. С точки зрения
пользователя
сценарий представляет собой страницу с адресом
http://www.ourserver.ru/book/index.html. Если набрать этот адрес в браузе-
ре, появится, во-первых, форма с предложением добавить новое сообщение в книгу,
а
во-вторых, список ранее добавленных "посланий". В атрибуте action тэга
указан адрес той же самой страницы index.html (это вписывается в трехуровневую
схему разработки сценариев), поэтому после набора сообщения и нажатия на кнопку
отправки фактически снова загружается та же самая страница. Только перед ее за-
грузкой генератор данных гостевой книги определяет, что необходимо добавить но-
вую запись, и делает это.
В общем-то, довольно стандартная схема. Пусть пользователь набрал свое послание
и
отправил его на сервер. Перед ним появится список сообщений, первым из которых
будет его собственное. Пока вроде бы все верно. И теперь пользователь, ничего
не
подозревая, нажимает на кнопку Обновить в браузере, заставляя последний, как он
думает, перезагрузить страницу гостевой книги.
Но в действительности происходит совсем не то, что он ожидает. Если данные
формы
были посланы методом POST, браузер выведет на экран диалоговое окно запроса
примерно такого содержания: "Вы пытаетесь обновить данные страницы, которая
была сгенерирована с применением метода POST. Повторить отправку данных (да
или нет)?" Если пользователь нажмет кнопку Нет, то гостевая книга не
перезагрузит-
ся, а появится совершенно бесполезная стандартная страница с сообщением о том,
что "данные устарели". Если же он подтвердит вторичную отправку данных, его со-
общение будет добавлено в книгу еще раз, а потому "размножится". Довольно не-
трудно понять, почему так происходит: ведь браузер "не знает", что в
действительно-
сти пользователь хочет лишь вторично "зайти" на адрес страницы книги, а не
повторить отправку всех данных формы.
Однако ситуация становится еще плачевнее, если мы применяем в нашей гостевой
книге метод GET. В этом случае при нажатии на кнопку Обновить браузер "без лиш-
них разговоров" пошлет данные формы на сервер повторно, так что сообщение будет
лишний раз добавлено в гостевую книгу без предупреждений. И это тоже понятно:
ведь метод GET — не что иное, как простое изменение URL страницы, а именно, до-
бавление в его конец символа ?, после которого следуют параметры (в том числе
текст записи).
Впрочем, метод GET практически никогда не применяется в интерактивных сце-
нариях, таких как гостевые книги, форумы и т. д. Мы уже говорили в первой
части книги на эту тему, но она настолько важна, что я повторюсь. Если для
Часть V. Приемы программирования на PHP 502
одних и тех же данных формы при их многократной отправке страница все-
гда выглядит одинаково, значит, эти данные логично передавать методом
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
?>
Введите текст:
$v) {?>
=$v?>
}?>
Мы обеспечиваем "уникальность" 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"); // Дата в прошлом
Часть V. Приемы программирования на PHP 504
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! ";
else echo "Вы не знаете языка $k. ";
}
?>
Какие языки программирования вы знаете?
PHP
Глава 33. Разные советы 505
PHP
Теперь в случае, если пользователь не выберет какой-нибудь из флажков, браузер
отправит сценарию пару Known[язык]=0, сгенерированную соответствующим скры-
тым полем, и в массиве $Known создастся соответствующий элемент. Если пользова-
тель выбрал флажок, эта пара также будет послана, но сразу же после нее
последует
пара Known[язык]=1, которая "перекроет" предыдущее значение.
Не включи мы скрытые поля в форму из листинга 33.5, сценарий печатал бы только
сообщения о тех языках, которые "знает пользователь", пропуская языки, ему
"неиз-
вестные". В нашем же случае сценарий реагирует и на неустановленные флажки.
Такой способ немного увеличивает объем данных, передаваемых методом
POST, за счет тех самых пар, которые генерируются скрытыми полями. Впро-
чем, в реальной жизни это "увеличение" практически незаметно (особенно для
POST-форм).
ЧАСТЬ VI.
ПРИЛОЖЕНИЯ
Приложение 1
Файл конфигурации
Apache httpd.conf
Это приложение содержит полный текст файла конфигурации сервера Apache
httpd.conf с комментариями на русском языке.
Содержимое листинга П1.1 полностью соответствует указаниям по настройке
Apache, приведенным в части II книги. Если у вас по какой-то причине не
получит-
ся правильно установить Apache и PHP версии 4, руководствуясь этими указания-
ми, представленный ниже текст файла httpd.conf решит все проблемы.
Несколько слов о формате httpd.conf. Файл состоит из строк, содержащих дирек-
тивы Apache. В одной строке может быть расположено не более одной директивы.
Текст от # aо конца строки считается комментарием и не берется в рассмотрение.
Также игнорируются пустые строки.
При изменении начальной конфигурации файла возможно группирование нескольких
директив в блоки, или контейнеры. При этом Apache поддерживает только ограни-
ченное количество допустимых типов контейнеров. Любой блок-контейнер начинает-
ся строкой вида <ИмяКонтейнера>, расположенной, как обычно, на отдельной стро-
ке, и завершается тэгом ИмяКонтейнера>. Некоторые (но не все) блоки могут
быть вложенными.
Директивы, касающиеся индивидуальных настроек для каталогов или файлов, могут
также помещаться в специальные файлы .htaccess, расположенные в соответст-
вующих местах дерева каталогов сайта. Эти файлы должны иметь тот же формат, что
и httpd.conf. Однако для них имеются особые ограничения на использование ди-
ректив и блоков — список недопустимых можно найти в документации, поставляе-
мой с Apache.
Листинг П1.1. Файл конфигурации Apache httpd.conf
# Основан на конфигурационных файлах сервера NSCA, созданных
# Робом МакКулом.
#
# Главный файл конфигурации сервера Apache, содержащий директивы,
Часть VI. Приложения
510
# управляющие работой сервера. За более детальной информацией
# обращайтесь по адресу http://www.apache.org/docs/.
#
# Не стоит читать эти директивы без понимания их роли. Они
# приведены здесь лишь в качестве примера одного из возможных
# вариантов. В случае сомнений обращайтесь к сопроводительной
# документации. Считайте, что вас предупредили.
#
# После просмотра и анализа файла httpd.conf сервер
# попробует найти и обработать файлы:
# C:/Program Files/Apache Group/Apache/conf/srm.conf, а затем
# C:/Program Files/Apache Group/Apache/conf/access.conf,
# если вы не переопределили эти имена директивами ResourceConfig
# и/или AccessConfig.
#
# Директивы конфигурации сгруппированы в три основных раздела:
#
# 1. Директивы, управляющие процессом Apache в целом (глобальное
# окружение).
# 2. Директивы, определяющие параметры "главного" сервера, или
# сервера "по умолчанию", отвечающего на запросы, которые
# не обрабатываются виртуальными хостами. Эти директивы задают
# также установки по умолчанию для всех остальных виртуальных хостов.
# 3. Установки для виртуальных хостов, позволяющие обрабатывать
# запросы Web одним-единственным сервером Apache, но направлять
# по раздельным IP-адресам или именам хостов.
#
# Файлы конфигурации программы и журналы регистрации событий
# (в программисткой среде они чаще называются "конфигами" и "логами",
# так что, я думаю, ничего страшного не произойдет, если я буду
# придерживаться этой терминологии и здесь).
# Если имена файлов, определенных вами для управления сервером,
# начинаются с символа / (или "диск:/" для Win32), сервер будет
# использовать явно указанный в этом имени полный путь. Если же имена не
# начинаются с "/", то для определения пути будет задействовано значение
# директивы ServerRoot. Так, logs/foo.log при значении ServerRoot,
# равном /usr/local/apache, будет интерпретироваться сервером как
# /usr/local/apache/logs/foo.log.
#
Приложение 1. Файл конфигурации Apache httpd.conf
511
# Внимание: В определении имен файлов вы должны использовать прямые слэши
# вместо обратных (т. е. c:/apache вместо c:\apache). Если не указано
# имя диска, по умолчанию будет выбран диск, на котором размещен
# Apache.exe; тем не менее, во избежание путаницы, рекомендуется, чтобы
# вы всегда явно указывали в абсолютных путях имя диска.
#
### Раздел 1: Глобальное окружение
#
# Директивы в этом разделе определяют общие параметры Apache, такие как,
# например, число запросов, которое он может обрабатывать одновременно,
# или где ему искать свои файлы конфигурации.
#
# Директива ServerType может иметь значения inetd или standalone.
# Режим inetd поддерживается только на платформах Unix.
ServerType standalone
#
# ServerRoot: вершина дерева каталогов, в которых содержатся файлы
# конфигурации, регистрации и отслеживания ошибок.
#
# В конце строки добавлять слэш не следует!
ServerRoot "C:/Program Files/Apache Group/Apache"
#
# PidFile: Файл, куда сервер при запуске должен записывать свой
# идентификатор процесса.
PidFile logs/httpd.pid
#
# ScoreBoardFile: Учетный файл, предназначенный для хранения внутренней
# информации процесса сервера. Он необходим не для всех архитектур.
# Если для вашей он нужен (об этом можно судить по тому, будет ли создан
# такой файл, когда вы запустите Apache), то вы должны обеспечить, чтобы
# никакие два экземпляра процесса Apache не использовали один и тот же
# учетный файл.
ScoreBoardFile logs/apache_runtime_status
Часть VI. Приложения
512
#
# В стандартной конфигурации сервер обработает при запуске файлы
# httpd.conf, srm.conf и access.conf (именно в таком порядке).
# Последние два файла в настоящее время поставляются пустыми, поскольку
# теперь рекомендуется для простоты, чтобы все директивы указывались в
# одном файле (httpd.conf).
# Закомментированные ниже значения встроены в сервер по умолчанию.
# Если вы используете другие имена файлов, отредактируйте и
# раскомментируйте "умолчальные". Если потребуется, чтобы сервер
# проигнорировал эти файлы, вы можете указать значения /dev/null (для
# Unix) или nul (для Win32).
#ResourceConfig conf/srm.conf
#AccessConfig conf/access.conf
#
# Timeout: Время ожидания в секундах, прежде чем сервер примет или
# отправит сообщение о тайм-ауте.
Timeout 300
#
# KeepAlive: Признак, позволено или нет устанавливать долговременные
# соединения (persistent connections) (т.е. когда обрабатывается более
# одного запроса на соединение). Для запрета укажите значение Off.
KeepAlive On
#
# MaxKeepAliveRequests: Максимальное число запросов, допустимое в одном
# долговременном соединении. Для снятия ограничений обнулите параметр,
# но для максимального быстродействия мы рекомендуем указать заведомо
# большое конкретное значение.
MaxKeepAliveRequests 100
#
# KeepAliveTimeout: Время ожидания в секундах следующего запроса от
# одного и того же клиента в одном подключении.
KeepAliveTimeout 15
#
# Для обработки запросов Apache для Win32 всегда порождает один дочерний
Приложение 1. Файл конфигурации Apache httpd.conf
513
# процесс. Если он по каким-либо причинам будет преждевременно завершен,
# другой дочерний процесс создается автоматически. Поступающие запросы
# внутри такого дочернего процесса обрабатываются отдельными потоками.
# Следующие две директивы управляют поведением таких потоков и процессов.
#
# MaxRequestsPerChild: Число запросов, которое позволено обрабатывать
# дочернему процессу до переполнения. При переполнении дочерний процесс
# будет принудительно завершен, чтобы избежать проблем при длительной
# непрерывной работе, если Apache (или используемые им библиотеки),
# допускают утечку памяти или других ресурсов. На большинстве систем
# это не требуется, но некоторые (например, Solaris) имеют заметные
# утечки в библиотеках. Если нет других рекомендаций, для Win32
# установите значение 0 (без ограничений).
#
MaxRequestsPerChild 0
#
# ThreadsPerChild: Число одновременно выполняющихся потоков (т.е.
# запросов), которое допускает сервер. Установите это значение в
# соответствии с требуемой загрузкой сервера (больше активных запросов
# одновременно означает, что они обслуживаются медленнее) и объемом
# системных ресурсов, который вы можете предоставить серверу.
#
ThreadsPerChild 50
#
# Listen: Позволяет привязать Apache к конкретному адресу IP, и/или
# порту, в дополнение к порту, определенному по умолчанию. См. также
# директиву .
#
#Listen 3000
#Listen 12.34.56.78:80
#
# BindAddress: Этой опцией вы можете обеспечить поддержку виртуальных
# хостов. Данная директива используется для указания серверу адреса IP,
# который необходимо отслеживать. Она может содержать *, адрес IP или
# полное имя домена Интернета. См. также директивы и
Listen.
Часть VI. Приложения
514
#
#BindAddress *
#
# Поддержка динамически разделяемых объектов (DSO, Dynamic Shared Object)
#
# Для того чтобы иметь возможность использовать модуль, созданный как
# библиотека DSO, вам следует поместить в этом месте соответствующую
# строку LoadModuleТогда модуль будет доступен
# прежде обращения к нему.
# За детальными разъяснениями механизмов DSO вы можете обратиться к
# файлу README.DSO в дистрибутиве Apache 1.3, а также выполнить
# команду 'apache -l', чтобы получить список уже встроенных
# (статически скомпонованных и таким образом всегда доступных)
# модулей сервера Apache.
#
# Внимание: Порядок, в котором загружаются модули, имеет большое
# значение. Не меняйте нижеследующий порядок без консультации со
# специалистом.
#
#LoadModule anon_auth_module modules/ApacheModuleAuthAnon.dll
#LoadModule dbm_auth_module modules/ApacheModuleAuthDBM.dll
#LoadModule digest_auth_module modules/ApacheModuleAuthDigest.dll
#LoadModule cern_meta_module modules/ApacheModuleCERNMeta.dll
#LoadModule digest_module modules/ApacheModuleDigest.dll
#LoadModule expires_module modules/ApacheModuleExpires.dll
#LoadModule headers_module modules/ApacheModuleHeaders.dll
#LoadModule proxy_module modules/ApacheModuleProxy.dll
#LoadModule rewrite_module modules/ApacheModuleRewrite.dll
#LoadModule speling_module modules/ApacheModuleSpeling.dll
#LoadModule info_module modules/ApacheModuleInfo.dll
#LoadModule status_module modules/ApacheModuleStatus.dll
#LoadModule usertrack_module modules/ApacheModuleUserTrack.dll
#
# Директива ExtendedStatus определяет, будет ли Apache генерировать
# детальную информацию о состоянии (ExtendedStatus On) или только
# общую информацию (ExtendedStatus Off) при обращении к функции
# server-status. Значение по умолчанию — Off.
Приложение 1. Файл конфигурации Apache httpd.conf
515
#
#ExtendedStatus On
### Раздел 2: Конфигурация сервера по умолчанию
#
# Директивы этого раздела устанавливают значения, используемые "главным
# сервером", который отвечает на запросы, не обрабатываемые виртуальными
# хостами. Эти значения обусловливают также установки по умолчанию для
# любых контейнеров , которые вы будете определять
# здесь далее.
#
# Любые из директив раздела могут быть включены в контейнер
# ; в таком случае установки по умолчанию будут
# переопределены ими для этого виртуального хоста.
#
#
# Если в директиве ServerType (установленной ранее в разделе "Глобальное
# окружение") задано значение inetd, следующие несколько директив не
# имеют никакого эффекта, поскольку их значение определено конфигурацией
# inetd. Переходите к директиве ServerAdmin.
#
# Port: Номер порта, к которому подключен сервер.
#
Port 80
#
# ServerAdmin: Ваш адрес, по которому следует направлять сообщения о
# проблемах с сервером. Этот адрес появится на некоторых сгенерированных
# сервером страницах, таких, как сообщения об ошибках.
#
ServerAdmin you@your.address
#
# Директива ServerName задает имя хоста, возвращаемое клиенту, если это
# имя отличается от того имени, которое получила программа (например,
# используйте www вместо реального имени хоста).
Часть VI. Приложения
516
#
# Внимание: Вы не можете просто выдумывать имена хостов в надежде, что
# это сработает. Имя, которое вы определяете здесь, должно быть
# действительным именем DNS для вашего хоста. В случае затруднений с
# пониманием изложенного справьтесь у
# администратора сети.
# Если ваш хост не имеет зарегистрированного имени DNS, вы можете указать
# здесь его адрес IP. В таком случае вам придется обращаться к хосту по
# адресу (например, http://123.45.67.89/) и это может сильно осложнить
# переадресацию ресурсов.
#
ServerName localhost
#
# DocumentRoot: Каталог, в котором будут находиться ваши документы (т.е.
# Web-страницы). По умолчанию, все запросы выбираются из этого каталога;
# для указания же других мест могут использоваться символические ссылки
# (links) и псевдонимы (aliases).
#
DocumentRoot "z:/home/localhost/www"
#
# Каждый каталог, к которому Apache имеет доступ, может быть
# сконфигурирован в отношении свойств и сервисов, которые могут быть
# разрешены и/или запрещены в этом каталоге (и его подкаталогах).
#
# Сначала мы определяем свойства "по умолчанию".
#
Options Indexes Includes
AllowOverride All
allow from all
#
# Обратите внимание, что с этого места и далее вы должны явным образом
# указывать свойства, которые могут быть разрешены, — так что, если что-
то
# не работает так, как вы ожидаете, сначала убедитесь, что вы разрешили
Приложение 1. Файл конфигурации Apache httpd.conf
517
# это свойство ниже.
#
# Здесь должен быть указан каталог, который вы установили как
# DocumentRoot.
#
#;
#
# Опции могут иметь значения None, All или любую комбинацию из
# Indexes, Includes, FollowSymLinks, ExecCGI или MultiViews.
#
# Заметьте, что MultiViews должен быть указан отдельно —
# Options All для этого не достаточно.
#
# Options Indexes FollowSymLinks MultiViews
#
# Директива перечисляет опции, которые могут быть переопределены в
# файлах .htaccess. Значением может быть All или любая комбинация из
# Options, FileInfo, AuthConfig и Limit.
#
# AllowOverride None
#
# Эти директивы определяют, какие пользователи имеют доступ к информации,
# расположенной на этом сервере.
#
# Order allow,deny
# Allow from all
#
#
# UserDir: Название каталога, которое прибавляется к именам
# пользовательских домашних каталогов при получении запроса ~user
# (например, http://www.server.com/~username).
#
# Под Win32 мы в настоящее время не пытались устанавливать каталог
# регистрации пользователя, поэтому приходится работать с форматом,
# приведенным ниже.
Часть VI. Приложения
518
#
UserDir "C:/Program Files/Apache Group/Apache/users/"
#
# DirectoryIndex: Имя файла (или файлов), используемое в качестве
# предопределенной страницы-указателя или оглавления. Если вы указываете
# несколько имен, разделяйте их пробелами.
#
DirectoryIndex index.htm index.html
#
# AccessFileName: Имя файла, который сервер ищет в каждом каталоге для
# определения прав доступа.
#
AccessFileName .htaccess
#
# Следующие строки предотвращают доступ к файлам .htaccess со стороны
# Web-клиентов. Поскольку файлы .htaccess нередко содержат информацию об
# аутентификации, доступ к ним запрещен из соображений безопасности. Вы
# можете удалить эти строки (или поставить символ комментария),
# если допускаете, чтобы посетители могли просматривать содержимое файлов
# .htaccess из Web. Если вы поменяете значение директивы AccessFileName
# выше, не забудьте внести и сюда соответствующие изменения.
#
Order allow,deny
Deny from all
#
# CacheNegotiatedDocs: По умолчанию с каждым документом Apache отправляет
# инструкцию "Pragma: no-cache", что является указанием proxy-серверам не
# кэшировать данный документ. Если раскрыть следующую строку, то
# поведение proxy-серверов изменится и им будет разрешено кэшировать
Приложение 1. Файл конфигурации Apache httpd.conf
519
# документы.
#
#CacheNegotiatedDocs
#
# UseCanonicalName: (Впервые в версии 1.3.) Если эта директива включена
# (On), то всякий раз, когда Apache требуется создать ссылку на самого
# себя (self-referencing URL, т.е. адрес сервера, с которого поступает
# ответ на запрос), для формирования "канонического имени" он будет
# использовать значения директив ServerName и Port, когда это возможно.
# Если директива выключена (Off), Apache будет по возможности
# использовать значения, предоставленные клиентом. Эта директива влияет
# также на значения переменных SERVER_NAME и SERVER_PORT в CGI-сценариях.
#
UseCanonicalName On
#
# Директива TypesConfig описывает расположение файла mime.types
# (или его эквивалента).
#
TypesConfig conf/mime.types
#
# Директива DefaultType определяет MIME-тип, который будет использоваться
# для какого-либо документа, если сервер не сможет определить его по иным
# признакам, например, по расширению имени файла. Если ваш сервер
# содержит по большей части тексты или HTML-документы, text/plain
# является приемлемым решением. Если большая часть содержимого является
# исполняемыми файлами или изображениями, вы можете поменять значение на
# application/octet-stream, чтобы предотвратить попытку браузера
# показать содержимое двоичного файла.
#
DefaultType text/plain
#
# Модуль mod_mime_magic позволяет серверу использовать разнообразные
# приемы определения типа файла по его содержимому. Директива
Часть VI. Приложения
520
# MIMEMagicFile указывает ему файл, где даны описания таких приемов.
# По умолчанию mod_mime_magic не включен в состав сервера (вы должны
# загрузить его сами с помощью директивы LoadModule — см. абзац DSO в
# разделе "Глобальное окружение", или заново откомпилировать сервер
# с этим модулем), поэтому директива MIMEMagicFile заключена в контейнер
# . Это означает, что она будет обработана только в том случае,
# если модуль mod_mime_magic уже загружен.
#
MIMEMagicFile conf/magic
#
# Директива HostnameLookups определяет, регистрировать ли клиентов по
# именам, или только по адресам IP, т.е. www.apache.org (On) или
# 204.62.129.132 (Off). По умолчанию — Off, поскольку для снижения
# нагрузки на сеть было бы лучше, если бы вы использовали эту
# возможность, зная о последствиях, т. к. отслеживание по именам означа-
ет,
# что каждый клиентский запрос приведет как минимум к еще одному запросу
# к серверу имен для преобразования IP-адреса в имя.
#
HostnameLookups Off
#
# ErrorLog: Расположение файла регистрации ошибок. Если вы не определяете
# директиву ErrorLog внутри контейнера , сообщения об
# ошибках, возникших при работе этого хоста, будут записаны в указанный
# ниже файл. В противном случае все сообщения направятся в специфичный
# для виртуального хоста журнал.
#
ErrorLog logs/error.log
#
# LogLevel: Определение характера ошибок, которые записываются в
# error.log. Возможные значения в порядке убывания количества сообщений:
# debug, info, notice, warn, error, crit, alert, emerg.
#
Приложение 1. Файл конфигурации Apache httpd.conf
521
LogLevel warn
#
# Следующие директивы указывают псевдонимы некоторых форматов, которые
# используются в директиве CustomLog (см. ниже).
#
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
#
# Расположение и формат файла регистрации (лога). Если вы не определяете
# никаких лог-файлов внутри контейнера , сведения
# будут записываться здесь. Если же вы определяете отдельный лог-файл
# для виртуального хоста, доступ будет отслеживаться в этом логе,
# но не здесь.
#
CustomLog logs/access.log common
#
# Если вы хотите, чтобы имелся агент ссылочных логов (referer logfiles
# agent), раскомментируйте следующие директивы.
#
#CustomLog logs/referer.log referer
#CustomLog logs/agent.log agent
#
# Если вы предпочитаете иметь один лог-файл с информацией о доступе,
# агентах и ссылках (комбинированный формат лог-файла), вы можете
# использовать следующую директиву.
#CustomLog logs/access.log combined
#
# Позволяет добавить дополнительную строку, содержащую версию сервера и
# имя виртуального хоста на страницах, сгенерированных сервером
# (сообщениях об ошибках, листингах каталогов FTP, в вывод модулей
# mod_status и mod_info, но не в CGI-документах). Чтобы дополнительно
Часть VI. Приложения
522
# включить ссылку mailto:, содержащую значение директивы ServerAdmin,
# установите значение EMail.
# Допустимые значения: On | Off | Email
#
ServerSignature On
#
# Apache по умолчанию анализирует первую строку каждого CGI-сценария.
# Если эта строка является комментарием и выглядит так: символ (#),
# затем восклицательный знак (!) и, наконец, путь к
# программе-интерпретатору, по которому осуществляется запуск
# сценария, Apache запускает этот интерпретатор.
# Например, для perl-сценариев, стартуемых под управлением perl.exe
# из каталога C:\Program Files\Perl, эта строка должна выглядеть так:
# !c:/program files/perl/perl
# Внимание: вы не должны вставлять пробелы перед символом (#). Кроме
# того, указанная специальная строка должна быть именно первой строкой
# файла. Конечно, для запускаемого файла должна быть разрешена обработка
# CGI — например, путем указания директивы ScriptAlias или
# Options ExecCGI.
#
# Тем не менее, Apache для Windows позволяет в дополнение к "магической"
# строке использовать Реестр для поиска ассоциаций с расширениями.
# Команда для запуска файла указанного типа в этом случае ищется в
# Реестре точно так же, как это происходит, например, при работе
# Проводника, когда вы выполняете двойной щелчок на файле. Действия по
# запуску сценария могут быть сконфигурированы из меню Вид Проводника.
# Там необходимо выбрать Свойства папки и переключиться на вкладку
# Типы файлов. Нажатие на кнопку Изменить позволяет задать действие,
# которое Apache выполнит при попытке открытия файла. Если это не
# удастся, Apache будет искать интерпретатор при помощи "магической"
# строки. Возможно, поведение сервера изменится в Apache версии 2.0.
#
# Чтобы разрешить это специфичное для Windows поведение сервера и, таким
# образом, запретить анализ "магической" строки, удалите комментарий
# со следующей директивы:
#
Приложение 1. Файл конфигурации Apache httpd.conf
523
ScriptInterpreterSource registry
#
# Эта директива может быть помещена в отдельный блок или
# в файл .htaccess с указанием в качестве значения registry
# (поведение Windows) или script (анализ "магической" строки, принятый
# в Unix). В таком случае она будет перекрывать директиву, расположенную
# здесь, в главном конфигурационном файле сервера.
#
#
# Псевдонимы: Можно добавлять любое количество псевдонимов (без
# ограничений).
# Формат: Alias псевдоним действительное_имя
#
# Обратите внимание, что если вы включаете завершающий слэш в
# "псевдоним", то сервер потребует его присутствия и в URL. Так,
# /icons не будет разыменован в данном примере, только /icons/.
#
Alias /icons/ "C:/Program Files/Apache Group/Apache/icons/"
Options Indexes MultiViews
AllowOverride None
Order allow,deny
Allow from all
#
# ScriptAlias: Указывает каталог, который содержит серверные
# сценарии. Свойства ScriptAlias’ов такие же, как и у простых
# псевдонимов, за исключением того, что документы в каталоге
# "действительное_имя" считаются приложениями и выполняются
# на сервере, а не отправляются клиенту. К директиве
# ScriptAlias применяются те же правила в отношении
# завершающего /, что и к Alias.
#
ScriptAlias /cgi-bin/ "z:/home/localhost/cgi/"
ScriptAlias /cgi/ "z:/home/localhost/cgi/"
Часть VI. Приложения
524
# Конец определений псевдонимов.
#
# Директива Redirect позволяет сообщить клиенту о документе, который
# существовал некогда в пространстве имен сервера, но был перемещен
# в другое место. Она информирует клиента о его новом адресе.
#
# Формат: Redirect старый_URL новый_URL
#
#
# Директивы, управляющие генерацией сервером листингов каталогов.
#
#
# FancyIndexing означает, что вы предпочитаете листинги с
# "украшательствами". О других возможных значениях директивы
# IndexOptions см. сопроводительную документацию.
#
IndexOptions FancyIndexing
#
# Директивы AddIcon* указывают серверу, какими ярлыками
# будут украшены имена файлов в листинге каталога. Ярлыки
# изображаются только в режиме FancyIndexing.
#
AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip
AddIconByType (TXT,/icons/text.gif) text/*
AddIconByType (IMG,/icons/image2.gif) image/*
AddIconByType (SND,/icons/sound2.gif) audio/*
AddIconByType (VID,/icons/movie.gif) video/*
AddIcon /icons/binary.gif .bin .exe
AddIcon /icons/binhex.gif .hqx
AddIcon /icons/tar.gif .tar
AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv
AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip
AddIcon /icons/a.gif .ps .ai .eps
Приложение 1. Файл конфигурации Apache httpd.conf
525
AddIcon /icons/layout.gif .html .shtml .htm .pdf
AddIcon /icons/text.gif .txt
AddIcon /icons/c.gif .c
AddIcon /icons/p.gif .pl .py
AddIcon /icons/f.gif .for
AddIcon /icons/dvi.gif .dvi
AddIcon /icons/uuencoded.gif .uu
AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl
AddIcon /icons/tex.gif .tex
AddIcon /icons/bomb.gif core
AddIcon /icons/back.gif ..
AddIcon /icons/hand.right.gif README
AddIcon /icons/folder.gif ^^DIRECTORY^^
AddIcon /icons/blank.gif ^^BLANKICON^^
#
# DefaultIcon определяет ярлык для файла по умолчанию
# если он не задан явно.
#
DefaultIcon /icons/unknown.gif
#
# AddDescription позволяет размещать краткое описание после имени
# файла в индексах (листингах каталогов), сгенерированных сервером.
# Такие описания выводятся только в режиме FancyIndexing.
#
# Формат: AddDescription "строка_описания" .расширение_имени_файла
#
#AddDescription "GZIP compressed document" .gz
#AddDescription "tar archive" .tar
#AddDescription "GZIP compressed tar archive" .tgz
#
# ReadmeName задает имя README-файла, который добавляется к листингу
# каталога по умолчанию.
#
# HeaderName указывает имя файла, выводимого в
# заголовке листингов каталога.
Часть VI. Приложения
526
#
# Если задана директива MultiViews в числе значений Options,
# сначала сервер попытается открыть файл имя.html и включит его в
# листинг, если файл существует. Если файл имя.html не существует,
# сервер переориентируется на открытие файла
# имя.txt и включение его в листинг в виде простого текста.
#
ReadmeName README
HeaderName HEADER
#
# IndexIgnore описывает набор имен файлов, которые должны быть
# исключены из листинга. В именах допустимы метасимволы подстановки
# в стиле shell.
IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t
# Конец секции директив управления листингами.
#
# Типы документов.
#
#
# AddEncoding позволяет вам заставить определенные браузеры
# (Mosaic/X 2.1+) распаковывать информацию "на лету".
# Внимание: это свойство поддерживают не все браузеры. Несмотря
# на сходство имен, нижеприведенные директивы Add* не
# имеют ничего общего с директивами оформления FancyIndexing,
# приведенными выше.
#
AddEncoding x-compress Z
AddEncoding x-gzip gz tgz
#
#
# AddLanguage позволяет указать язык документа. Вы можете затем
# использовать протокол обмена (content negotiation) для выдачи
# браузеру документа на том языке, который он (браузер) предпочитает.
Приложение 1. Файл конфигурации Apache httpd.conf
527
#
# Примечание 1: Суффикс не обязательно должен совпадать с буквенным
# кодом языка — те, у кого есть документы на польском языке
# (стандартный сетевой буквенный код pl), могут воспользоваться
# директивой AddLanguage pl .po во избежание конфликта с
# распространенным суффиксом сценариев на языке Perl.
#
# Примечание 2: Нижеследующие примеры показывают, что в нескольких
# случаях двухбуквенный код языка не совпадает с двухбуквенным кодом
# страны.
# Например, "Датский/da" вместо "Дания/dk".
#
# Примечание 3: В случае ltz мы нарушаем требования RFC, используя
# трехбуквенный код. Но уж тут ничего не поделаешь. В будущем,
# возможно, несоответствия с RFC1766 будут устранены.
#
# Коды языков:
# датский (Danish) da; голландский, Нидерланды (Dutch) nl;
# английский (English) en; эстонский (Estonian) ee;
# французский (French) fr; немецкий (German) de;
# новогреческий (Greek-Modern) el; итальянский (Italian) it;
# португальский (Portuguese) pt;
# люксембургский (Luxembourgeois*) ltz;
# испанский (Spanish) es; шведский (Swedish) sv;
# каталонский (Catalan) ca; чешский (Czech) cz;
# русский (Russian) ru.
#
AddLanguage da .dk
AddLanguage nl .nl
AddLanguage en .en
AddLanguage et .ee
AddLanguage fr .fr
AddLanguage de .de
AddLanguage el .el
AddLanguage he .he
AddCharset ISO-8859-8 .iso8859-8
AddLanguage it .it
AddLanguage ja .ja
AddCharset ISO-2022-JP .jis
Часть VI. Приложения
528
AddLanguage kr .kr
AddCharset ISO-2022-KR .iso-kr
AddLanguage no .no
AddLanguage pl .po
AddCharset ISO-8859-2 .iso-pl
AddLanguage pt .pt
AddLanguage pt-br .pt-br
AddLanguage ltz .lu
AddLanguage ca .ca
AddLanguage es .es
AddLanguage sv .se
AddLanguage cz .cz
AddLanguage ru .ru
AddLanguage tw .tw
AddCharset Big5 .Big5 .big5
AddCharset WINDOWS-1251 .cp-1251
AddCharset CP866 .cp866
AddCharset ISO-8859-5 .iso-ru
AddCharset KOI8-R .koi8-r
AddCharset UCS-2 .ucs2
AddCharset UCS-4 .ucs4
AddCharset UTF-8 .utf8
# LanguagePriority позволяет определить первоочередность некоторых
# языков при установлении протокола обмена.
#
# Возможно, вы захотите изменить предложенный порядок языков. Просто
# перечислите их в порядке убывания приоритета.
#
LanguagePriority en da nl fr de el it ja no pl pt ru ca es sv tw
#
# AddType позволяет слегка подправить mime.types, не редактируя его,
# или объявить конкретные файлы имеющими определенный тип.
#
# Например, модуль PHP3 (этот модуль не является частью дистрибутива
# сервера Apache), обычно использует следующие объявления:
Приложение 1. Файл конфигурации Apache httpd.conf
529
#
#AddType application/x-httpd-php3 .php3
# AddType application/x-httpd-php3-source .phps
#
# В случае PHP 4.x укажите:
#
AddType application/x-httpd-php .php
# AddType application/x-httpd-php-source .phps
# Следующие строки не относятся к заданию типов документов,
# но их удобно поместить сюда для подключения PHP:
#
ScriptAlias /_php/ "C:/Program Files/PHP4/"
Action application/x-httpd-php "/_php/php.exe"
AddType application/x-tar .tgz
#
# AddHandler позволяет отобразить определенные расширения имен файлов
# на обработчиков вне связи с определениями типов файлов. Обработчики
# могут быть как встроены в сервер, так и объявлены директивой
# Action (см. ниже).
#
# Если вы хотите использовать файлы, вставляемые сервером в ваши
# документы (SSI — server side includes), снимите комментарий
# со следующих строк:
#
# для использования сценариев CGI —
#
AddHandler cgi-script .bat .exe .cgi
#
# для HTML-файлов, предварительно обрабатываемых
# сервером (server-parsed HTML files):
#
AddType text/html .shtml
AddHandler server-parsed .shtml .html .htm
#
Часть VI. Приложения
530
# Раскомментируйте следующую строку, чтобы разрешить Apache передачу
# специальных файлов, которые не сопровождаются стандартными
# заголовками HTTP (send-asis HTTP file).
#
# AddHandler send-as-is asis
#
# Если вы хотите использовать карты-изображения, обрабатываемые
# сервером, раскройте следующую директиву:
#
# AddHandler imap-file map
#
# Если вы хотите задействовать карты типов (type maps, см.
# документацию), используйте:
#
# AddHandler type-map var
# Конец блока директив описания типов документов.
#
# Директива Action позволяет определить приложение, выполняющее сценарии,
# когда запрашиваются содержащие их файлы. Это устраняет необходимость
# многократного упоминания URL часто используемых процессоров
# CGI-сценариев.
# Формат: Action псевдоним_типа /псевдоним_пути/обработчик
# Action среда/тип /псевдоним_пути/обработчик
#
#
# MetaDir: определяет имя каталога, в котором Apache может найти файлы с
# метаинформацией. Эти файлы содержат дополнительные заголовки HTTP,
# включаемые при отправке определенных документов.
#
# MetaDir .web
#
# MetaSuffix устанавливает суффикс имени файла, содержащего метаинформацию.
Приложение 1. Файл конфигурации Apache httpd.conf
531
#
# MetaSuffix .meta
#
# Настраиваемая реакция на ошибки (собственный стиль Apache) может быть
# трех типов.
#
# 1) простой текст
# ErrorDocument 500 "Сервер сказал а-я-яй!"
# Внимание: знак двойной кавычки просто означает, что далее следует
# текст.
#
# 2) локальная переадресация
# Чтобы перенаправить на локальный документ:
# ErrorDocument 404 /missing.html
# Перенаправлять можно и на сценарий, и на документ, использующий
# включения на стороне сервера:
# ErrorDocument 404 /cgi-bin/missing_handler.pl
#
# 3) внешняя переадресация
# ErrorDocument 402 http://some.other_server.com/info.html
# Большинство переменных окружения, связанных с исходным запросом,
# станут недоступны при такой переадресации.
#
# Установки, связанные с браузером пользователя.
#
#
# Следующие директивы отменяют поддержку долговременных соединений
# (keepalives) и "смывание" заголовков HTTP. Первая директива
# отменяет их для Netscape 2.x и браузеров, которые "притворяются",
# что они — Netscape (известны некоторые проблемы с такими
# браузерами). Вторая директива предназначена для Microsoft Internet
# Explorer 4.0b2, реализация HTTP/1.1 которого не полна и не
# поддерживает должным образом keepalive, когда он используется в
# откликах 301 или 302 (переадресация).
#
BrowserMatch "Mozilla/2" nokeepalive
Часть VI. Приложения
532
BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0
#
# Следующая директива отключает отклики по HTTP/1.1 браузерам,
# которые нарушают стандарты HTTP/1.0 и не могут разобрать
# основной отклик 1.1.
#
BrowserMatch "RealPlayer 4\.0" force-response-1.0
BrowserMatch "Java/1\.0" force-response-1.0
BrowserMatch "JDK/1\.0" force-response-1.0
# Конец настроек, связанных с браузерами.
#
# Следующая группа директив управляет отчетами о состоянии сервера,
# имеющего URL http://servername/server-status. Для приведения в
# соответствие с вашими нуждами измените .your_domain.com.
#
#
# SetHandler server-status
# Order deny,allow
# Deny from all
# Allow from .your_domain.com
#
#
# Эта группа директив управляет отчетами конфигурации удаленного
# сервера http://servername/server-info (требуется, чтобы был загружен
# mod_info.c). Замените .your_domain.com на имя вашего домена.
#
#
# SetHandler server-info
# Order deny,allow
# Deny from all
# Allow from .your_domain.com
#
#
Приложение 1. Файл конфигурации Apache httpd.conf
533
# Поступали сообщения, что некие люди пытаются злоупотреблять древней
# ошибкой старых версий Apache. Ошибка касалась CGI-сценария,
# поставлявшегося с Apache.
# Раскрыв следующие строки, вы можете переадресовать эти атаки
# на регистрирующий сценарий на phf.apache.org. А можете регистрировать
# их сами, используя сценарий support/phf_abuse_log.cgi.
#
#
# Deny from all
# ErrorDocument 403 http://phf.apache.org/phf_abuse_log.cgi
#
#
# Директивы proxy-сервера.
#
#
# Раскройте следующую строку для того, чтобы разрешить
# работу с proxy.
# ProxyRequests On
#
# Order deny,allow
# Deny from all
# Allow from .your_domain.com
#
#
# Разрешить/запретить обработку заголовков HTTP/1.1 Via:.
# Возможные значения: Off | On | Full | Block. Full добавляет в
# заголовок версию сервера, Block удаляет все исходящие
# заголовки Via:.
#
# ProxyVia On
#
# Для разрешения также кэширования отредактируйте и раскройте
# следующие строки (нельзя включать кэширование без указания
# CacheRoot):
#
Часть VI. Приложения
534
# CacheRoot "C:/Program Files/Apache Group/Apache/proxy"
# CacheSize 5
# CacheGcInterval 4
# CacheMaxExpire 24
# CacheLastModifiedFactor 0.1
# CacheDefaultExpire 1
# NoCache a_domain.com another_domain.edu joes.garage_sale.com
#
# Конец настроек proxy-сервера.
### Раздел 3: Виртуальные хосты
#
# Директива VirtualHost: Если вы хотите держать на своей машине несколько
# хостов, следует для каждого из них завести контейнер VirtualHost.
# Прежде чем их устанавливать, обращайтесь за подробными разъяснениями к
# документации по адресу http://www.apache.org/docs/vhosts/. Для проверки
# конфигурации ваших виртуальных хостов вы можете задавать опцию -S
# командной строки.
#
# Если вы хотите использовать именные виртуальные хосты (name-based
# virtual hosts), вам необходимо определить для них как минимум один
# адрес IP (и номер порта).
#
NameVirtualHost 127.0.0.1:80
#
# Пример использования директивы VirtualHost:
# В контейнер VirtualHost может включаться почти любая
# директива Apache.
#
#
# ServerAdmin webmaster@host.some_domain.com
# DocumentRoot /www/docs/host.some_domain.com
# ServerName host.some_domain.com
# ErrorLog logs/host.some_domain.com-error_log
# CustomLog logs/host.some_domain.com-access_log common
#
#
Приложение 1. Файл конфигурации Apache httpd.conf
535
#
# Далее идут настройки для виртуальных хостов, описанных во второй
# части этой книги.
#----localhost
ServerAdmin webmaster@localhost.ru
ServerName localhost
DocumentRoot "z:/home/localhost/www"
ScriptAlias /cgi/ "z:/home/localhost/cgi/"
ErrorLog z:/home/localhost/error.log
CustomLog z:/home/localhost/access.log common
#----hacker
ServerAdmin webmaster@hacker.ru
ServerName hacker
DocumentRoot "z:/home/hacker/www"
ScriptAlias /cgi/ "z:/home/hacker/cgi/"
ErrorLog z:/home/hacker/error.log
CustomLog z:/home/hacker/access.log common
#----cracker
ServerAdmin webmaster@cracker.ru
ServerName cracker
DocumentRoot "z:/home/cracker/www"
ScriptAlias /cgi/ "z:/home/cracker/cgi/"
ErrorLog z:/home/cracker/error.log
CustomLog z:/home/cracker/access.log common
# Конец главного файла конфигурации Apache.
Приложение 2
Файл конфигурации
PHP php.ini
Приложение 2, которое вы видите перед собой, уважаемый читатель, включает пол-
ный перевод на русский язык комментариев внутри файла конфигурации PHP
php.ini.
Директивы в листинге П2.1 полностью соответствуют рекомендациям по уста-
новке PHP для Windows, представленным в части II книги. Впрочем, чтобы
получить этот файл, мне понадобилось всего пара изменений в настройках
PHP по умолчанию (настройки по умолчанию хранятся в файле php.inidist)
— не то, что в случае с Apache.
Если вы установили PHP как модуль Apache, перед вами открываются дополнитель-
ные возможности: вы можете задавать значения некоторых директив прямо в файлах
httpd.conf или .htaccess. В силу специфики синтаксиса файлов конфигурации
Apache, для отделения имени директивы и ее значения нужно использовать пробел,
а
не знак =. Кроме того, имена директив PHP должны быть предварены префиксом
php_. Например, директива из php.ini
auto_prepend_file=top.html
будет выглядеть в httpd.conf или .htaccess так:
php_auto_prepend_file top.html
Приведенного листинга с комментариями должно быть вполне достаточно для пони-
мания роли большинства директив PHP. Именно поэтому я уделил им так мало стра-
ниц в частях IV и V данной книги. И все-таки, если у вас возникнут какие-то
затруд-
нения, их легко сможет разрешить документация, которую можно получить,
например, с официального сайта PHP: http://www.php.net.
Листинг П2.1. Файл php.ini
[PHP]
;;;;;;;;;;;;;;;;;
; Об этом файле ;
Приложение 2. Файл конфигурации PHP php.ini 537
;;;;;;;;;;;;;;;;;
; Этот файл содержит большинство установок PHP. Чтобы PHP смог его
; обнаружить, он должен называться 'php.ini'. Интерпретатор ищет файл в
; текущем каталоге, в случае неудачи — в каталоге, указанном в
; переменной окружения PHPRC, и, наконец, в каталоге, заданном при
; компиляции и сборке PHP (именно в таком порядке).
; В системе Windows путь, указанный при компиляции PHP,
; соответствует каталогу Windows (в большинстве случаев это
; c:\windows). Папка, в которой будет производиться поиск файла
; 'php.ini', может быть также определена с использованием ключа –c
; командной строки.
;
; Синтаксис файла крайне прост. Пробельные символы (то есть, пробелы,
; символы табуляции и т. д.), строки, начинающиеся с точки с запятой (;)
; игнорируются (как вы, наверное, уже догадались). Заголовки секций
; (например, [Foo]) также пропускаются, но, возможно, будут учитываться
; в будущих версиях PHP.
;
; Директивы задаются примерно так:
; directive=value
; Имена директив чувствительны к регистру символов — foo=bar не то же
; самое, что FOO=bar.
;
; Значение value может быть строкой, числом, константой PHP (например,
; E_ALL или M_PI), одной из INI-констант (On, Off, True, False, Yes, No
; или None), выражением (например, E_ALL & ~E_NOTICE), а также строкой
; в кавычках ("foo").
;
; В выражениях могут использоваться только побитовые и логические
; операторы, а также скобки:
; | поразрядное ИЛИ (OR)
; & поразрядное И (AND)
; ~ поразрядное НЕ (NOT)
; ! логическое отрицание (NOT)
;
; В качестве логических флагов со значением "истина" могут быть
; использованы значения 1, On, True или Yes. Значение "ложь" дают 0, Off,
; False и No.
Часть VI. Приложения 538
;
; Пустая строка может быть задана, если "не указать ничего" после знака
; равенства, или же указать слово None:
; foo= ; устанавливаем foo равным пустой сторке
; foo=none ; аналогично
; foo="none" ; устанавливаем foo равным строке 'none'
;
; Если вы используете константы в качестве части значения директивы и эти
; константы определяются в каком-нибудь динамически загружаемом
; расширении (модуле PHP или Zend), вы можете указывать их только после
; строки, которая загружает расширение.
;
; Все значения в файле php.ini-dist соответствуют встроенным значениям
; по умолчанию. Если php.ini не задействуется, или же вы удалите из него
; некоторые строки, будут установлены значения по умолчанию.
;;;;;;;;;;;;;;;;;;;
; Настройки языка ;
;;;;;;;;;;;;;;;;;;;
; Разрешает работу PHP для сервера Apache.
engine=On
; Разрешает использовать короткие тэги . Иначе будут распознаваться
; только тэги .
short_open_tag=On
; Позволяет использовать тэги <% %> а-ля ASP.
asp_tags=Off
; Число значащих цифр после запятой, которые отображаются для чисел с
; плавающей точкой.
precision=14
; Признак коррекции дат (проблема 2000 года, которая может
; вызвать непонимание со стороны браузеров, которые
; на это не рассчитывают)
y2k_compliance=Off
; Использование буферизации вывода. Позволяет посылать заголовки (включая
Приложение 2. Файл конфигурации PHP php.ini 539
; Cookies) после вывода текста. Правда, это происходит ценой
; незначительного замедления вывода.
; Вы можете разрешить буферизацию во время выполнения сценария путем
; вызова функций буферизации, или же включить ее по умолчанию с помощью
; следующей директивы:
output_buffering=Off
; Директива неявной отсылки говорит PHP о том, что выводимые данные нужно
; автоматически передавать браузеру после вывода каждого блока данных.
; Ее действие эквивалентно вызовам функции flush() после
; каждого использования print() или echo() и после каждого HTML-блока.
; Включение этой директивы серьезно замедляет работу, поэтому ее
; рекомендуется применять лишь в отладочных целях.
implicit_flush=Off
; Параметр определяет, должен ли PHP использовать возможность всегда
; передавать аргументы функциям по ссылке при выполнении сценария.
; Этот метод устарел, и, скорее всего, он не будет
; поддерживаться в будущих версиях PHP/Zend.
; Описание того, каким способом должен быть передан аргумент —
; по ссылке или по значению — рекомендуется указывать при объявлении
; функции. Лучше всего, если вы попробуете установить параметр в Off
; и проверите, все ли сценарии по-прежнему работают. Если это так,
; то все в порядке, и сценарии будут совместимы и с будущими версиями
; PHP. В противном случае вы будете получать предупреждения каждый раз,
; когда аргументы передаются ненадлежащим образом и по значению там,
; где должны передаваться по ссылке.
allow_call_time_pass_reference=On
; Безопасный режим
safe_mode=Off
safe_mode_exec_dir=
; Установка некоторых переменных окружения может потенциально породить
; "дыры" в защите сценариев. Следующая директива содержит разделенный
; запятыми список префиксов. В режиме включенного безопасного режима
; пользователь сможет изменять только те переменные окружения, имена
; которых начинаются с перечисленных префиксов.
; По умолчанию пользователь имеет возможность устанавливать только
Часть VI. Приложения 540
; переменные окружения, начинающиеся с PHP_ (например,
; PHP_FOO=something).
; Замечание: если эта директива пуста, PHP позволяет пользователям
; модифицировать любые переменные окружения!
safe_mode_allowed_env_vars=PHP_
; Следующая директива содержит разделенный запятыми список имен
; переменных окружения, которые конечный пользователь не сможет изменять
; путем вызова putenv().
; Эти переменные будут защищены даже в том случае, если директива
; разрешает их использовать.
safe_mode_protected_env_vars=LD_LIBRARY_PATH
; Эта директива позволяет вам запрещать вызовы некоторых функций
; из соображений безопасности. Список задается в виде имен функций,
; разграниченных запятыми. Директива действует независимо от того,
; установлен ли безопасный режим или нет!
disable_functions=
; Цвета для режима раскраски синтаксиса. Любой цвет, допустимый в тэге
; , допустим и здесь.
highlight.string=#DD0000
highlight.comment=#FF8000
highlight.keyword=#007700
highlight.bg=#FFFFFF
highlight.default=#0000BB
highlight.html=#000000
; Другие директивы
; Следующая директива указывает, должен ли PHP добавлять заголовок
; X-Powered-by в заголовки, посылаемые браузеру, и, таким образом,
; обнаруживать себя. Это никак не может повлиять на безопасность
; сценария, однако позволяет пользователю определить, использовался
; ли PHP для генерации страницы, или нет.
expose_php=On
;;;;;;;;;;;;;;;;;;;;;;;;
Приложение 2. Файл конфигурации PHP php.ini 541
; Ограничения ресурсов ;
;;;;;;;;;;;;;;;;;;;;;;;;
; Максимальное возможное время выполнения сценария в секундах. Если
; сценарий будет выполняться дольше, PHP принудительно завершит его.
max_execution_time=30
; Максимальный объем памяти, выделяемый сценарию (8MB)
memory_limit=8M
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Обработка ошибок и подключений ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Директива error_reporting должна задаваться в виде битового
; поля. Его значение можно устанавливать с помощью следующих констант,
; объединенных оператором | (OR):
; E_ALL - Все предупреждения и ошибки.
; E_ERROR - Критические ошибки времени выполнения.
; E_WARNING - Предупреждения времени выполнения.
; E_PARSE - Ошибки трансляции.
; E_NOTICE - Замечания времени выполнения (это такие
; предупреждения, которые, скорее всего,
; свидетельствуют о логических ошибках в
; сценарии, — например, использовании
; неинициализированной переменной).
; E_CORE_ERROR - Критические ошибки в момент старта PHP.
; E_CORE_WARNING - Некритические предупреждения во время старта PHP.
; E_COMPILE_ERROR - Критические ошибки времени трансляции.
; E_COMPILE_WARNING - Предупреждения времени трансляции.
; E_USER_ERROR - Сгенерированные пользователем ошибки.
; E_USER_WARNING - Сгенерированные пользователем предупреждения.
; E_USER_NOTICE - Сгенерированные пользователем замечания.
; Пример:
; показывать все ошибки, за исключением замечаний
; error_reporting = E_ALL & ~E_NOTICE
; показывать только сообщения об ошибках
; error_reporting=E_COMPILE_ERROR|E_ERROR|E_CORE_ERROR
; отображать все ошибки, предупреждения и замечания
error_reporting= E_ALL
; Печать ошибок и предупреждений прямо в браузер.
Часть VI. Приложения 542
; Для готовых сайтов рекомендуется отключать следующую директиву и
; использовать вместо нее журнализацию (см. ниже). Включенная директива
; display_errors в "рабочих" сайтах может открыть доступ пользователю к
; секретной информации: например, полному пути к документу, используемой
; базе данных и т. д.
display_errors=On
; Даже если display_errors включена, ошибки, возникающие во время старта
; PHP, не отображаются. Рекомендуется устанавливать следующую директиву
; в выключенное состояние, за исключением случая, когда вы применяете
; ее при отладке.
display_startup_errors=Off
; Сохранять ли сообщения об ошибках в файле журнала. Журнал может
; определяться настройками сервера, быть связанным с потоком stderr
; или же задаваться директивой error_log, описанной ниже. Как уже было
; сказано, в коммерческих проектах желательно использовать именно
; журнализацию, а не отображать ошибки в браузер.
log_errors=Off
; Сохранять ли последнее сообщение об ошибке или предупреждение в
; переменной $php_errormsg
track_errors=On
; Строка, которая выводится перед сообщением об ошибке.
;error_prepend_string=""
; Строка, которая отображается после сообщения.
;error_append_string=" "
; Раскомментируйте, чтобы вести журнал в указанном файле.
;error_log=filename;
; Раскройте, чтобы использовать системный журнал.
;error_log=syslog
; Предупреждать, когда оператор + применяется к строкам.
warn_plus_overloading=Off
;;;;;;;;;;;;;;;;;;;;
; Обработка данных ;
;;;;;;;;;;;;;;;;;;;;
Приложение 2. Файл конфигурации PHP php.ini 543
; Замечание: track_vars всегда включена, начиная с PHP 4.0.3.
; Следующая директива определяет, в каком порядке PHP будет
; регистрировать данные, полученные методами GET, POST, а также
; переменные окружения и встроенные переменные (соответственно, значение
; задается буквами G, P, C, E и S, например, EGPCS или GPC). Регистрация
; производится на основе чтения этой строки слева направо, новые значения
; переопределяют старые.
variables_order="EGPCS"
; Должен ли PHP регистрировать EGPCS-переменные как глобальные
; переменные. Возможно, вы захотите отключить эту возможность, если не
; хотите "засорять" глобальную область видимости сценария. Это имеет
; смысл, если вы используете директиву track_vars — в этом случае вы
; можете получить доступ к GPC-данным через массив $HTTP_???_VARS.
; Желательно так писать сценарии, чтобы они по возможности
; старались обходиться без директивы register_globals. Использование
; данных, поступивших из формы, как глобальных переменных, потенциально
; может породить проблемы в защите сценария, если программист не особенно
; позаботится об их устранении.
register_globals=On
; Следующая директива указывает PHP, обязан ли он создавать переменные
; $argv и $argc на основе информации, поступившей методом GET. Если вы не
; используете эти переменные, отключите директиву register_argc_argv для
; небольшого убыстрения работы PHP.
register_argc_argv=On
; Максимальный размер данных POST, который PHP сможет принять.
post_max_size=8M
; Следующая директива устарела — используйте variables_order.
gpc_order="GPC"
; Автоматическая обработка кавычек и апострофов:
; использовать ли автокавычки для входящих GET/POST/Cookie данных
magic_quotes_gpc=Off
; заключать ли данные в автокавычки во время выполнения, например,
; для данных из SQL, exec() и т. д.
magic_quotes_runtime=Off
Часть VI. Приложения 544
; Нужно ли PHP оформлять автокавычки в стиле Sybase-style (заменять '
; на '', а не на \')
magic_quotes_sybase=Off
; Следующие директивы указывают PHP, содержимое каких файлов он должен
; обрабатывать до и после вывода сценария.
auto_prepend_file=
auto_append_file=
; Начиная с версии 4.0b4, PHP всегда сообщает браузеру об используемой
; кодировке в заголовке Content-type. Для того чтобы запретить это,
; просто установите следующую директиву пустой. По умолчанию
; используется text/html без указания кодировки.
default_mimetype="text/html"
;default_charset="iso-8859-1"
;;;;;;;;;;;;;;;;;;;
; Пути и каталоги ;
;;;;;;;;;;;;;;;;;;;
; Для UNIX: "/path1:/path2".
; Для Windows: "\path1;\path2"
include_path=
; Корневой каталог для PHP-сценариев.
; Игнорируется, если значение равно пустому "".
doc_root=
; Каталог, который PHP использует при открытии сценария вида
; /~username. Не оказывает действия, если значение равно "".
user_dir=
; Каталог, в котором хранятся динамически загружаемые расширения.
extension_dir=C:/Program Files/PHP4/extensions
; Следующая директива разрешает или запрещает использование функции dl().
; Функция dl() работает неправильно в многопоточных Web-серверах,
; например, в IIS или Zeus, и автоматически отключается для них.
enable_dl=On
Приложение 2. Файл конфигурации PHP php.ini 545
;;;;;;;;;;;;;;;;;;
; Закачка файлов ;
;;;;;;;;;;;;;;;;;;
; Разрешает PHP обрабатывать закачку файлов
file_uploads=On
; Каталог для временных файлов, в который PHP помещает закачанные
; файлы (используется системный временный каталог, если в директиве
; указана пустая строка)
;upload_tmp_dir=
; Максимальный размер закачанного файла
upload_max_filesize=2M
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Динамически загружаемые расширения ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Если вы хотите, чтобы какие-то модули загружались автоматически,
; задавайте директиву extension в формате:
; extension=modulename.extension
; Например, для Windows:
; extension=msql.dll
; или для UNIX:
; extension=msql.so
; Должно быть указано только имя, без пути. Чтобы задать
; каталог, в котором расположены расширения, используйте директиву
; extension_dir, описанную выше.
; Модули для Windows
; Замечание: поддержка MySQL и ODBC теперь включена в ядро PHP, так что
; для нее уже не нужны никакие библиотеки DLL.
;extension=php_cpdf.dll
;extension=php_cybercash.dll
;extension=php_db.dll
;extension=php_dbase.dll
;extension=php_domxml.dll
;extension=php_dotnet.dll
Часть VI. Приложения 546
;extension=php_exif.dll
;extension=php_fdf.dll
extension=php_gd.dll
;extension=php_gettext.dll
;extension=php_ifx.dll
;extension=php_imap.dll
;extension=php_interbase.dll
;extension=php_java.dll
;extension=php_ldap.dll
;extension=php_mhash.dll
;extension=php_mssql65.dll
;extension=php_mssql70.dll
;extension=php_oci8.dll
;extension=php_oracle.dll
;extension=php_pdf.dll
;extension=php_pgsql.dll
;extension=php_sablot.dll
;extension=php_swf.dll
;extension=php_sybase_ct.dll
;extension=php_zlib.dll
;;;;;;;;;;;;;;;;;;;;;;;;;
; Установки для модулей ;
;;;;;;;;;;;;;;;;;;;;;;;;;
[Syslog]
; Нужно или нет определять различные переменные Syslog, такие как
; $LOG_PID, $LOG_CRON и т. д. Для ускорения работы рекомендуется
; выключать следующую директиву. Во время выполнения сценария вы
; можете включить или выключить директиву путем вызова
; функции define_syslog_variables().
define_syslog_variables=Off
[mail function]
; Только для Win32 — используемый SMTP-сервер.
SMTP=mail.dklab.ru
; Только для Win32 — поле From: по умолчанию.
sendmail_from= dk@dklab.ru
Приложение 2. Файл конфигурации PHP php.ini 547
; Только для UNIX — задает путь и аргументы программы sendmail (по
; умолчанию — 'sendmail -t -i').
;sendmail_path=
[Debugger]
debugger.host=localhost
debugger.port=7869
debugger.enabled=False
[Logging]
; Следующие директивы используются сценарием-примером.
; При потребности в детальном описании см. examples/README.logging.
;logging.method=db
;logging.directory=/path/to/log/directory
[Java]
;java.class.path=.\php_java.jar
;java.home=c:\jdk
;java.library=c:\jdk\jre\bin\hotspot\jvm.dll
;java.library.path=.\
[SQL]
sql.safe_mode=Off
[ODBC]
;uodbc.default_db=Not yet implemented
;uodbc.default_user=Not yet implemented
;uodbc.default_pw=Not yet implemented
; Разрешает или запрещает устойчивые соединения
uodbc.allow_persistent=On
; Проверка доступности соединения перед его использованием.
uodbc.check_persistent=On
; Макс. число устойчивых соединений. -1 означает, что ограничений нет.
uodbc.max_persistent=-1
; Макс. число соединений (устойчивых + неустойчивых).
uodbc.max_links=-1
Часть VI. Приложения 548
; Установки для LONG-полей.
uodbc.defaultlrl=4096
; Установки для бинарных данных. 0 означает режим passthru, 1 – режим
; as is, 2 – преобразование в символы.
uodbc.defaultbinmode=1
; См. документацию по odbc_binmode и odbc_longreadlen для более
; детального разъяснения смысла директив uodbc.defaultlrl и
; uodbc.defaultbinmode.
[MySQL]
mysql.allow_persistent=On
mysql.max_persistent=-1
mysql.max_links=-1
; Порт по умолчанию для функции mysql_connect(). Если не задан, функция
; попытается использовать переменную $MYSQL_TCP_PORT или запись mysql-tcp
; в /etc/services, а также заданную во время компиляции PHP константу
; MYSQL_PORT (именно в таком порядке). К PHP для Win32 применимо только
; последнее.
mysql.default_port=
; Определяет имя сокета для локальных соединений MySQL. Если он не задан,
; использует встроенное значение по умолчанию.
mysql.default_socket=
; Хост по умолчанию для mysql_connect() (не работает в безопасном
режиме).
mysql.default_host=
; Пользователь по умолчанию (не работает в безопасном режиме).
mysql.default_user=
; Пароль по умолчанию (не работает в безопасном режиме).
; Замечание: идея хранить пароль в этом файле просто отвратительна. Любой
; пользователь, который может запускать PHP, сможет узнать пароль путем
; выполнения:
; echo cfg_get_var("mysql.default_password")
; Конечно, узнать пароль сможет также и пользователь, который имеет права
Приложение 2. Файл конфигурации PHP php.ini 549
; на чтение для файла php.ini.
mysql.default_password=
[mSQL]
msql.allow_persistent=On
msql.max_persistent=-1
msql.max_links=-1
[PostgresSQL]
pgsql.allow_persistent=On
pgsql.max_persistent=-1
pgsql.max_links=-1
[Sybase]
sybase.allow_persistent=On
sybase.max_persistent=-1
sybase.max_links=-1
;sybase.interface_file="/usr/sybase/interfaces"
; Максимальный уровень серьезности отображаемых ошибок.
sybase.min_error_severity=10
; Минимальный уровень серьезности отображаемых ошибок.
sybase.min_message_severity=10
; Режим совместимости со старыми версиями PHP 3.0.
; Если следующая директива установлена в On, PHP будет автоматически
; присваивать тип результату на основе его типа в Sybase, вместо того,
; чтобы преобразовывать полученные значения в строки. Этот режим
; совместимости, возможно, в будущем не будет поддерживаться, так что
; лучше исправьте свои сценарии, если вам он нужен.
sybase.compatability_mode=Off
[Sybase-CT]
sybct.allow_persistent=On
sybct.max_persistent=-1
sybct.max_links=-1
sybct.min_server_severity=10
sybct.min_client_severity=10
[bcmath]
Часть VI. Приложения 550
; Число десятичных цифр для всех bcmath-функций.
bcmath.scale=0
[browscap]
;browscap=extra/browscap.ini
[Informix]
ifx.default_host=
ifx.default_user=
ifx.default_password=
ifx.allow_persistent=On
ifx.max_persistent=-1
ifx.max_links=-1
; Если следующая директива установлена в On, выражение select возвращает
; содержимое поля типа text blob вместо его идентификатора.
ifx.textasvarchar=0
; Заставляет команду select возвращать значение поля типа byte blob
; вместо его идентификатора.
ifx.byteasvarchar=0
; Принуждает PHP удалять завершающие пробелы из колонок с типом char
; фиксированного размера. Может помочь пользователям Informix SE.
ifx.charasvarchar=0
; Если установлена, содержимое полей text и byte сохраняется в файле,
; вместо того, чтобы храниться в памяти.
ifx.blobinfile=0
; Если установлена в 0, значения NULL возвращаются как пустые строки,
; иначе они возвращаются как строки 'NULL'.
ifx.nullformat=0
[Session]
; Определяет режим хранения данных сессий.
session.save_handler=files
; Следующая директива задает аргумент, передаваемый save_handler-у.
; В случае режима сохранения в файлах здесь должен указываться каталог,
Приложение 2. Файл конфигурации PHP php.ini 551
; в который будут помещены файлы сессий.
session.save_path=C:\Program Files\PHP4\sessiondata
; Должен ли PHP использовать Cookies.
session.use_cookies=1
session.name=PHPSESSID
; Инициализировать ли сессии при старте.
session.auto_start=0
; Время жизни Cookie для сессии. Если до закрытия браузера, то 0.
session.cookie_lifetime=0
; Путь для Cookie с идентификатором сессии.
session.cookie_path=/
; Домен для Cookie с идентификатором сессии.
session.cookie_domain=
; Функция, используемая для сериализации данных. Значение php задает
; стандартную функцию.
session.serialize_handler=php
; Вероятность того, что при очередном запуске сценария, работающего с
; сессиями, будет вызвана функция "сборки мусора" для очистки сессий,
; которые пользователь уже покинул.
session.gc_probability=1
; После указанного здесь промежутка времени сохраненные
; данные будут удалены автоматически сборщиком мусора.
session.gc_maxlifetime=1440
; Проверять ли HTTP Referer на предмет того, не является ли ID сессии
; "фальшивым".
session.referer_check=
; Указывает, сколько байтов читать из файла.
session.entropy_length=0
;session.entropy_length=16
Часть VI. Приложения 552
; Файл, используемый для генерации идентификаторов сессии.
session.entropy_file=
;session.entropy_file=/dev/urandom
; Установите одно из значений nocache, private, public для определения
; аспектов кэширования HTTP.
session.cache_limiter=nocache
; Документ будет считаться устаревшим по истечении заданного
; здесь количества минут
session.cache_expire=180
; Использовать ли поддержку "переходящих" SID. Действует, если PHP был
; скомпилирован с включенной опцией --enable-trans-sid.
session.use_trans_sid=1
[MSSQL]
;extension=php_mssql.dll
mssql.allow_persistent=On
mssql.max_persistent=-1
mssql.max_links=-1
mssql.min_error_severity=10
mssql.min_message_severity=10
; Режим совместимости со старыми версиями PHP 3.0.
mssql.compatability_mode=Off
[Assertion]
;assert.active=On
; Генерирует предупреждения PHP для каждых неудавшихся проверок
; выражений.
;assert.warning=On
; По умолчанию не завершать программу в случае неудачи.
;assert.bail=Off
; Пользовательская функция, которая будет вызвана при неудаче проверки.
;assert.callback=0
; Вычислять выражения в eval с использованием текущих установок
Приложение 2. Файл конфигурации PHP php.ini 553
; error_reporting. Установите в true, если вы хотите, чтобы действие
; режима error_reporting(0) было сохранено и при переходе через
; границу eval().
;assert.quiet_eval=0
[Ingres II]
ingres.allow_persistent=On
ingres.max_persistent=-1
ingres.max_links=-1
; База данных по умолчанию (формат: [node_id::]dbname[/srv_class]
ingres.default_database=
ingres.default_user=
ingres.default_password=
[Verisign Payflow Pro]
pfpro.defaulthost="test.signio.com"
pfpro.defaultport=443
pfpro.defaulttimeout=30
; IP-адрес proxy-сервера по умолчанию (если требуется).
; pfpro.proxyaddress=
; Порт proxy-сервера по умолчанию
; pfpro.proxyport=
; Логин для proxy-сервера по умолчанию
; pfpro.proxylogon=
; Пароль для proxy-сервера по умолчанию
; pfpro.proxypassword=
Предметный указатель
A
Apache 79
B
basic-авторизация 74
C
Cookies 67
D
DNS 16
DriveSpace 83
G
GD 316
H
HTML 27
HTTPS 26
I
IP-адрес 15
M
MySQL 361
N
Name-based хосты 88
P
PCRE 205
S
self-redirect 45
SQL 363
stdin 51
stdout 43
subst 82
T
timestamp 280
U
URI 32
URL 25
Предметный указатель 556
А
Авторизация 73
Б
База данных 361
Базовая точка строки 328
Бинарный режим 245
В
Взаимная блокировка 274
Виртуальные хосты 88
Г
Григорианский календарь 283
Группы сессий 349
Д
Директивы Apache 509
Ж
Жесткие ссылки 276
З
Заголовки 31
Записи 361
И
Идентификатор сессии 346
Исключительная блокировка 262
К
Квантификаторы 302
Код ответа сервера 44
Контейнеры 509
Л
Локали 217
М
Мнимые символы 304
О
Обработчики сессии 347
П
Палитра 321
Переменные окружения 31
Поля 361
Последовательность слабо связанных
точек 325
Р
Разделяемая блокировка 262
Регулярные выражения 298
С
Сессия 345
Сильно связанный путь 324
Символическая ссылка 275
Стандартный
поток ввода 51
поток вывода 43
Сценарий 29
Т
Таблица 361
Текущий каталог 268
Дмитрий Котеров
САМОУЧИТЕЛЬ
PHP 4
Дюссельдорф w Киев w Москва w Санкт-Петербург
УДК 681.3.06
Учебное пособие по использованию языка PHP версии 4 содержит обширную
информацию о
приемах, призванных в кратчайшие сроки сделать новичка, владеющего хотя бы
одним
алгоритмическим языком, Web-программистом. Рассматриваются основы протоколов
HTTP и
CGI, схемы разработки крупных сценариев на PHP, синтаксис языка и работа с
простейшими
функциями, объектно-ориентированное программирование на PHP с применением
идеологии
интерфейсов, манипуляции со строками и массивами, создание баз данных и многое
другое.
Для программистов и Web-разработчиков
Группа подготовки издания:
Главный редактор Екатерина Кондукова
Зав. редакцией Наталья Таркова
Редактор Евгений Васильев
Компьютерная верстка Натальи Смирновой
Корректор Наталия Першакова
Дизайн обложки Игоря Цырульникова
Зав. производством Николай Тверских
[Весь Текст] Страница: из 287