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

Самоучитель по PHP 4

.pdf
Скачиваний:
82
Добавлен:
02.05.2014
Размер:
4.36 Mб
Скачать

Глава 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<count($Lines); $i++) { $l=&$Lines[$i];

//Проставляем текущую кодировку у письма. Для этого

//проверяем, задан ли в нем заголовок Content-type и,

//если задан, то модифицируем его, а если нет —

//добавляем этот заголовок в начало и конец письма. if(eregi("^Content-type:",$l)) {

if(eregi("charset *=",$l)) $l=eregi_Replace("charset *= *[^;,\n]+",$chs,$l);

else

$l.="; $chs"; $HasContType++;

}

//Проверяем значение поля "to" в письме — там может быть имя

//получателя. В этом случае добавляем к нему еще и адрес. if(eregi("^to:([^\r\n]*)",$l,$regs)) {

$to=trim($regs[1])." <$to>"; $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;

492 Часть V. Приемы программирования на PHP

// В 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: Система рассылки <subscribe@ourserver.ru> To: <?=$User['name']?>.

Subject: Свежие новости

Content-type: text/plain ~StartOfMail

Уважаемый <?=$User['name']?>!

Вы подписались на наш лист рассылки <?=date("d.m.Y",$User['time'])?>.

Предлагаем Вашему вниманию последние новости.

---------------------------------------------------------------

<?foreach($News as $k=>$v) {?> <?=WordWrap($v,60)?>.

<?}?>

Как видим, шаблон практически ничем не отличается от небольшого сценария на PHP. Он получает данные из переменных $User (данные пользователя) и $News (блоки новостей), которые должны устанавливаться запускающей программой. Вскоре мы рассмотрим процедуру более подробно, а пока обратите внимание на некоторые моменты при написании этого шаблона.

rМы указали заголовок Content-type сразу в двух местах шаблона — в начале и конце. В силу рассуждений, приведенных в главе 20, это необходимо для того, чтобы помочь некоторым "недогадливым" почтовым программам в определении кодировки письма.

494

Часть V. Приемы программирования на PHP

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.

498 Часть V. Приемы программирования на PHP

$Prg=array(

"Todo"

=> array($Root),

// для накопления путей необработанных файлов

"Res"

=> array()

// результат обработки всех файлов

);

 

 

//Пытаемся загрузить текущее состояние. Если не получается,

//значит, обход только что начался.

if($f=@fopen($cache,"rb")) { if(@flock($f,LOCK_SH)) {

$Prg=Unserialize(fread($f,filesize($cache))); fclose($f);

}

}

//Обходим сайт — по одной итерации цикла на каждый файл или

//каталог. Найденные файлы добавляются в конец массива

//$Prg['Res'], а подвергающиеся обработке — извлекаются из его

//начала. Таким образом, мы продолжаем процесс до тех пор,

//пока не будут "пройдены" все файлы на сервере.

do {

// очередное полное имя файла $fname=array_shift($Prg['Todo']);

//если это не файл и не каталог, пропускаем if(!@is_file($fname) && !@is_dir($fname)) continue;

//если это каталог, добавляем все его содержимое if(@is_dir($fname)) {

$Files=array();

for($d=openDir($fname); $e=readDir($d); ) { if($e=="."||$e=="..") continue; $Files[]="$fname/$e";

}

closeDir($d);

//вставляем в начало массива, чтобы на следующей итерации

//цикла обрабатывались именно эти файлы

$Prg['Todo']=array_merge($Files,$Prg['Todo']);

}

//вызываем функцию для обработки очередного файла или каталога $Func($fname,$Prg['Res']);

//выходим, если время истекло, или же необработанных

//файлов не осталось.

Глава 33. Разные советы

499

} while(time()-$Start<$time && count($Prg['Todo']));

//Вернуть текущий результат в $Result. $Result=$Prg['Res'];

//Если еще есть файлы для обработки, сохранить состояние. if(count($Prg['Todo'])) {

//Сохраняем текущее состояние. В следующий раз мы начнем с него. $f=fopen($cache,"a+b");

flock($f,LOCK_EX); ftruncate($f,0); fwrite($f,Serialize($Prg)); fflush($f); fclose($f);

return true; // процесс продолжается

}

// Иначе процесс закончился. Удалить файл состояния. @unlink($cache);

return false;

}

?>

Я не буду приводить здесь реальный сценарий для построения карты сервера, потому что он слишком велик и, к тому же, довольно однообразен и неинтересен. Вся "изюминка" заключена именно в функции WalkSite(). Листинг 33.2 содержит небольшую "демонстрацию" ее возможностей. Сценарий собирает сведения о размере каждого файла сайта, печатая на каждом этапе имена обработанных объектов, а затем выводит сводную информацию.

Листинг 33.2. Демонстрация возможностей функции WalkSite(): demo.php

<?

//Подключаем библиотекаря "прямым" способом. include "$DOCUMENT_ROOT/php/Librarian.phl";

//Подключаем модуль с функцией WalkSite(). Uses("SiteWalker");

//Эта функция будет вызываться для каждого файла на сервере.

//Ее задача — добавить обработанные данные из этого файла

//в массив $Result (формат определяется назначением этих данных). function Walk($fname,&$Result)

{ // для диагностики выводим имя файла

500

Часть V. Приемы программирования на PHP

print ">$fname<br>";

// в качестве примера — просто добавляем имя файла в массив $Result[]="$fname: <b>".filesize($fname)."</b>";

}

// Если WalkSite() вернула false, значит, процесс закончился. if(!WalkSite($DOCUMENT_ROOT,"Walk","map",0,$Result)) {

//В качестве примера просто выводим содержимое массива,

//сформированного вызовами функции Walk(). Реальный код

//должен был бы вырабатывать HTML-представление карты,

//данные которой накоплены в $Result.

print "<hr>";

print join("<br>\n",$Result);

}else {

//для примера заставляем страницу обновить саму себя,

//имитируя многократные посещения пользователей.

print "<meta http-equiv=refresh content='0; url=$SCRIPT_NAME'>";

}

?>

В этом сценарии функции WalkSite() передается 0 как значение размера кванта времени, в течение которого можно собирать данные о сайте. Это означает, что файлы будут обрабатываться по одному при каждом запросе. В реальном коде карты сервера, конечно, это не так — нужно указывать приемлемый промежуток времени, чтобы в него "уложилась" обработка сразу нескольких страниц. Чем меньше будет этот промежуток, тем менее заметным для пользователя станет замедление, связанное с работой сценария, но тем значительнее будут "накладные расходы", вызванные работой функций сериализации. Так что тут нужно выбирать некоторый "средний" вариант. Проще всего это сделать опытным путем — например, так, чтобы примерно за час при известной посещаемости успевала перестроиться вся карта сервера.

Функция WalkSite() из листинга 33.2 работает с файлами, устанавливая на них рекомендательные блокировки. Этот процесс хоть и позволяет обойти про- блемы с разделением доступа к файлам, немного сложен для понимания. Он подробно описан в главе 15 части IV.

Использование самопереадресации

Термин самопереадресация (или, в английском варианте, self-redirect) означает свойство сценария подавать в браузер клиента запрос, заставляющий его (браузер) заново