Работа со временем в PHP/MySQL

версия для печати

Ничто не постоянно, даже время :) Особенно если речь идет о времени в PHP/MySQL. Когда я первый раз столкнулся с проблемой преобразования времени в PHP, я долго копался в мануалах и инете, но ни как не мог поймать суть. В итоге я все же нашел свою ошибку, все подробно для себя описал в txt-файле и забыл. Сейчас (спустя полтора года) опять возникли запарки, поднимаю текстовик.. и ничего не могу разобрать. Пришло время систематизировать записи про время (вот такой каламбурчик ;))

Перейти к части про MySQL

Время в PHP
Установка временной зоны (часового пояса)

Узнать как правильно пишется нужная временная зона можно так: <? print_r(DateTimeZone::listIdentifiers()); ?> Получим полный список временных зон, зашитый в текущюю сборку php. я для примеров беру временную зону Asia/Irkutsk (GMT+8:00). Кстати, нулевой часовой пояс в программах обозначается "GMT".

Установка временной зоны php.ini: date.timezone = "Asia/Irkutsk" Установка временной зоны .htaccess: php_value date.timezone "Asia/Irkutsk"

Unix-системы при выполнении скриптов могут определять текущее время на основе указанного местоположения через переменную окружения TZ (имеются в виду переменные окружения скрипта). При этом учитывается перевод часов на летнее время. Установка через переменную окружения в .htaccess: SetEnv TZ Asia/Irkutsk Установка переменной окружения через PHP (работает так же в Win-системах): <? putenv("TZ=Asia/Irkutsk"); ?>

Начиная с 5-х версий PHP есть такая функция (ее вызов установит временную зону для скрипта на время его выполнения): <? date_default_timezone_set("Asia/Irkutsk"); ?>

Функции

После установки временной зоны функции типа date(), localtime() и т.д. будут возвращать значения с учетом установленной временной зоны.

Какая бы временная зона не была, функция gmdate() возвращает дату/время по Гринвичу, т.е. GMT+0:0 (вообще так не пишется, для понятности приписал "+0:0"). Функция time() возвращает возвращает текущую метку времени Unix (формат UTC, timestamp) не зависимо от установленного часового пояса. Форматы времени GMT и UTC - абсолютные и используются, как эталоны отсчета даты/времени. Разница между ними в том, что:

  • GMT считает дату/время от рождества Христова (от начала эпохи). Данные представляются примерно как dd.mm.yyyy hh:mm:ss (в развисимости от региональных настроек)
  • UTC считает от 1 января 1970 00:00:00 GMT (от начала эпохи Unix). Данные представляются числом секунд, прошедших с точки отчета (больше целое число)

Собственно, любые функции конвертации форматов GMT<->UTC корректно все это учитывают и переводят. Итак, можно считать GMT/UTC за абсолютное значение времени.

Когда я разбирался в разнице функций, я запускал скрипты на localhost (на локальном компе с PHP-машиной). Так вот PHP у меня работал в нулевом часовом поясе, т.е. по Гринвичу! Поэтому не было разницы в возращаемых значениях функций даты/времени, и поэтому до меня не доходил смысл справок по этим функциям. После явного указания часового пояса все встало на свои места.

Вот этот код должен показать, как все происходит со временем в разных функциях:

date_default_timezone_set('Asia/Irkutsk');          //Сменили часовой пояс на GMT+8:0
$timestamp=time();                                  //UTC-абсолютное значение даты/времени в данный момент.
echo gmdate("M d Y H:i:s", $timestamp).' - GMT<br>';//GMT-абсолютное время (конвертация UTC->GMT)
echo date("M d Y H:i:s",$timestamp).' - local<br>'; //Форматированное "местное" время.
$t=localtime ($timestamp,0);                        //"Местное" время, расчитанное от UTC-времени
echo "localtime: $t[2]:$t[1]:$t[0]<br>";            //Его форматированный вывод

Результат:

Aug 18 2011 02:56:03 - GMT
Aug 18 2011 11:56:03 - local
localtime: 11:56:3

Вот здесь всплывает еще один интересный момент: разница во времени 9 часов, хотя часовой пояс устанавливается +8 часов. Все потому, что хитрые функции вполне корректно воспринимают перевод на летнее/зимнее время.

Замечание: если временная зона не установлена в настройках PHP-машины (основной php.ini), не задается нигде в настройках для сайта (php.ini или .htaccess сайта), а также не определяется функциями в скрипте, то при вызове функции date() получишь "Warning: date() It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. ...". В двух словах, это требование явного указания временной зоны =). А вот если при этом еще и сообщения типа E_WARNING отключены, тогда ошибок не увидишь и будешь потом голову ломать, почему все функции одинаковое время показывают и почему php работает в нулевом поясе ;) Вот у меня как раз был самый тяжелый случай.

