
Самоучитель по PHP 4
.pdf
Глава 19. Работа с датами и временем |
281 |
С функцией microtime() связано одно недоразумение. Дело в том, что мил- лисекунды в различных ОС выглядят по-разному. Например, в Unix это дейст- вительно число микросекунд, а в Windows — непонятное значение, связанное неизвестно с чем. Возможно, оно все же несет какой-то смысл, но мне до него "докопаться", увы, не удалось.
int mktime([int $hour] [,int $minute] [,int $second] [,int $month] [,int $day] [,int $year])
До сих пор мы рассматривали функции, которые преобразуют формат timestamp в представление, удобное для человека. Существует всего одна функция, которая проводит обратное преобразование — mktime(). Как мы видим, все ее параметры необязательны, но пропускать их можно, конечно же, только справа налево. Если какието параметры не заданы, на их место подставляются значения, соответствующие текущей дате. Функция возвращает значение timestamp, соответствующее указанной дате.
Правильность даты, переданной в параметрах, не проверяется. В случае некорректной даты ничего особенного не происходит — функция "делает вид", что это ее не касается, и формирует соответствующий timestamp. Для иллюстрации рассмотрим три вызова (два из них — с ошибочной датой), которые тем не менее возвращают один и тот же результат:
echo date("M-d-Y", mktime(0,0,0,1,1,1998)); // правильная дата echo date("M-d-Y", mktime(0,0,0,12,32,1997)); // неправильная дата echo date("M-d-Y", mktime(0,0,0,13,1,1997)); // неправильная дата
Легко убедиться, что выводятся три одинаковых числа.
Работа с датами
string date(string $format [,int $timestamp])
Эта функция крайне полезна и весьма универсальна. Она возвращает строку, отформатированную в соответствии с параметром $format и сформированную на основе параметра $timestamp (если последний не задан — то на основе текущей даты). Строка формата может содержать обычный текст, перемежаемый одним или несколькими символами форматирования:
rU — количество секунд, прошедших с полуночи 1 января 1970 года;
rz — номер дня от начала года;
rY — год, 4 цифры;
ry — год, 2 цифры;
rF — название месяца, например, January;
282 |
Часть IV. Стандартные функции PHP |
rm — номер месяца;
rM — название месяца, трехсимвольная аббревиатура, например, Jan;
rd — номер дня в месяце, всегда 2 цифры (первая может быть 0);
rj — номер дня в месяце без предваряющего нуля;
rw — день недели, 0 соответствует воскресенью, 1 — понедельнику, и т. д.;
rl — день недели, текстовое полное название, например, Friday;
rD — день недели, английское трехсимвольное сокращение, например, Fri;
ra — am или pm;
rA — AM или PM;
rh — часы, 12-часовой формат;
rH — часы, 24-часовой формат;
ri — минуты;
rs — секунды;
rS — английский числовой суффикс (nd, th и т. д.).
Те символы, которые не были распознаны как форматирующие, подставляются в результирующую строку "как есть". Впрочем, не советую этим злоупотреблять, поскольку довольно мало английских слов не содержат ни одной из перечисленных выше букв.
Как видите, набор символов форматирования весьма и весьма богат. Вот пример применения функции date():
echo date("l dS of F Y h:i:s A"); echo date("Сегодня d.m.Y");
echo date("Этот файл датирован d.m.Y",filectime("myfile"));
Остается еще отметить, что формат выдачи для таких символов, как F (название месяца), зависит от текущих настроек локали (см. функцию setlocale()) и вполне может быть названием месяца на родном языке пользователя.
int checkdate(int $month, int $day, int $year)
Эта функция проверяет, существует ли дата, переданная ей в параметрах: вначале ищется месяц, затем — день, и, наконец, — год. Конкретнее, checkdate() проверяет следующее:
rгод должен быть между 1900 и 32 767 включительно;
rмесяц обязан принадлежать диапазону от 1 до 12;
rчисло должно быть допустимым для указанного месяца и года (если год високосный).
Глава 19. Работа с датами и временем |
283 |
Функция очень полезна, например, при автоматическом формировании HTMLкалендарика для указанного месяца и года. В самом деле, мы можем определить, какие числа в месяце "не существуют", и для них вместо номера проставить пустое место.
array getdate(int $timestamp)
Возвращает ассоциативный массив, содержащий данные об указанном времени. В массив будут помещены следующие ключи и значения:
rseconds — секунды;
rminutes — минуты;
rhours — часы;
rmday — число;
rwday — день недели, число;
rmon — номер месяца;
ryear — год;
ryday — номер дня с начала года;
rweekday — полное название дня недели, например, Friday;
rmonth — полное название месяца, например, January.
В общем-то, всю эту информацию можно получить и с помощью функции date(), но тут разработчики PHP предоставляют нам альтернативный способ.
Григорианский календарь
Григорианский календарь — это как раз тот самый календарь, который мы постоянно используем в своей жизни. В России он был введен Петром I в 1700 году.
Описываемые далее три функции представляют большой интерес, если вам понадобится автоматически формировать календари в сценариях. Все они имеют дело с так называемым Julian Day Count (JDC). Что это такое?
Каждой дате соответствует свой JDC. Ведь, фактически, JDC — это всего лишь число дней, прошедших с определенной даты (кажется, где-то с 4714-го года до нашей эры).
Зачем это нужно? Например, нам заданы две даты в формате "дд.мм.гггг". Нужно вычислить количество дней между этими датами. Поставленная задача как раз легко решается через перевод обеих дат в JDC и определение разности получившихся величин.
int GregorianToJD(int $month, int $day, int $year)
Преобразует дату в формат JDC. Допустимые значения года для григорианского календаря — от 4714 года до нашей эры до 9999 года нашей эры.
string JDToGregorian(int $julianday)
284 |
Часть IV. Стандартные функции PHP |
Преобразует дату в формате JDC в строку, выглядящую как месяц/число/год. Наверняка затем вы захотите разбить эту строку на составляющие, чтобы работать с ними по отдельности. Для этого воспользуйтесь функцией explode():
$jd = GregorianToJD(10,11,1970); echo "$jd<br>\n";
$gregorian = JDToGregorian($jd); echo "$gregorian<br>\n"; $list=explode($gregorian,"/");
mixed JDDayOfWeek(int $julianday, int $mode)
Последняя функция этой серии — JDDayOfWeek() — тоже совершенно незаменима: она возвращает день недели, на который приходится указанная JDC-дата. Фактически, это единственное, чего нам не хватало бы для формирования календарика. Параметр $mode задает, в каком виде должен быть возвращен результат:
r0 — номер дня недели (0 — воскресенье, 1 — понедельник, и т. д.);
r1 — английское название дня недели;
r2 — сокращение английского названия дня недели.
В PHP существует еще множество функций для работы с другими календарями — в том числе с Республиканским, Юлианским и т. д. Объем книги не позволяет привести здесь их описания.

