Работа с датой и временем в PHP: DateTime, форматирование и часовые пояса (PHP)

Работа с датами — одна из самых коварных вещей в коде: на секунду ошибся, часовой пояс не учёл — и логика ломается. В реальных задачах (логи, сессии, сроки действия токенов, пагинация по дате) лучше не полагаться на date() и ручное склеивание строк, а использовать DateTime и DateTimeZone.


Зачем нужен DateTime вместо date()/time()

time() даёт timestamp, date() делает строку. Этого хватает, чтобы «быстро вывести», но не хватает для:

  • сравнения дат («эта дата раньше той?»),
  • арифметики («плюс 3 дня», «минус 2 часа»),
  • корректной работы с часовыми поясами,
  • валидации формата ввода.

DateTime хранит состояние, умеет менять часовой пояс, считать разницу, модифицировать дату и форматировать. Это именно то, что нужно в плагинах, интеграции и любой нормальной бизнес‑логике.


Как создать DateTime

Текущее время

php
$now = new DateTime(); // время сервера с его часовым поясом по умолчанию

Из строки (самый частый кейс)

Если приходит дата из формы или API, всегда указывай формат:

php
$format = 'Y-m-d H:i:s';
$input = '2024-10-05 14:30:00';
 
$dt = DateTime::createFromFormat($format, $input);
if (!$dt) {
    // здесь обрабатываем ошибку: массив ошибок покажет, что не так
    $errors = DateTime::getLastErrors();
    throw new Exception('Неверная дата: ' . implode(', ', $errors['errors']));
}

Это надёжнее, чем пытаться распарсить строку вручную.

Из timestamp

php
$ts = 1700000000;
$dt = (new DateTime())->setTimestamp($ts);

Часовые пояса: как не запутаться

Правило: хранить в UTC, показывать в локальном поясе. Это стандарт, который спасает от проблем при переездах серверов и смене настроек.

Создать сразу с нужным поясом

php
$utc = new DateTime('now', new DateTimeZone('UTC'));
$msk = new DateTime('now', new DateTimeZone('Europe/Moscow'));

Сменить пояс у существующего объекта

php
$dt = new DateTime('2024-10-05 12:00:00', new DateTimeZone('UTC'));
$dt->setTimezone(new DateTimeZone('Europe/Moscow')); 
// момент времени тот же, но теперь это 15:00 по Москве

Важно: setTimezone() не «переводит» время, он меняет интерпретацию момента. Это как посмотреть на одни и те же часы из разных городов.

Получить список поясов

php
$zones = DateTimeZone::listIdentifiers();
// массив строк: 'UTC', 'Europe/Moscow', 'Asia/Yekaterinburg' и т.д.

Форматирование: как вывести дату

Используй format(). Спецификаторы те же, что и в date():

php
echo $dt->format('Y-m-d H:i:s');      // 2024-10-05 15:30:00
echo $dt->format('d.m.Y');           // 05.10.2024
echo $dt->format('j F Y, H:i');      // 5 October 2024, 15:30
echo $dt->format('c');               // ISO 8601: 2024-10-05T15:30:00+03:00

Для API и обмена данными лучше брать однозначные форматы: c или Y-m-d\TH:i:sP.


Арифметика: плюс, минус, разница

Изменить дату

php
$dt = new DateTime('2024-10-01');
$dt->modify('+7 days');       // 2024-10-08
$dt->modify('-1 month');       // 2024-09-08
$dt->modify('next Monday');    // ближайший понедельник вперёд
$dt->modify('last Friday');    // последняя пятница назад

modify() меняет сам объект. Если нужно оставить оригинал — клонируй:

php
$original = new DateTime('2024-10-01');
$copy = clone $original;
$copy->modify('+1 day');

Разница между двумя датами

php
$start = new DateTime('2024-10-01 10:00:00');
$end   = new DateTime('2024-10-03 14:30:00');
 
$interval = $start->diff($end);
 
echo $interval->days;                 // полные дни
echo $interval->h;                    // часы (в пределах суток)
echo $interval->i;                     // минуты
echo $interval->format('%a дней');    // общее число дней
echo $interval->format('%h часов');   // часы

$interval — это отдельный объект DateInterval. Он умеет форматироваться и считать дни, месяцы, годы.


Сравнение дат

DateTime можно сравнивать напрямую:

php
$a = new DateTime('2024-10-01');
$b = new DateTime('2024-10-05');
 
if ($a < $b) { /* $a раньше */ }
if ($a > $b) { /* $a позже */ }
if ($a == $b) { /* равны */ }

Работает даже с разными часовыми поясами: сравнение идёт по моменту времени (UTC‑эквиваленту).


Практические кейсы и типовые решения

Кейс 1: срок действия токена истёк?

php
$expiresAt = new DateTime($token['expires_at']); // из БД, уже в UTC
$now       = new DateTime('now', new DateTimeZone('UTC'));
 
if ($expiresAt <= $now) {
    // токен истёк
}

Главное: обе даты в одном поясе (лучше UTC).

Кейс 2: «через N дней» для напоминания

php
$base = new DateTime('now', new DateTimeZone('UTC'));
$remind = clone $base;
$remind->modify('+3 days');
 
$remindString = $remind->format('Y-m-d H:i:s'); // для сохранения в БД

Кейс 3: валидация диапазона дат из формы

php
$format = 'Y-m-d';
$from   = DateTime::createFromFormat($format, $_POST['from']);
$to     = DateTime::createFromFormat($format, $_POST['to']);
 
if (!$from || !$to) {
    throw new Exception('Некорректный формат даты');
}
 
if ($from > $to) {
    throw new Exception('Дата "от" не может быть позже даты "до"');
}

Частые ошибки и как их избежать

  • Смешивают строки и объекты. Не делайте $date = date('Y-m-d') + 1. Для арифметики всегда используйте DateTime.
  • Не проверяют результат createFromFormat. Если формат не совпал, вернётся false. Всегда проверяйте и смотрите getLastErrors().
  • Хранят локальное время вместо UTC. Потом при смене сервера или пояса всё «уедет». Храните в UTC, конвертируйте только на выводе.
  • Забывают клонировать объект. modify() меняет оригинал. Если оригинал нужен — делайте clone.
  • Думали, что setTimezone «переводит» часы. Он только меняет интерпретацию. Момент времени остаётся тем же.

Мини‑шпаргалка по форматам

Формат Пример Где удобно
Y-m-d H:i:s 2024-10-05 15:30:00 БД, логи, внутренний обмен
d.m.Y 05.10.2024 Вывод пользователю
c 2024-10-05T15:30:00+03:00 JSON, API, машиночитаемые данные
j F Y, H:i 5 October 2024, 15:30 Читаемый вывод в интерфейсе

Добавить комментарий