Маски для date() и strftime(). Вторая функция использует функции системы, поэтому не все форматирующие символы работают. Например, Винда не признает %e (дни месяца без ведущих нулей), вместо этого можно использовать %#d. Символ # говорит о том, что ведущие нули нужно удалять. Но это работает только в Windows, фиг его знает, как на Линухе будет исполняться тот же код. С другой стороны date() все форматирует, но без учета локали, т.е. названия дней и месяцев на английском.

Пример: 03.07.2010 9:08:34
date:     "j F Y G:i:s"         --> 3 july 2010 9:08:34     
strftime: "%#d %B %Y %H:%M:%S"  --> 3 июль 2010 9:08:34  (если скрипт исполняется в Win)

Мое заблуждение о возможностях функции setlocale(). Она устанавливает/показывает локаль выполнения скрипта. В частности <? setlocale(LC_TIME, 'ru_RU', 'rus', 'russia'); ?> установит форматирование даты в представлении русского языка. А это значит, что название месяцев, последовательность чисел в записи даты будет идти по правилам, принятым в России. Только и всего. Установка локали не устанавливает часовой пояс для скрипта или сервера.


Время в MySQL
Корректная установка временной зоны в MySQL

(по материалам мануала "MySQL Server Time Zone Support")
В дистрибутиве MySQL под Windows таблицы с временными зонами - пустые. Их нужно отдельно cкачать здесь. Сейчас по ссылке доступны для скачивания два варианта "POSIX standard Time, 2010" и "Time zone with leap seconds, 2010". Разница в том, что второй вариант учитывает еще одно смещение - "cекунды координации". Смысл примерно следующий: 61-ая секунда называется leap second и добавляется она в UTC-время раз в несколько лет либо 30 июня, либо 31 декабря. Такая корректировка вызвана замедляющимся вращением Земли. По состоянию на 2010 год во время добавлено 23 секунды. Подробнее об этом можно почитать на Википедии

Наконец скачанные таблицы нужно скопировать в каталог [.../Mysql/data/mysql] и перезапустить MySQL-сервер.

Ахтунг! Для nix-подобных систем существует иная процедура, рекомендуемая тут, см. раздел "Populating the Time Zone Tables". Суть сводится примерно к такой вот команде:

user@comp:~$ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -p mysql

Утилита прочитает информацию по часовым поясам, хранящуюся в системе и заполнит нужную таблицу. Не без косяков, но это лучше, чем ничего.


Получить список зон можно, например, таким запросом (выбор по Азии): mysql> SELECT `name` FROM `mysql`.`time_zone_name` WHERE `name` LIKE 'Asia%'; Следующий запрос устанавливает временную зону на всем сервере. Не рекомендую так делать. mysql> SET GLOBAL time_zone = 'Asia/Irkutsk'; Откатиться можно так (значение 'SYSTEM' указывает на то, что зона должна быть та же, что и временная зона в ОС): mysql> SET GLOBAL time_zone = 'SYSTEM'; Установка временной зоны для текущей сессии. Вполне нормальный подход mysql> SET SESSION time_zone = 'Asia/Irkutsk'; Проверить установки зон сервера и сессии можно так (в ответе должно быть то, что установили): mysql> SELECT @@global.time_zone, @@session.time_zone; Пример:
mysql> SET SESSION time_zone = 'Asia/Irkutsk'; 
mysql> SELECT @@global.time_zone, @@session.time_zone;
Query OK, 0 rows affected (0.00 sec)

+--------------------+---------------------+
| @@global.time_zone | @@session.time_zone |
+--------------------+---------------------+
| SYSTEM             | Asia/Irkutsk        |
+--------------------+---------------------+
1 row in set (0.00 sec)

Если на MySQL-сервере не загружены таблицы с временными зонами, то все равно можно установить временную зону, при этом указывать ее нужно числом. Например, сместить время сессии +5 часов: mysql> SET SESSION time_zone = '+5:00';

Здесь есть слабое место: когда вы указываете часовой пояс "словами", MySQL читает смещение из таблицы временных зон и учитывает переход на летнее время, а если явно задать число, то естественно ни какого учета не происходит. Не забывайте об этом.

TIMESTAMP

Где-то вычитал, что timestamp в MySQL - это не тоже самое, что TIMESTAMP в Unix, т.е. отличается от универсального времени (UTC). Проверил - фигня все это. Есть в БД поле формата 'timestamp'. Если явно в него что-то не пишется, до при добавлении/изменении записи в него пишется именно абсолютное UTC-время. В упомянутом выше мануале сказано примерно следующее:
"Установка временной зоны для сессии влияет только на вывод данных в ней. При записи времени в базу значение сначала переводится в абсолютное UTC-значение, потом пишется. При чтении - наоборот".