Глава 20
Посылка писем через PHP
Одно из самых мощных средств PHP — возможность автоматической посылки писем по электронной почте, минуя использование внешних программ и утилит. Функция отсылки встроена в PHP. С нее мы и начнем.
Функция отправки письма
bool mail(string $to, string $subject, string $msg [,string $headers])
Функция mail() посылает сообщение с телом $msg (это может быть "многострочная строка", т. е. переменная, содержащая несколько строк, разделенных символом перевода строки) по адресу $to. Можно задать сразу нескольких получателей, разделив их адреса пробелами в параметре $to. Пример:
mail("rasmus@lerdorf.on.ca ca.ok@oklab.ru, "My Subject",
"Line 1\nLine 2\nLine 3"
);
В случае, если указан четвертый параметр, переданная в нем строка вставляется между концом стандартных почтовых заголовков (таких как To, Content-type и т. д.) и началом текста письма. Обычно этот параметр используется для задания дополнительных заголовков письма. Пример:
mail("ssb@guardian.no dk@dizain.ru", "the subject",
"Line 1\nLine 2\nLine 3",
"From: webmaster@$SERVER_NAME\n". "Reply-To: webmaster@$SERVER_NAME\n". "X-Mailer: PHP/" . phpversion()
);
Необходимо добавить, что этот пример довольно-таки неказист. Гораздо лучше было бы включить указанные заголовки прямо в тело письма $msg (в начало тела), отделив их от самого письма пустой строкой (прямо как в стандарте HTTP). То же самое применимо и к параметру $subject: лучше задавать в нем всегда пустую строку и ука-

