Дата и время изнутри - база, объект, экран
Разбираем дату по слоям: что лежит в колонке базы, что приходит в код объектом и что видит человек на экране. Дальше смотрим, в каких точках платформа переводит время и где по дороге появляется тот самый сдвиг на несколько часов.
Механика
Платформа различает два типа значения: дату и дату со временем. Тип даты нормализует время к нулю, поэтому час и минута в него просто не помещаются. Тип даты со временем хранит момент целиком, и именно он нужен факту события.
В базе значение лежит в колонке подходящего типа, а не строкой в формате сайта. Записанное время всегда серверное: пояс посетителя в колонку не попадает никогда. Поэтому две записи, сделанные одновременно из Москвы и из Лондона, в базе совпадают.
Из выборки поле даты приходит объектом, а не готовым текстом для шаблона. Печать такого объекта строкой даёт формат сайта, а печать с маской - ровно то, что просили. Сравнивать значение из выборки со строкой напрямую поэтому бессмысленно.
Поясов в проекте сразу три: пояс сервера, пояс сайта в региональных настройках и пояс посетителя из его профиля. При включённых поясах на выводе применяется пояс посетителя, а хранение остаётся серверным. Разница между экраном и колонкой равна ровно этому сдвигу.
При записи перевод делает не платформа, а код разработчика. Строка из формы - это время посетителя, и превращать её в серверное значение нужно отдельным вызовом. Объект, собранный конструктором из той же строки, считается серверным и уезжает на величину сдвига.
При чтении перевод, наоборот, срабатывает сам собой. Печать объекта в шаблоне даёт время посетителя, а журналу и обмену нужен исходный серверный момент. Отключение пользовательского времени у объекта возвращает то, что лежит в колонке.
Разбор строки без явной маски опирается на региональные настройки текущего сайта. Формат даты берётся из культуры сайта, и на втором сайте проекта он бывает другим. Один и тот же код тогда разбирает одну строку по-разному в двух местах.
В фильтре выборки объект даты превращается в серверное значение нужной колонки. Условие без оператора сравнения в ORM работает как поиск подстроки, и для даты это лишено смысла. Оператор в ключе фильтра пишут явно даже там, где равенство кажется очевидным.
Дата в свойстве инфоблока и дата в поле сущности ORM различаются самим хранением. Свойство лежит значением пользовательского типа в таблицах свойств инфоблока, а поле сущности - отдельной колонкой своего типа. Отсюда разный вход при записи и разное поведение сортировки.
Шаги
- Определить тип значения: нужен ли час события или достаточно календарного дня.
- Проверить в настройках, включены ли часовые пояса и какой пояс стоит у посетителя.
- Найти точку входа даты в код и перевести пользовательский ввод в серверное время явно.
- Найти точку вывода и решить, нужен ли там пояс посетителя или серверный момент.
- Сверить слой хранения: колонка сущности 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+3echo $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?
Тип даты нормализует время к нулю, тип со временем хранит момент целиком. Выбор делают по тому, важен ли в задаче час события.
В каком времени дата лежит в колонке?
В серверном, без пояса посетителя. Пояс применяется при выводе, а при записи перевод делает код разработчика.
Почему конструктор падает на нормальной с виду дате?
Строка разобрана по формату сайта, а он оказался другим. Формат передают вторым аргументом, а ввод из формы разбирают безопасным методом.
Как удалить значение поля с датой в своей таблице?
Разрешить полю пустое значение в описании сущности и записать пустоту. Без этого разрешения колонка отвергает пустое значение ошибкой базы.
Смежное
- Даты и время - оглавление подтемы
- Ядро D7 - устройство ядра целиком
- Даты в коде: объект, часовой пояс, фильтр по периоду - готовые приёмы работы с датой
- Дата сохранилась не та: разбор причин - когда сдвиг уже случился
- Вывод дат на витрине: падежи, форматы сайта, относительное время - последний слой, экран
- Рабочий календарь: график, праздники, расчёт срока - расчёты поверх этих типов
- Дата, число и HTML в свойствах: хранение, вывод, фильтр - слой свойств инфоблока
- Пользовательское поле изнутри: описание, значение, вывод - как устроено поле с датой
- Своя таблица ORM: описание, миграция, выборка - где объявляют поля даты
- Отчёты по заказам: агрегаты, группировка, выгрузка - где сдвиг виден лучше всего