W3docs

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ГодыP3Y3 года
MМесяцыP6M6 месяцев
DДниP10D10 дней
HЧасыPT4H4 часа
MМинутыPT30M30 минут
SСекундыPT45S45 секунд

Обратите внимание: 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() для форматирования результата.

Практика

Практика
Что делает DateTime::sub() с переданным ему DateInterval?
Что делает DateTime::sub() с переданным ему DateInterval?
Was this page helpful?