[UPD] Спустя три года(!) я знаю, где собака зарыта. В MySQL есть тип данных даты и времени. Он может быть представлен типом поля DATETIME, DATE и TIMESTAMP. В данном представлении "TIMESTAMP" - это действительно не то понятие, что в Unix. Это название буквенного формата типа "YYYYMMDDHHMMSS" или покороче. Не знаю историю, что было раньше, но кому-то (разрабам MySQL или Unix) не следовало называть разные понятия одним и тем же словом :) В качестве ликбеза читаем, например, здесь.

Расставляя точки на i: MySQL полностью поддерживает UTC и нормально преобразует дату любого своего формата в Unix timestamp. Просто надо понимать разницу: тип поля может быть TIMESTAMP, и в нем будет записана форматированная дата/время. Если sql-запросом получить данные из этого поля, то будет скорее всего человекопонятная дата/время. А вот получить UTC из него можно например MySQL-функцией UNIX_TIMESTAMP() или php-функцией strtotime().

Тип поля TIMESTAMP удобно использовать в логах, где можно автоматически назначать метку времени в записи. Для этого значением по умолчанию в поле указываем CURRENT_TIMESTAMP. Это MySQL-функция, которая пишет текущее время. Синтаксические аналоги: now() и sysdate(). Подробности опять же в RTFM.

[UPD] MySQL конвертирует значения типа TIMESTAMP из текущего часового пояса в UTC при сохранении, и обратно из UTC в текущий часовой пояс при чтении. Но это не касается остальных типов, таких как DATETIME. (c) источник Однако тут требуется пояснение: при записи значения в поле DATETIME функцией NOW() или ее аналогами, время берется текущее на сервере с учетом заданного пояса. И записанное значение уже неизменно. Если же пишем в TIMESTAMP, вот тогда происходит перевод в UTC. И при чтении перевод в текущий часовой пояс. Т.е. значение типа TIMESTAMP зависит от заданного пояса сервера.

На stackoverflow.com хорошо объяснено поведение MySQL со временем. Как рекомендация: установить часовой пояс MySQL-сервера в GMT (+0:00). Тогда при сохранении TIMESTAMP-значений на них не повлияет, например, летнее время какого-то пояса. При чтении так же не будет побочных эффектов. И если потребуется TIMESTAMP-значение привести в какой-то часовой пояс, это можно сделать уже на PHP.

Если же вам нужно сохранить дату/время в неизменном виде независимо от времени года, cекунд координации и т.д., передавайте его явно и храните в полях типа DATETIME.

Кстати, у типов DATETIME и TIMESTAMP отличаются диапазоны допустимых значений:

  • DATETIME: '1000-01-01 00:00:00' - '9999-12-31 23:59:59'
  • TIMESTAMP: '1970-01-01 00:00:01' UTC - '2038-01-19 03:14:07' UTC.
Функции

now() - получить текущее время:

mysql> select now();
->2010-05-18 09:54:30

unix_timestamp() - получить текущее время в формате UTC

mysql> select UNIX_TIMESTAMP(now());
->1274144095

from_unixtime() - получить текущее читабельное дату/время из UTC

mysql> select FROM_UNIXTIME(1274144095);
->2010-05-18 09:54:30

Из PHP переключить зону MySQL-сессии можно так (действует до конца сессии):

<? mysql_query("SET time_zone = '". date_default_timezone_get() ."';", $db_resource); ?> или просто: <? mysql_query("SET time_zone = 'Asia/Irkursk'"); ?>

Если на MySQL-сервере не загружена таблица с временными зонами (болезнь моего хостера), то получишь ошибку типа: "ERROR 1298: Unknown or incorrect time zone: 'Asia/Irkursk'". Решить можно так - явно указать смещение в часах (переход на летнее время придется учитывать вручную, php-функция пересчета):

<? mysql_query("SET time_zone = '+08:00'"); ?>

Другой решение - так же ставим зону числом, но определяем ее, полагаясь на текущий часовой пояс php-скрипта (здесь автоматически учитывается переход на летнее время):

<? putenv("TZ=Asia/Irkursk");
mysql_query("SET LOCAL time_zone = '".date('P')."'") ?>
Функцию putenv() вызываем, если в настройках сайта (не всего сервера) не установлена временная зона, или если ее нужно сменить для текущего скрипта.

[1oo%, EoF]


Похожие материалы: Пересчет часового пояса с учетом летнего времени на PHP
Понравилась статья? Расскажите о ней друзьям:

Метки: MySQL, PHP, кодинг

Комментарии
Для работы модуля комментариев включите javaScript


Показать/скрыть правила
Имя
[i] [b] [u] [s] [url]
:-) ;-) :D *lol* 8-) :-* :-| :-( *cry* :o :-? *unsure* *oops* :-x *shocked* *zzz* :P *evil*

Осталось 1000 символов.
Код защиты от спама Обновить код
Каждый комментарий проходит ручную модерацию. 100% фильтрация спама.
Продвижение
Время
Метки