В Битрикс даты разъезжаются быстрее, чем кажется: MySQL хранит DATETIME без таймзоны, PHP смотрит на date.timezone, сайт — на настройки «Региональные настройки», а ORM ждёт объекты Bitrix\Main\Type\DateTime, а не строки. Если в интеграции «вчера» и «сегодня» прыгают на час — почти всегда виноват этот зоопарк.
Ниже — практический минимум по Bitrix\Main\Type\Date и Bitrix\Main\Type\DateTime: создание, форматы, часовые пояса, ORM, агенты и типичные ошибки на проде. Связанные темы: ORM D7, агенты, ServiceLocator.
Date vs DateTime: что брать
Bitrix\Main\Type\Date — календарная дата без времени (день рождения, дата доставки «только день»). Bitrix\Main\Type\DateTime — дата+время с привязкой к часовому поясу PHP/сайта. Для полей заказа, логов, дедлайнов и «обновлено в» почти всегда нужен DateTime.
<?php
use Bitrix\Main\Type\Date;
use Bitrix\Main\Type\DateTime;
$day = new Date('08.09.2026', 'd.m.Y');
$now = new DateTime(); // сейчас в таймзоне сайта/PHP
$fromIso = new DateTime('2026-09-08T14:30:00', 'Y-m-d\TH:i:s');
Конструктор: new DateTime($time = 'now', $format = null, \DateTimeZone $timezone = null). Если $format не задан, строка парсится как для нативного \DateTime (относительные фразы вроде +1 day тоже работают).
Форматирование и разбор строк
В шаблонах и API не клеите дату через date() от unix-timestamp из разных источников — держите объект до последнего шага:
<?php
use Bitrix\Main\Type\DateTime;
$dt = new DateTime('08.09.2026 17:45:00', 'd.m.Y H:i:s');
echo $dt->format('Y-m-d H:i:s'); // 2026-09-08 17:45:00
echo $dt->format('d.m.Y'); // 08.09.2026
// для вывода пользователю удобны Culture-форматы сайта
echo $dt->toString(); // зависит от формата культуры
Разбор «грязного» ввода из формы:
<?php
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\Context;
$raw = (string)Context::getCurrent()->getRequest()->getPost('deadline');
try {
// явный формат из UI — меньше сюрпризов, чем strtotime
$deadline = new DateTime($raw, 'd.m.Y H:i');
} catch (\Throwable $e) {
throw new \InvalidArgumentException('Некорректная дата: ' . $raw);
}
Если формат неизвестен заранее, сначала нормализуйте на своей стороне (один формат в API), а не надейтесь на «умный» парсер.
Часовые пояса: сайт, PHP, MySQL
Три слоя, которые должны совпадать по смыслу:
- PHP —
date.timezoneв php.ini / пуле FPM; - Битрикс — настройки сайта (часовой пояс) и культура;
- MySQL —
time_zoneсессии и тип колонки (DATETIMEvsTIMESTAMP).
<?php
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\Config\Option;
// текущий пояс PHP
$phpTz = date_default_timezone_get();
// явно в Europe/Moscow
$msk = new DateTime('now', null, new \DateTimeZone('Europe/Moscow'));
// сдвиг относительно UTC для API
$utc = clone $msk;
$utc->setTimeZone(new \DateTimeZone('UTC'));
$payload = [
'event_at' => $utc->format('Y-m-d\TH:i:s\Z'),
];
Правило для интеграций: внутри Битрикс храните и считайте в таймзоне сайта, наружу (вебхуки, REST партнёра) отдавайте ISO-8601 в UTC или с явным offset. Не смешивайте unix-timestamp из JS (Date.now()) с «наивным» DATETIME без пометки пояса.
Арифметика: плюс день, минус час, конец суток
<?php
use Bitrix\Main\Type\DateTime;
$dt = new DateTime();
$dt->add('1 day'); // +1 сутки
$dt->add('-2 hours'); // -2 часа
$dt->add(new \DateInterval('P7D')); // +7 дней через DateInterval
// границы дня — частый кейс для отчётов
$from = new DateTime('today');
$to = new DateTime('today');
$to->add('1 day');
$to->add('-1 second'); // 23:59:59 текущего дня
// или явнее
$start = DateTime::createFromPhp(new \DateTime('today 00:00:00'));
$end = DateTime::createFromPhp(new \DateTime('today 23:59:59'));
add() меняет объект на месте (как и многие методы D7). Если нужна «копия до сдвига» — клонируйте: $copy = clone $dt;.
Сравнение и «раньше / позже»
<?php
use Bitrix\Main\Type\DateTime;
$a = new DateTime('2026-09-08 10:00:00', 'Y-m-d H:i:s');
$b = new DateTime('2026-09-08 12:00:00', 'Y-m-d H:i:s');
if ($a->getTimestamp() < $b->getTimestamp()) {
// a раньше b
}
// удобные хелперы культуры/формата — для UI; для логики лучше timestamp или diff
$diffSeconds = $b->getTimestamp() - $a->getTimestamp(); // 7200
Не сравнивайте через == объекты и не сравнивайте отформатированные строки d.m.Y — это ломается на ведущих нулях и разных культурах. Для фильтров ORM используйте объекты DateTime целиком.
ORM: запись и выборка по дате
В DataManager-полях типа datetime / date ORM ждёт (и отдаёт) объекты Type\DateTime / Type\Date:
<?php
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\ORM\Query\Query;
// пример: своя таблица модуля
use Local\Exchange\OrderTable;
$now = new DateTime();
$result = OrderTable::add([
'UF_EXTERNAL_ID' => 'EXT-100500',
'UF_SYNCED_AT' => $now,
]);
// фильтр «за последние 24 часа»
$from = new DateTime();
$from->add('-1 day');
$rows = OrderTable::getList([
'filter' => [
'>=UF_SYNCED_AT' => $from,
'<=UF_SYNCED_AT' => new DateTime(),
],
'order' => ['UF_SYNCED_AT' => 'DESC'],
'limit' => 100,
])->fetchAll();
foreach ($rows as $row) {
/** @var DateTime|null $synced */
$synced = $row['UF_SYNCED_AT'];
if ($synced instanceof DateTime) {
echo $synced->format('Y-m-d H:i:s');
}
}
Передавать в фильтр сырую строку '08.09.2026' иногда «прокатывает», но поведение зависит от культуры и драйвера. Явный DateTime — предсказуемее, особенно в агентах без хита пользователя.
Инфоблоки и свойства типа «Дата/Время»
В старом API и в D7-обёртках инфоблоков даты часто приходят строкой в формате сайта. Нормализуйте на входе сервиса:
<?php
use Bitrix\Main\Type\DateTime;
use Bitrix\Iblock\ElementTable;
// если пришла строка из GetList / CIBlockElement
function parseBitrixDate(?string $value): ?DateTime
{
if ($value === null || $value === '') {
return null;
}
// типичные форматы админки
foreach (['d.m.Y H:i:s', 'd.m.Y H:i', 'd.m.Y', 'Y-m-d H:i:s', 'Y-m-d'] as $fmt) {
$dt = DateTime::tryParse($value, $fmt);
if ($dt instanceof DateTime) {
return $dt;
}
}
return null;
}
Если в вашей версии ядра нет tryParse, оберните new DateTime(...) в try/catch — смысл тот же: один хелпер на модуль, без копипасты форматов по компонентам.
Агенты и cron: «сейчас» без сюрпризов
Агент крутится в CLI/cron-контексте. Таймзона должна быть той же, что у веб-пула, иначе ночной отчёт «за вчера» уедет на час:
<?php
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\Config\Option;
function local_exchange_agent(): string
{
// на всякий случай фиксируем пояс явно в коде агента
$tzName = Option::get('main', 'default_time_zone') ?: 'Europe/Moscow';
if (@timezone_open($tzName)) {
date_default_timezone_set($tzName);
}
$from = new DateTime('today');
$to = new DateTime('tomorrow');
// ... выборка и обмен ...
return 'local_exchange_agent();';
}
Проверьте php -i | grep date.timezone на том же PHP binary, которым крутится cron Битрикс. Расхождение FPM vs CLI — классика BitrixVM.
Связка с HttpClient и внешними API
Во внешний мир лучше отдавать UTC. Пример рядом с HttpClient:
<?php
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
$local = new DateTime(); // таймзона сайта
$utc = clone $local;
$utc->setTimeZone(new \DateTimeZone('UTC'));
$http = new HttpClient([
'socketTimeout' => 5,
'streamTimeout' => 15,
]);
$http->setHeader('Content-Type', 'application/json');
$http->post('https://api.example/v1/events', Json::encode([
'occurred_at' => $utc->format('Y-m-d\TH:i:s\Z'),
'local_at' => $local->format('Y-m-d H:i:s'),
'tz' => date_default_timezone_get(),
]));
Типичные ошибки
- Строка вместо объекта в ORM. Иногда запишется, иногда получите странный фильтр. Держите DateTime до
add/update. - date() от timestamp из JS. Браузер шлёт UTC ms, PHP форматирует в локали сервера — час «прыгает».
- Разный timezone у FPM и CLI. Веб показывает одно, агент считает другое.
- Сравнение строк d.m.Y. Лексикографически
01.10<30.09— отчёты врут. - Мутация через add() без clone. Один объект уехал в фильтр и в лог уже «сдвинутым».
- MySQL TIMESTAMP vs DATETIME. TIMESTAMP конвертируется в session time_zone; DATETIME — «как записали». Для Битрикс чаще DATETIME + дисциплина пояса в приложении.
Быстрая отладка
<?php
use Bitrix\Main\Type\DateTime;
$dt = new DateTime();
AddMessage2Log([
'php_tz' => date_default_timezone_get(),
'now' => $dt->format('Y-m-d H:i:s'),
'ts' => $dt->getTimestamp(),
'offset' => $dt->format('P'),
], 'local.datetime');
Сверьте три значения: вывод из веба, вывод из агента, SELECT NOW(); в MySQL под тем же пользователем, что у сайта.
Краткий чеклист
- В коде модулей —
Type\DateTime/Type\Date, не сырые строки. - Один канонический формат на границе UI (например
d.m.Y H:i). - Наружу — ISO-8601 с Z или offset.
- FPM и CLI с одной
date.timezone. - Перед арифметикой —
clone, если объект ещё нужен «как был». - В фильтрах ORM — объекты, не «почти правильные» строки.
Вывод
Bitrix\Main\Type\DateTime — не «обёртка ради обёртки», а способ не размножать strtotime и date() по проекту. Зафиксируйте пояс, парсьте явно, в ORM отдавайте объекты, во внешние API — UTC. Вместе с ServiceLocator удобно спрятать хелпер дат в сервис модуля и не тащить форматы в шаблоны. Дальше по контуру интеграций логично держать рядом HttpClient и единый контракт времени в payload.
