Даты в PHP: частые ошибки новичков и как их не совершать
Почему date('Y-m-d') отдаёт вчерашний день, откуда берётся разница в час после перевода часов и как сравнивать даты, не наступая на грабли строк. Разбор типичных ошибок на PHP 8.x.

Вы вывели дату регистрации пользователя через date('Y-m-d'), а на сервере она отличается от той, что видит клиент, на три часа. Или сравнили две даты как строки и получили, что '2024-9-1' больше, чем '2024-10-1'. Всё это — не баги PHP, а следствие того, как в языке устроены дата, время и часовые пояса. Разберём ошибки, которые делают почти все, кто впервые работает с датами, и покажем рабочие приёмы для PHP 8.x.
Что нужно знать до начала
Всё ниже проверено на PHP 8.1–8.3. Классы DateTime, DateTimeImmutable и DateInterval доступны с PHP 5.3, так что версия здесь не критична — критично то, какой из них вы выбираете. Расширение ext-intl (для форматирования на русском) должно быть установлено; проверьте командой:
php -m | grep intl
Если строки intl в выводе нет — часть примеров с локалью работать не будет, но базовая работа с датами не пострадает.
Ошибка 1. Полагаться на часовой пояс сервера
Самая частая и самая коварная. Функция date() и класс DateTime без явного пояса берут значение из настройки date.timezone в php.ini. На вашем ноутбуке это может быть Europe/Moscow, на хостинге — UTC, а после переезда на другой сервер — вообще неизвестно что.
Результат: даты «плавают» между окружениями, и вы ловите баг, который не воспроизводится локально.
Правило, которое экономит часы отладки: храните всё в UTC, конвертируйте в пояс пользователя только при выводе. База данных не должна знать о часовых поясах пользователей.
Задавайте пояс явно при создании объекта, а не надейтесь на настройку:
// PHP 8.x
$utc = new DateTimeImmutable('now', new DateTimeZone('UTC'));
// вывод в поясе пользователя
$moscow = $utc->setTimezone(new DateTimeZone('Europe/Moscow'));
echo $moscow->format('Y-m-d H:i:s');
Проверка, что получилось: два вызова format() одного и того же момента в UTC и в Europe/Moscow должны отличаться ровно на смещение пояса, а не совпадать.
Ошибка 2. Мутирующий DateTime
Класс DateTime изменяемый: методы modify(), add(), sub() меняют сам объект, а не возвращают новый. Новичок пишет так и удивляется, что обе переменные показывают одну дату:
$start = new DateTime('2024-01-01');
$end = $start; // это ссылка на тот же объект
$end->modify('+1 month');
echo $start->format('Y-m-d'); // 2024-02-01, а не 2024-01-01
Здесь $start изменился вместе с $end, потому что оба указывают на один объект. Лечится это переходом на DateTimeImmutable: его методы возвращают новый объект, а исходный не трогают.
$start = new DateTimeImmutable('2024-01-01');
$end = $start->modify('+1 month');
echo $start->format('Y-m-d'); // 2024-01-01
echo $end->format('Y-m-d'); // 2024-02-01
По умолчанию берите DateTimeImmutable. Изменяемый DateTime нужен редко — только когда вы сознательно хотите менять объект по месту.
Ошибка 3. Сравнивать даты как строки
Строковое сравнение работает, только пока формат строго Y-m-d с ведущими нулями. Стоит появиться 2024-9-1 без нуля — и порядок ломается, потому что символ 9 «больше» символа 1.
Объекты дат сравниваются операторами напрямую и правильно:
$a = new DateTimeImmutable('2024-09-01');
$b = new DateTimeImmutable('2024-10-01');
var_dump($a < $b); // bool(true)
А разницу между датами считает diff(), возвращая объект DateInterval:
$diff = $a->diff($b);
echo $diff->days; // 30 — всего дней между датами
echo $diff->m; // 1 — компонент "месяцы" разбитого интервала
echo $diff->invert; // 0, если $b позже $a
Разница между days и m — типичная ловушка. days — это общее число суток, а m — только «месячная» часть разложенного интервала. Для «сколько всего дней прошло» берите days, а не складывайте y, m и d.
Ошибка 4. Парсить дату из строки вслепую
Конструктор new DateTime('01/02/2024') угадывает формат по своим правилам, и для американской строки 01/02/2024 он поймёт это как 2 января, а не 1 февраля. Для строк из внешних источников используйте createFromFormat с явной маской:
$date = DateTimeImmutable::createFromFormat(
'd.m.Y',
'01.02.2024',
new DateTimeZone('Europe/Moscow')
);
if ($date === false) {
// строка не подошла под формат
}
// обязательно проверяйте предупреждения парсера
$errors = DateTimeImmutable::getLastErrors();
if ($errors && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) {
// например, '32.02.2024' пройдёт как false или с предупреждением
}
Начиная с PHP 8.2 getLastErrors() возвращает false, если ошибок не было, — раньше возвращался массив с нулевыми счётчиками. Поэтому проверяйте результат на false перед обращением к ключам, иначе получите предупреждение о доступе к элементу массива.
Ошибка 5. Игнорировать перевод часов и DST
В поясах с летним временем сутки не всегда равны 24 часам. Если добавлять +1 day к моменту рядом с переводом стрелок, локальное время сдвинется на 23 или 25 часов. В России постоянного перехода на летнее время нет с 2014 года, но если вы работаете с пользователями из Европы или США — учитывайте это.
Правило простое: арифметику по «суткам» и «месяцам» делайте в поясе пользователя, а арифметику по «ровно столько-то секунд» — в UTC. Смешивать нельзя.
Сравнение подходов к хранению и выводу
| Задача | Правильный инструмент | Чем платите за ошибку |
|---|---|---|
| Хранить момент в БД | UTC в столбце datetime/timestamp | Плавающие даты между серверами |
| Считать разницу | diff() → DateInterval | Ошибка на границе месяцев и лет |
| Парсить внешнюю строку | createFromFormat + проверка | Перепутанные день и месяц |
| Менять дату в коде | DateTimeImmutable | Неожиданная мутация исходной переменной |
| Вывод на русском | IntlDateFormatter | Английские названия месяцев |
Бонус: месяц на русском без костылей
Форматтер date('F') вернёт January, а не Январь. Правильный способ — расширение intl:
$fmt = new IntlDateFormatter(
'ru_RU',
IntlDateFormatter::LONG,
IntlDateFormatter::NONE,
'Europe/Moscow'
);
$date = new DateTimeImmutable('2024-02-01');
echo $fmt->format($date); // 1 февраля 2024 г.
Так вы получаете локализованные названия без ручных массивов месяцев, которые придётся склонять вручную.
Что делать дальше
Заведите в проекте одно правило: DateTimeImmutable плюс UTC для хранения — и придерживайтесь его во всём коде. Проверьте настройку date.timezone на всех окружениях, чтобы они совпадали. Если работаете с временными рядами и агрегацией по дням, смотрите на границы суток в поясе пользователя — именно там появляются расхождения в отчётах на день. Синтаксис форматирующих символов (Y, m, d и остальные) держите под рукой в официальной документации date() — их легко перепутать.
Универсальный ассистент: Обычный чат: тексты, идеи, разбор, перевод, расчёты.
Спросить за 8 ₽Источники: PHP Manual: Date and Time (DateTimeImmutable), PHP Manual: date() format characters
Частые вопросы
DateTime или DateTimeImmutable — что выбирать по умолчанию?
У меня другая версия PHP, код будет работать?
Почему date() показывает не то время, что я жду?
Как правильно посчитать, сколько дней между двумя датами?
createFromFormat вернул объект, но дата неверная — почему?
Нужно ли учитывать перевод часов, если пользователи из России?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.