286 |
Часть IV. Стандартные функции PHP |
зывать заголовок Subject в самом письме. Всегда старайтесь поступать таким образом. Далее будет ясно, зачем.
Проблема с кодировками
Думаю, не надо напоминать, что русских кодировок существует великое множество. Поэтому от того, насколько умело вы перекодируете письмо перед его отсылкой, зависит, прочтет ли его получатель или, махнув рукой, отправит в корзину, даже не попытавшись установить в своем почтовом клиенте нужную кодировку. Эта глава как раз и призвана раз и навсегда решить проблему кодировок. Если вы воспользуетесь советами, изложенными в ней, ваши письма всегда будут читаемыми.
Посылка в указанной кодировке
Сначала давайте договоримся об одном соглашении: функции mail() передавать только адрес получателя и текст письма. Ни заголовков, ни темы — и то и другое должно присутствовать в самом письме. Например:
$message=
"From: Лист рассылки To: Иванов Иван Иванович
Subject: Пробная рассылка
Content-type: text/plain; charset=windows-1825
Уважаемый товарищ! Это письмо послано почтовым роботом. Всего хорошего!"; Mail("ivanov@ivan.ivanovich.ru","",$message);
Обратите внимание на заголовок Content-type (в некоторых системах он обязательно должен стоять последним — проверьте это экспериментально). Он задает, что, во-первых, письмо доставляется как простой текст (text/plain), а во-вторых, что его кодировка — Windows. Теперь письмо всегда можно будет прочитать, даже если почтовая программа клиента по умолчанию настроена на китайскую кодировку.
И почему некоторые программы так не делают, а посылают письма без указа- ния их кодировки? Неужели им жалко лишнего десятка байтов для названия кодировки?
Обратите внимание на то, что тело письма отделяется от заголовков пустой строкой, с тем, чтобы почтовая программа могла понять, где кончаются заголовки и начинается тело.
Глава 20. Посылка писем через PHP |
287 |
Динамическая смена кодировки
Приведенное в предыдущем примере письмо можно будет прочитать в 90% существующих почтовых программ. Для "удовлетворения" остальных желательно посылать письма не в Windows-кодировке, а в KOI-8R. Для перекодирования можно воспользоваться уже известной нам функцией convert_cyr_string(). И, конечно, нужно в Content-type заменить charset на koi8-r. Вот что у нас получится:
$message=
"From: Лист рассылки
To: Иванов Иван Иванович <ivanov@ivan.ivanovich.ru> Subject: Пробная рассылка
Content-type: text/plain; charset=koi8-r
Уважаемый товарищ! Это письмо послано почтовым роботом. Всего хорошего!"; $message=convert_cyr_string($message,"w","k"); Mail("ivanov@ivan.ivanovich.ru","",$message);
Теперь вы понимаете, почему я говорил о том, чтобы все заголовки и Subject находились внутри тела письма? Этим мы достигаем того, что одной командой convert_cyr_string() перекодируется сразу все письмо, включая поля From, To, Subject и др. Иначе пришлось бы применять эту функцию дополнительно для перекодировки параметров $subject и $headers...
Проблема с заголовками
Есть одна проблема, возникающая при подобном использовании заголовка Contenttype. Дело в том, что существуют почтовые программы, которые понимают заголовок Content-type, но не понимают русский текст в поле Subject, если это поле стоит до Content-type. В то же время, другие почтовые клиенты обязывают нас задавать Content-type последним заголовком в списке. Чтобы обойти этот заколдованный круг, проще всего разместить поле Content-type сразу в двух местах — перед первым и после последнего заголовка:
$message=
"Content-type: text/plain; charset=koi8-r From: Лист рассылки
To: Иванов Иван Иванович <ivanov@ivan.ivanovich.ru> Subject: Пробная рассылка
Content-type: text/plain; charset=koi8-r
Уважаемый товарищ! Это письмо послано почтовым роботом. Всего хорошего!"; $message=convert_cyr_string($message,"w","k"); Mail("ivanov@ivan.ivanovich.ru","",$message);
288 |
Часть IV. Стандартные функции PHP |
Да, это может показаться весьма искусственным приемом, но зато работает "на ура". Теперь вы можете быть уверены, что ваше письмо прочитает любой пользователь (особенно если оно послано в кодировке KOI8), даже если его почтовая программа вообще не настроена ни на какую кодировку. Можете похвастать этим достижением перед начальством, предложив ему поставить у себя в Outlook Express по умолчанию японскую кодировку, а затем попросив принять письмо, сгенерированное роботом по указанной схеме.
Перспективы: создание "умной" функции для отправки писем
Возможно, вам уже пришла в голову идея сделать универсальную функцию для рассылки писем — чтобы она сама добавляла к полю To в письме E-mail в угловых скобках (как в примере выше), проставляла нужную кодировку у письма (которая задается в параметрах при вызове функции), ну и т. д. Это вполне осуществимо. Функция может выглядеть, например, так:
bool PostMail(string $ToAddress, string $Encode, string $Message)
Посылает письмо $Message по адресу $ToAddress, перекодировав его предварительно в кодировку, заданную в $Encode. Параметр $Encode может принимать следующие значения:
rw — Windows
rk — KOI8-R
rm — Mac
ri — Iso Latin
rt — Translit
В письме автоматически проставляется Content-type...charset (если заголовок Content-type уже присутствует в письме, то он не портится, а просто у него меняется поле charset на нужное значение). Также корректируется поле To в письме. Одновременно правильно обрабатываются вставки PHP-кода в тело письма (можно использовать глобальные переменные и оператор echo). Для этого, как обычно, применяются "скобки" <? и ?>.
Реализацию поставленной задачи мы отложим до части V, где описаны и другие приемы, облегчающие работу на PHP. Для этого нам понадобится техника регулярных выражений, которыми мы вскоре займемся, а также еще некоторые навыки. Если вы уже сейчас хотите использовать функцию PostMail(), можете сразу открыть часть V книги и скопировать оттуда ее исходный код на PHP.

