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

Дата и время изнутри - база, объект, экран

Разбираем дату по слоям: что лежит в колонке базы, что приходит в код объектом и что видит человек на экране. Дальше смотрим, в каких точках платформа переводит время и где по дороге появляется тот самый сдвиг на несколько часов.

Механика

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

В базе значение лежит в колонке подходящего типа, а не строкой в формате сайта. Записанное время всегда серверное: пояс посетителя в колонку не попадает никогда. Поэтому две записи, сделанные одновременно из Москвы и из Лондона, в базе совпадают.

Из выборки поле даты приходит объектом, а не готовым текстом для шаблона. Печать такого объекта строкой даёт формат сайта, а печать с маской - ровно то, что просили. Сравнивать значение из выборки со строкой напрямую поэтому бессмысленно.

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

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

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

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

В фильтре выборки объект даты превращается в серверное значение нужной колонки. Условие без оператора сравнения в ORM работает как поиск подстроки, и для даты это лишено смысла. Оператор в ключе фильтра пишут явно даже там, где равенство кажется очевидным.

Дата в свойстве инфоблока и дата в поле сущности ORM различаются самим хранением. Свойство лежит значением пользовательского типа в таблицах свойств инфоблока, а поле сущности - отдельной колонкой своего типа. Отсюда разный вход при записи и разное поведение сортировки.

Шаги

  1. Определить тип значения: нужен ли час события или достаточно календарного дня.
  2. Проверить в настройках, включены ли часовые пояса и какой пояс стоит у посетителя.
  3. Найти точку входа даты в код и перевести пользовательский ввод в серверное время явно.
  4. Найти точку вывода и решить, нужен ли там пояс посетителя или серверный момент.
  5. Сверить слой хранения: колонка сущности ORM, свойство инфоблока или пользовательское поле.

Код

Смотрим разницу двух типов:

use Bitrix\Main\Type\Date;
use Bitrix\Main\Type\DateTime;
$date = new Date('25.11.2025', 'd.m.Y');
$moment = new DateTime('25.11.2025 14:30:00', 'd.m.Y H:i:s');
echo $date->format('d.m.Y H:i:s'); // 25.11.2025 00:00:00: время нормализовано к нулю
echo $moment->format('H:i'); // 14:30: момент сохранён целиком
$date->add('1 day'); // add меняет сам объект, копию не возвращает
echo $date->format('d.m.Y'); // 26.11.2025: исходное значение уже другое

Тип даты теряет часы молча, без ошибки и предупреждения. Поэтому срок действия хранят датой, а факт события - датой со временем: на границе суток разница между ними становится заметной.

Разбираем строку из формы:

$dt = DateTime::tryParse($request->get('start'), 'DD.MM.YYYY HH:MI:SS');
if ($dt === null) {
$errors[] = 'Дата введена в неизвестном формате'; // разбор вернул пустое значение
}
$risky = new DateTime($request->get('start')); // без маски: формат берётся у сайта
// конструктор при непонятной строке бросает Main\ObjectException и роняет страницу

Маска разбора задаётся отдельно от формата вывода. Без маски строка читается по региональным настройкам текущего сайта, и на втором сайте проекта тот же код ошибается на той же самой строке.

Переводим ввод посетителя в серверное время:

$server = DateTime::createFromUserTime('25.11.2025 18:00'); // посетитель в UTC+3
echo $server->format('H:i'); // 15:00, если сервер стоит в UTC+0
$forLog = clone $server;
$forLog->disableUserTime(); // печать без автоперевода в пояс посетителя
echo $forLog; // серверный момент: ровно то, что лежит в колонке
$fromApi = DateTime::createFromTimestamp(1764079800); // unix-время всегда серверное
$phpDate = DateTime::createFromPhp(new \DateTime('now')); // из объекта самого языка

Перевод при записи делает код, а перевод при выводе платформа берёт на себя сама. Журналу и обмену нужен серверный момент, поэтому автоперевод у такого объекта отключают явным вызовом.

Описываем поле даты в своей сущности:

use Bitrix\Main\ORM\Fields\DateField;
use Bitrix\Main\ORM\Fields\DatetimeField;
(new DatetimeField('DATE_START'))
->configureNullable() // без этого запись пустого значения даст ошибку колонки
->configureDefaultValueNow(), // значение «сейчас» подставляется при вставке
(new DateField('DEADLINE'))
->configureNullable(), // только дата: время в такой колонке не хранится

Пустая дата допустима только у поля, объявленного допускающим пустое значение. Иначе попытка стереть дату упирается в ошибку базы о недопустимом значении в колонке, и запись не проходит.

