Самоучитель PHP 4 - Котеров Д. В.
.pdf
488 |
Часть V. Приемы программирования на PHP |
//в файле $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",
490 |
Часть V. Приемы программирования на PHP |
"Щ"=>"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: Вася <vasya@pupkin.ru>"). Если работа происходит
//в 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<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.