Глава 21
Работа с WWW
Мы уже рассматривали основы протокола HTTP в части I книги. Оператор echo, предназначенный для вывода данных в браузер, нам также хорошо знаком. Теперь осталось лишь разобрать, какие средства предусмотрены в PHP для работы с заголов-
ками HTTP.
Установка заголовков ответа
Первая функция, которую мы сейчас рассмотрим, — Header() — предназначена для посылки заголовка браузеру (точнее, для добавления заголовка к документу, пересылаемому браузеру). Она может быть вызвана только до первой команды вывода сценария (конечно, если вы до этого не воспользовались функцией буферизации ob_start()).
Вывод заголовка
int Header(string $string)
Обычно функция Header() является одной из первых команд сценария. Она предназначена для установки заголовков ответа, которые будут переданы браузеру — по одному заголовку на вызов. Не забывайте, что вызов Header() обязательно должен осуществляться до любого оператора вывода в сценарии — в противном случае вы получите предупреждение. Заметьте, что текст вне <? и ?> также рассматривается как оператор вывода, а потому старайтесь не делать лишних пробелов до первой "скобки" <? в сценарии (и в особенности в файле, который этим сценарием включается) и, конечно, после последнего ограничителя ?> во включаемом файле. Впрочем, вы сможете легко обнаружить подобную ошибку, если выставите уровень контроля ошибок, равный 15 (1+2+4+8) — в этом случае при недопустимом вызове Header() вы получите предупреждение. Пример:
//перенаправляет браузер на сайт PHP Header("Location: http://www.php.net");
//теперь принудительно завершаем сценарий, ввиду того, что после
//перенаправления больше делать нечего
exit;

290 |
Часть IV. Стандартные функции PHP |
Запрет кэширования
Еще одно полезное приложение функции Header() — запрет кэширования документа браузером и Proxy-серверами. Большинство сценариев формируют документы, которые при каждом запуске программы изменяются. Очевидно, если браузер пользователя начнет кэшировать такие документы, ничего хорошего не получится. Выход — использовать в начале сценария следующие команды:
Header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); |
// Дата в прошлом |
Header("Cache-Control: no-cache, must-revalidate"); |
// HTTP/1.1 |
Header("Pragma: no-cache"); |
// HTTP/1.0 |
Header("Last-Modified: ".gmdate("D, d M Y H:i:s")."GMT");
Самое неприятное то, что для полного запрета кэширования приходится всегда посылать 4 указанных заголовка, и ни один пропустить нельзя — в противном случае не сработает либо браузер, либо Proxy-сервер. Так что рекомендую оформить их все в виде функции (например, с именем NoCache()) и затем вызывать эту функцию в нужный момент.
Получение заголовков запроса
Для получения всех заголовков запроса (того самого запроса, что вынудил запуститься сценарий) следует воспользоваться функцией GetAllHeaders():
array GetAllHeaders()
Функция GetAllHeaders() возвращает ассоциативный массив, содержащий данные о HTTP-заголовках запроса клиента, породившего запуск сценария. Ключи массива содержат названия заголовков, а значения — их величины.
Вот пример использования этой функции:
$headers = GetAllHeaders(); foreach($headers as $header=>$value)
echo "$header: $value<br>\n";
Функция GetAllHeaders() поддерживается PHP только в том случае, если он установлен в виде модуля Apache. В противном случае этой функции про- сто не будет (да и не может быть, потому что обычный CGI-сценарий не имеет доступа к заголовкам запроса). В частности, в PHP для Windows (который ча- ще всего реализуют именно в виде сценария) функция GetAllHeaders() не- доступна.
Не стоит увлекаться вызовами GetAllHeaders(): часто интересующую информацию (такую, например, как название браузера) можно получить и через переменные