Кладём и читаем объект через ORM:

ProjectTable::add(['DATE_START' => new DateTime()]); // объект, а не строка
$row = ProjectTable::getRowById($id);
echo $row['DATE_START']->format('d.m.Y H:i'); // машинный формат по маске
echo $row['DATE_START']->toString(); // формат берётся у культуры сайта

Печать с маской - это машинный контракт, печать культурой сайта - показ человеку. Первое годится для обмена, ключей кэша и имён файлов, второе для витрины и страниц административной части.

Сравниваем дату в фильтре выборки:

$rows = ProjectTable::getList([
'filter' => ['>=DATE_START' => new DateTime('01.11.2025 00:00:00', 'd.m.Y H:i:s')],
'order' => ['DATE_START' => 'DESC'], // сортировка идёт по колонке, а не по тексту
])->fetchAll();
// ключ без оператора ORM понимает как поиск подстроки, а не как точное равенство
// для точного совпадения оператор пишут явно: '=DATE_START'

Объект в фильтре превращается в серверное значение колонки. Строка на его месте сравнивается посимвольно, и запись, сохранённая в другом формате, просто не попадает в результат выборки.

Кладём дату в свойство инфоблока и в поле Highload-блока:

CIBlockElement::SetPropertyValuesEx($elementId, $iblockId, ['DEADLINE' => '25.11.2025']);
// свойству инфоблока дату передают в формате сайта, а не в формате базы
CIBlockElement::SetPropertyValuesEx($elementId, $iblockId, ['DATES' => false]);
// очистка множественного свойства идёт признаком false, а не пустым массивом
$hlDataClass::add(['UF_DATETIME' => new \Bitrix\Main\Type\DateTime]);
// пользовательскому полю Highload-блока нужен объект, как и полю сущности ORM

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

Ограничения

Тип даты без времени нормализует час и минуту к нулю прямо при создании объекта. Ошибки при этом не будет, и потеря времени обнаруживается уже на границе суток в готовом отчёте.

Прибавление интервала меняет сам объект даты, а не возвращает новую копию. Исходное значение после такого вызова уже другое, и повторный расчёт от той же переменной даёт неверный результат.

Разбор текста без времени подставляет не текущий час, а время окончания рабочего дня. Значение берётся из настроек календаря и по умолчанию равно шести часам вечера.

Помощник построения SQL не экранирует аргументы у своих функций работы с датами. Пользовательские данные в такие вызовы передают только после явного экранирования средствами того же помощника.

Пользовательские поля попадают в выборку ORM только у сущности с объявленным идентификатором набора полей. Без него поле даты видно в административной части, но недоступно фильтру и сортировке запроса.

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

Из заказа приходит объект вместо даты, и вывод ломается.

Поле даты в выборке отдаётся объектом, а не готовой строкой для шаблона. Строку берут печатью с маской или культурным выводом, а не приведением типа.

Создание объекта падает с ошибкой о неверной дате.

Конструктор без маски разбирает строку по формату текущего сайта и не узнаёт её. Формат задают вторым аргументом, а ввод из формы разбирают безопасным методом.

Стереть дату в своей таблице не выходит: база ругается на null.

Поле сущности объявлено без права на пустое значение, и колонка запрещает null. Право на пустое значение задают прямо в описании поля при объявлении сущности.

В журнале событий время на несколько часов впереди.

При включённых поясах печать объекта переводит момент в пояс посетителя. Для служебных записей автоперевод у объекта отключают и получают серверное время.

После правки скриптом у всех элементов слетела дата изменения.

Обновление элемента по умолчанию проставляет текущий момент в поле даты изменения. Прежнее значение сохраняет явная передача признака отказа от обновления этого поля.

Свойство типа дата не сохраняется из своего скрипта.

Значение передано в формате базы, а свойство инфоблока ожидает формат сайта. Массовая запись дат в свойства идёт строкой формата сайта, а не объектом даты.

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

Как получить нормальную дату из объекта DateTime?

Печатью с маской нужного формата либо культурным выводом для показа человеку. Приведение объекта к строке даёт формат сайта и время посетителя.

Чем Date отличается от DateTime?

Тип даты нормализует время к нулю, тип со временем хранит момент целиком. Выбор делают по тому, важен ли в задаче час события.

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

В серверном, без пояса посетителя. Пояс применяется при выводе, а при записи перевод делает код разработчика.

Почему конструктор падает на нормальной с виду дате?

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

Как удалить значение поля с датой в своей таблице?

Разрешить полю пустое значение в описании сущности и записать пустоту. Без этого разрешения колонка отвергает пустое значение ошибкой базы.

Смежное

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