date_sub()
Узнайте, как вычитать даты в PHP с помощью DateTime::sub() и DateInterval с учётом особенностей переполнения месяцев и DST.
Вычитание времени из даты в PHP
Вычитание промежутка времени из даты — «30 дней назад», «начало прошлого месяца», «два часа до текущего момента» — одна из самых распространённых задач при работе с датами в PHP. Устаревшая процедурная функция date_sub() (псевдоним DateTime::sub()) была удалена в PHP 8.0, поэтому в современном коде следует использовать объектно-ориентированный метод DateTime::sub() (или DateTimeImmutable::sub()) совместно с DateInterval.
На этой странице рассматривается работа DateTime::sub(), построение строки интервала, различие между изменяемым и неизменяемым вариантами, а также подводные камни календаря (переполнение месяца, инвертированные интервалы, переход на летнее время), с которыми часто сталкиваются разработчики.
Как работает DateTime::sub()
sub() принимает единственный аргумент DateInterval, описывающий насколько нужно сдвинуться назад. Сигнатура метода:
public DateTime::sub(DateInterval $interval): DateTimeДве важные вещи:
- Метод изменяет объект на месте — исходный
DateTimeбудет изменён. - Он также возвращает тот же объект, поэтому вызовы можно объединять в цепочку.
$date = new DateTime('2023-10-15', new DateTimeZone('UTC'));
$interval = new DateInterval('P5D'); // 5 days
$date->sub($interval);
echo $date->format('Y-m-d'); // Outputs: 2023-10-10Передача явного DateTimeZone делает результат предсказуемым; без него PHP использует часовой пояс из date_default_timezone_set() или php.ini.
Построение строки DateInterval
DateInterval использует формат длительности ISO 8601: P[n]Y[n]M[n]DT[n]H[n]M[n]S. Префикс P (period) обязателен, а T отделяет датовую часть от временной.
| Токен | Значение | Пример | Длительность |
|---|---|---|---|
Y | Годы | P3Y | 3 года |
M | Месяцы | P6M | 6 месяцев |
D | Дни | P10D | 10 дней |
H | Часы | PT4H | 4 часа |
M | Минуты | PT30M | 30 минут |
S | Секунды | PT45S | 45 секунд |
Обратите внимание: M означает месяцы до T и минуты после него — частый источник ошибок. Комбинируйте токены, чтобы вычесть несколько единиц сразу:
$date = new DateTime('2023-10-15 12:00:00');
$interval = new DateInterval('P2M1DT3H'); // 2 months, 1 day, 3 hours
$date->sub($interval);
echo $date->format('Y-m-d H:i:s'); // Outputs: 2023-08-14 09:00:00Интервалы, содержащие только время, работают аналогично — например, 90 минут:
$date = new DateTime('2023-10-15 08:30:00');
$date->sub(new DateInterval('PT90M')); // 90 minutes
echo $date->format('Y-m-d H:i:s'); // Outputs: 2023-10-15 07:00:00Примечание:
DateIntervalстрого проверяет формат. Некорректная строка (например, отсутствующийPилиPTбез временных токенов) вызовет исключениеException. Если длительность поступает от пользователя, оберните конструктор вtry...catch.
Изменяемый и неизменяемый варианты: предпочитайте DateTimeImmutable
Поскольку DateTime::sub() изменяет объект на месте, дата, которую вы передаёте в другой код, может быть неожиданно изменена, если тот вызовет sub(). DateTimeImmutable решает эту проблему: его метод sub() не трогает оригинал и возвращает новый экземпляр.
$date = new DateTimeImmutable('2023-10-15');
$earlier = $date->sub(new DateInterval('P10D'));
echo $date->format('Y-m-d'); // Outputs: 2023-10-15 (unchanged)
echo "\n";
echo $earlier->format('Y-m-d'); // Outputs: 2023-10-05В большинстве случаев рекомендуется использовать DateTimeImmutable по умолчанию и прибегать к изменяемому DateTime только тогда, когда явно требуется обновление на месте.
Подводные камни, о которых нужно знать
Переполнение месяца
Вычитание целых месяцев не ограничивает результат последним днём целевого месяца — PHP нормализует переполнение, перенося его в следующий месяц. Вычитание одного месяца из 31 марта даёт март, а не февраль:
$date = new DateTime('2023-03-31');
$date->sub(new DateInterval('P1M'));
echo $date->format('Y-m-d'); // Outputs: 2023-03-03Здесь P1M сначала переходит к «31 февраля», которое PHP сдвигает на 3 марта (в 2023 году в феврале 28 дней). Если нужен последний день предыдущего месяца, используйте относительную строку: new DateTime('last day of previous month').
Инвертированные интервалы прибавляют, а не вычитают
У DateInterval есть свойство invert. Когда оно равно 1, интервал является отрицательным, поэтому sub() фактически прибавляет длительность:
$interval = new DateInterval('P1M');
$interval->invert = 1; // negative interval
$date = new DateTime('2023-10-15');
$date->sub($interval);
echo $date->format('Y-m-d'); // Outputs: 2023-11-15Это важно, когда вы повторно используете интервал, возвращённый DateTime::diff(), который автоматически устанавливает invert в зависимости от направления разницы.
Переход на летнее время
При вычитании дней через границу перехода на летнее время PHP корректирует время на циферблате так, чтобы календарный результат оставался правильным. Если же вычитать часы (PT24H), вы получите ровно 24 часа прошедшего времени, что может дать другой час на часах. Выбирайте интервалы на основе дней или часов осознанно, в зависимости от того, имеете ли вы в виду «то же время, предыдущий день» или «ровно 24 часа назад».
Использование Carbon для удобочитаемого синтаксиса
Для крупных кодовых баз библиотека Carbon обёртывает DateTimeImmutable удобочитаемыми, цепочечными вспомогательными методами. Она необязательна — стандартные классы покрывают типовые случаи, — но может сделать сложную логику яснее:
use Carbon\Carbon;
$date = Carbon::parse('2023-10-15');
$newDate = $date->subMonths(2)->subDays(5);
echo $newDate->toDateString(); // Outputs: 2023-08-10Заключение
Используйте DateTime::sub() (или, предпочтительно, DateTimeImmutable::sub()) с DateInterval для вычитания промежутков времени из даты. Стройте интервал в формате ISO 8601 P…T…, помните о различии M для месяцев и минут и следите за переполнением месяцев и инвертированными интервалами.
Для дальнейшего изучения см. date_add() для обратной операции, date_diff() для измерения разницы между двумя датами и date_format() для форматирования результата.