Перейти к содержимому

Даты в коде - объект, часовой пояс, фильтр по периоду

Разбираемся с датами так, чтобы отчёт «за вчера» сходился у всех, а фильтр по периоду не терял записи последнего дня.

Решение

Работаем с датой объектом:

use Bitrix\Main\Type\DateTime;
$from = new DateTime('01.08.2026 00:00:00', 'd.m.Y H:i:s');
$to = (clone $from)->add('1 month'); // прибавление понимает словами
$isPast = $from < new DateTime(); // объекты сравниваются напрямую

С датой работают объектом, а не строкой. Строка зависит от формата и языка сайта, а объект складывается, сравнивается и уходит в запрос без пересборки формата на каждом шаге.

Пишем и читаем дату в базе:

SyncTable::add(['DATE_START' => new DateTime()]); // объект, а не строка
$row = SyncTable::getList([
'filter' => ['>=DATE_START' => $from, '<DATE_START' => $to],
])->fetch();
echo $row['DATE_START']->format('d.m.Y H:i'); // из базы тоже приходит объект

В поле даты кладут объект, а не отформатированный текст. Тогда сравнение в фильтре работает по времени, а не по совпадению символов, и запись с другим форматом не теряется.

Отключаем сдвиг часового пояса:

\CTimeZone::Disable();
$rows = OrderTable::getList(['select' => ['ID', 'DATE_INSERT']])->fetchAll();
\CTimeZone::Enable();
// в отчётах и обменах пояс отключают: там нужны одинаковые числа для всех

Показ сдвигается поясом пользователя, а хранение - нет. Отчёт, посчитанный под менеджером из другого пояса, отличается от такого же отчёта под администратором ровно на этот сдвиг.

Считаем границы периода:

$dayStart = new DateTime(date('d.m.Y') . ' 00:00:00', 'd.m.Y H:i:s');
$dayEnd = (clone $dayStart)->add('1 day');
// фильтр «больше или равно началу и строго меньше следующего дня»
// так последняя секунда суток не теряется и не задваивается

Границы периода задают двумя явными отметками. Условие «меньше конца суток» с временем 23:59:59 теряет записи последней секунды, а строгое сравнение со следующим днём - нет.

Даты из внешних систем приводят к своему виду сразу на входе. Чужой формат и чужой пояс, доехавшие до базы как есть, потом ищутся по всему проекту.

Дату без времени и дату со временем стоит различать в голове и в коде. День рождения и момент оформления заказа ведут себя по-разному ровно на границе суток.

Отчёты по времени полезно подписывать поясом. Строка «время московское» под таблицей снимает половину вопросов к цифрам.

Типичные проблемы

Фильтр по дате не находит записи.

В фильтре строка в одном формате, а в базе дата в другом. Объект даты снимает вопрос формата целиком.

Отчёт «за вчера» у всех разный.

Пояс пользователя сдвигает границы суток при выводе. В отчётах пояс отключают явно.

Из периода выпали заказы последней минуты.

Верхняя граница задана временем 23:59:59 вместо начала следующего дня. Сравнение делают строгим и по следующей отметке.

Дата из обмена уехала на несколько часов.

Внешняя система прислала время в своём поясе, а его приняли как серверное. Чужие даты приводят к своему поясу на входе.

Сравнение дат работает через раз.

Даты сравниваются как строки, а не как объекты. У строк порядок символов, а не времени.

Частые вопросы

В каком времени дата лежит в базе?

Во времени сервера, без пояса пользователя. Сдвиг добавляется на выводе.

Когда отключать сдвиг пояса?

В отчётах, обменах и всём, что должно давать одинаковые числа. На витрине сдвиг обычно нужен.

Как хранить дату без времени?

Отдельным типом поля для даты. Иначе время само по себе начнёт участвовать в сравнениях.

Что делать с датами из внешнего API?

Разбирать по известному формату и приводить к своему поясу сразу. Хранить чужой формат не нужно.

Смежное

Первоисточник