Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Дата и время: часовые пояса, интервалы и реальные сроки
  2. Показать покупателю известную дату отправки
  3. Получить следующий день и сохранить исходную дату
  4. Вернуть рассчитанный срок в вызывающий код
  5. Отличить момент события, календарную дату и местное расписание
  6. Сначала разобрать одну дату, затем проверить результат
  7. Вернуть только принятую дату из функции проверки
  8. Применить к правильной дате правила доставки
  9. Выбрать, что означает продление на месяц
  10. Различить ровно24 часа и завтра в09:00
  11. Посмотреть, почему календарный день иногда короче24 часов
  12. Проверить истечение срока с заданным «сейчас»
  13. Функции и методы этой темы
  14. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 23 · С нуля → практика

Дата и время: часовые пояса, интервалы и реальные сроки

Не путаем момент времени с календарной датой и явно определяем, что означает «через месяц».

Показать покупателю известную дату отправки

Наглядная модель

Одно событие, разные показания часов

Заказ создан в один момент, но покупатель и склад могут находиться в разных часовых поясах.

  • Храним момент: 09:00 UTC

    Дата и время с зоной однозначно определяют событие.

  • Показываем: 12:00 в Москве

    Для UTC+03:00 изменилось представление того же момента.

Преобразование зоны не переносит событие на три часа. Календарные действия вроде «плюс один месяц» имеют отдельные правила; их нельзя всегда заменить фиксированным числом секунд.

Магазин подтвердил отправку на12 октября2026 года. Нужно получить подпись «Дата отправки:12.10.2026» и затем уметь рассчитывать соседние даты. Простая строка хранит буквы, но сама не знает календаря. Готовый класс DateTimeImmutable предоставляет объект даты с операциями форматирования и расчёта; собственный класс писать не нужно.

От календарного значения к подписи
  1. zone содержит правила UTC
  2. dispatch получает12 октября2026,00:00:00 в этой зоне
  3. dispatch->format("d.m.Y") возвращает строку12.10.2026
  4. label хранит эту строку → echo показывает дату отправки

New создаёт готовый объект. В new DateTimeZone('UTC') строка UTC выбирает пояс без сезонного сдвига. В следующем вызове два аргумента разделены запятой: известная дата/время и объект zone. Знак -> выбирает действие объекта слева; круглые скобки передают аргумент этому действию. У format аргумент — строка шаблона: d означает день из двух цифр, m — месяц, Y — год из четырёх цифр; точки выводятся буквально. = сохраняет результат, ; завершает инструкцию.

Получить готовую подпись датыСамостоятельный пример
<?php
$zone = new DateTimeZone('UTC');
$dispatch = new DateTimeImmutable('2026-10-12 00:00:00', $zone);
$label = $dispatch->format('d.m.Y');
echo 'Дата отправки: ', $label, "\n";
Ожидаемый результат
Дата отправки: 12.10.2026

Dispatch хранит объект, label — возвращённую строку. Вызов format не печатает самостоятельно: вывод выполняет echo. Шаблон меняет вид подписи, а не дату отправки. UTC выбран явно для воспроизводимости. Для реальной местной встречи позднее укажем её регион; для календарного дня не будем выдумывать перевод в другой пояс.

DateTimeImmutable означает, что обычная операция изменения создаёт другой объект. В нём есть год, месяц, день, время и зона. Модель «карточка календаря» помогает читать пример, но объект не является календарём сайта, записью БД или запущенным таймером. Все примеры здесь — полные отдельные CLI-файлы с заданными датами; строка now пока не используется.

Получить следующий день и сохранить исходную дату

Покупателю нужны обе подписи: отправка12 октября и ожидаемый следующий календарный день13 октября. Метод modify получает текст календарного правила и возвращает новую дату. В '+1 day' знак плюс, число и английское слово day — содержимое одной строки правила, а не отдельные операторы PHP.

Два результата с разным назначением
  1. dispatch →12 октября
  2. modify(+1 day) → новая дата13 октября
  3. nextDay хранит возвращённый объект
  4. format каждой даты → две отдельные подписи
Оставить дату отправки рядом с новым срокомСамостоятельный пример
<?php
$dispatch = new DateTimeImmutable('2026-10-12 00:00:00', new DateTimeZone('UTC'));
$nextDay = $dispatch->modify('+1 day');
echo 'Отправка: ', $dispatch->format('d.m.Y'), "\n";
echo 'Следующий календарный день: ', $nextDay->format('d.m.Y'), "\n";
Ожидаемый результат
Отправка: 12.10.2026
Следующий календарный день: 13.10.2026

Вторая строка получает объект13 октября в nextDay. Исходный dispatch остаётся12 октября. Далее каждый format возвращает свою строку прямо в ожидающий echo. Вложенный new DateTimeZone выполняет уже знакомое создание зоны как второй аргумент; он не получает результат modify. Календарный день пока не означает рабочий день: выходные, праздники и расписание склада требуют отдельной политики.

Сохраняйте возвращённый объект. В PHP 8.5 у обычных изменяющих методов Immutable действует NoDiscard: игнорирование результата может дать предупреждение. Для самостоятельной пробы замените правило на +2 days и оставьте присваивание nextDay: получите 14.10.2026, исходная отправка останется 12.10.2026. Полный договор правил и ошибок — в modify.

Вернуть рассчитанный срок в вызывающий код

Если одинаковое правило используется в нескольких местах, функция получает дату отправки и возвращает дату готовности. Здесь выбран учебный срок «три календарных дня». Тип перед shipped разрешает объект DateTimeImmutable, а тип после двоеточия обещает такой же вид результата. Как и у обычного числа, return отдаёт одно значение в точное место вызова.

Вызов readyDate(dispatch)
  1. Аргумент dispatch12 октября → местный shipped
  2. shipped->modify(+3 days) создаёт15 октября → местный ready
  3. return ready возвращает объект15 октября в readyDate(dispatch)
  4. pickup получает этот объект → format → подпись даты готовности
Использовать возвращённую дату в подтверждении заказаСамостоятельный пример
<?php
function readyDate(DateTimeImmutable $shipped): DateTimeImmutable {
    $ready = $shipped->modify('+3 days');
    return $ready;
}
$dispatch = new DateTimeImmutable('2026-10-12 00:00:00', new DateTimeZone('UTC'));
$pickup = readyDate($dispatch);
echo 'Отправка: ', $dispatch->format('d.m.Y'), "\n";
echo 'Готовность к выдаче: ', $pickup->format('d.m.Y'), "\n";
Ожидаемый результат
Отправка: 12.10.2026
Готовность к выдаче: 15.10.2026

Объявление функции само ничего не считает. Выполнение начинается в readyDate($dispatch). Shipped обозначает переданную дату; modify создаёт отдельный результат, return передаёт его наружу. После присваивания pickup программа продолжает два echo. Функция не ставит задание планировщику и не отправляет уведомление; она только вычисляет дату по заданному правилу.

Возвращённый объект можно передать другой функции или отформатировать для подтверждения. Если нужен только текст, можно объявить результат string и вернуть format; тогда дальнейшая календарная арифметика потребует исходной даты. Выбирайте договор по тому, что должен делать вызывающий код.

Отличить момент события, календарную дату и местное расписание

Оплата произошла в конкретный момент: в UTC часы показывали12:00, в Москве в этот же момент —15:00. У дня рождения иной смысл: это календарная дата, а не придуманная полночь UTC. «Встреча в09:00 следующего понедельника в Берлине» — местное расписание с правилами региона.

ЗадачаКакие сведения сохраняютЧто показывают
Совершившаяся оплатаМомент UTC/Unix и согласованную точностьМестную подпись в поясе посетителя
День рождения или календарный день отправкиСаму календарную дату по договоруЭту дату без ненужного перевода полуночи в другой пояс
Будущая встреча/повторениеМестную дату, часы, идентификатор зоны и правило неоднозначных часовВремя региона; при обмене — также определённый момент
Одна оплата показана в двух местах
  1. paidAt:12:00 UTC
  2. setTimezone(Europe/Moscow) сохраняет момент
  3. moscow:15:00 +03:00
  4. Формат P показывает выбранное смещение
Показать время оплаты покупателю и операторуСамостоятельный пример
<?php
$paidAt = new DateTimeImmutable('2026-10-12 12:00:00', new DateTimeZone('UTC'));
$moscow = $paidAt->setTimezone(new DateTimeZone('Europe/Moscow'));
echo 'В журнале UTC: ', $paidAt->format('d.m.Y H:i P'), "\n";
echo 'Для покупателя в Москве: ', $moscow->format('d.m.Y H:i P'), "\n";
Ожидаемый результат
В журнале UTC: 12.10.2026 12:00 +00:00
Для покупателя в Москве: 12.10.2026 15:00 +03:00

H даёт часы00–23, i — минуты, P — смещение вида+03:00. SetTimezone возвращает новый вариант представления того же момента; не прибавляет три часа длительности. PaidAt остался в UTC. Региональное имя хранит правила зоны, а фиксированное+03:00 описывает только смещение. Для другого региона или исторической даты смещение может отличаться.

Unix timestamp — число секунд от1970-01-01 00:00:00 UTC. Оно одинаково для одного момента в разных зонах. Ручное прибавление10800 к timestamp создаст другой момент. Неоднозначные и отсутствующие местные часы при DST рассматриваются после обычных сроков; полный справочник зон — DateTimeZone.

Сначала разобрать одну дату, затем проверить результат

Поле формы обещает формат ГГГГ-ММ-ДД. CreateFromFormat получает именно этот шаблон, исходный текст и зону. Запись DateTimeImmutable::createFromFormat(...) вызывает фабрику класса: готового объекта слева ещё нет, поэтому используется ::. Возврат — новый объект или false. До проверки false метод объекта вызывать нельзя.

Шаблон !Y-m-d читает год, месяц, день; дефисы должны быть на своих местах. Восклицательный знак сбрасывает неуказанные части, поэтому часы не берутся из текущего времени. Сразу после разбора getLastErrors читает диагностику: в PHP8.2+ false означает её отсутствие, иначе возвращается массив с warning_count/error_count и сообщениями.

Три результата проверки одного ввода
  1. Вход2026-02-28 → createFromFormat
  2. Объект или false: удалось ли разобрать?
  3. warnings/errors: не исправлял ли парсер невозможные компоненты?
  4. format(Y-m-d) совпал с исходным текстом → допустим именно наш формат
Принять известный день и показать подписьСамостоятельный пример
<?php
$input = '2026-02-28';
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $input, new DateTimeZone('UTC'));
$errors = DateTimeImmutable::getLastErrors();
$hasProblems = $errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0);
if ($date === false || $hasProblems || $date->format('Y-m-d') !== $input) {
    echo "Дата не принята\n";
} else {
    echo 'Выбран день: ', $date->format('d.m.Y'), "\n";
}
Ожидаемый результат
Выбран день: 28.02.2026

Разбор дал объект28 февраля00:00, диагностикаfalse, поэтому hasProblems=false. В условии || ищется хотя бы одна причина отказа. При date=false короткое замыкание не допускает вызов format на false. Здесь причин нет: else форматирует объект для посетителя. Вход в этом опыте уже известная корректная строка; защиту произвольного текста добавим в следующей функции.

Увидеть, во что превратилось несуществующее 30 февраляСамостоятельный пример
<?php
$input = '2026-02-30';
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $input, new DateTimeZone('UTC'));
$errors = DateTimeImmutable::getLastErrors();
echo 'Введено: ', $input, "\n";
if ($date !== false) { echo 'Парсер построил: ', $date->format('Y-m-d'), "\n"; }
if ($errors !== false && $errors['warning_count'] > 0) {
    echo "Дата отклонена: календарные части были исправлены\n";
}
Ожидаемый результат
Введено: 2026-02-30
Парсер построил: 2026-03-02
Дата отклонена: календарные части были исправлены

Парсер смог создать объект, но перенёс два лишних дня в март. Поэтому существование объекта не означает корректность исходного дня. Показанный мартовский объект используется только для объяснения отказа; сохранять его как будто пользователь выбрал2 марта нельзя.

Именно ожидание «несуществующая дата должна дать false» встречается в вопросе о createFromFormat. Здесь решение строится из разных проверок: разбираемость, отсутствие исправлений и точная форма. Свободный strtotime не заменяет этот договор ввода.

Вернуть только принятую дату из функции проверки

Теперь объединим уже разобранные проверки в parseCalendarDate. На входе string input, на выходе DateTimeImmutable. Длина нашего ASCII-формата ровно10 байтов. Strlen возвращает эту длину, str_contains проверяет запрещённый нулевой байт; такие входы отклоняются до парсера, чтобы не пропустить ValueError. Исключение означает отсутствие принятой даты, а не запасной календарный день.

Где продолжается успешный и неуспешный вызов
  1. parseCalendarDate(2026-02-28) → местный input
  2. Проверка формы, разбор, диагностика и обратный формат
  3. return date → переменная chosen → подпись28.02.2026
  4. Для30 февраля throw → catch; присваивания chosen в этом вызове нет
Правильная дата используется, неправильные получают причину отказаСамостоятельный пример
<?php
function parseCalendarDate(string $input): DateTimeImmutable {
    if (strlen($input) !== 10 || str_contains($input, "\0")) {
        throw new InvalidArgumentException('Нужна дата ГГГГ-ММ-ДД без лишних байтов');
    }
    $date = DateTimeImmutable::createFromFormat('!Y-m-d', $input, new DateTimeZone('UTC'));
    $errors = DateTimeImmutable::getLastErrors();
    if ($date === false || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))
        || $date->format('Y-m-d') !== $input) {
        throw new InvalidArgumentException('Нужна существующая дата ГГГГ-ММ-ДД');
    }
    return $date;
}
foreach (['2026-02-28', '2026-02-30', '28.02.2026'] as $input) {
    try {
        $chosen = parseCalendarDate($input);
        echo '[', $input, '] принято: ', $chosen->format('d.m.Y'), "\n";
    } catch (InvalidArgumentException $error) {
        echo '[', $input, '] отклонено: ', $error->getMessage(), "\n";
    }
}
try { parseCalendarDate("2026-02-2\0"); }
catch (InvalidArgumentException $error) { echo 'Нулевой байт отклонён: ', $error->getMessage(), "\n"; }
Ожидаемый результат
[2026-02-28] принято: 28.02.2026
[2026-02-30] отклонено: Нужна существующая дата ГГГГ-ММ-ДД
[28.02.2026] отклонено: Нужна существующая дата ГГГГ-ММ-ДД
Нулевой байт отклонён: Нужна дата ГГГГ-ММ-ДД без лишних байтов

Три прохода foreach независимо передают input в локальный параметр. Только первый return отдаёт объект в chosen, затем внешний format возвращает строку для echo. Два других входа направляются в catch и не используются для заказа. Отдельный последний вызов показывает раннее отклонение нулевого байта. Настоящее поле формы сначала проверяют на is_string; массив вместо поля не является строкой. Клиентская подсказка формата не заменяет серверную проверку.

У DateTimeImmutable::createFromFormat есть дополнительные параметры шаблона, дроби секунды и правила зоны: они перечислены в полной карточке. Здесь намеренно выбран один конкретный формат формы. Проверка календаря ещё не отвечает, разрешена ли доставка в этот день.

Применить к правильной дате правила доставки

Пусть для воспроизводимого расчёта «сегодня» —12 октября2026, доставка доступна с этого дня по19 октября включительно. Все входные даты ниже уже календарно корректны и стоят в одной зоне в00:00. Следующая проверка отвечает о допустимом диапазоне. Для реальной формы сначала вызывают parseCalendarDate из предыдущего шага.

Отличить прошлый день, разрешённый и слишком далёкийСамостоятельный пример
<?php
$zone = new DateTimeZone('UTC');
$today = new DateTimeImmutable('2026-10-12 00:00:00', $zone);
$lastDay = $today->modify('+7 days');
echo 'Доступно: ', $today->format('d.m.Y'), ' — ', $lastDay->format('d.m.Y'), "\n";
foreach (['2026-10-11', '2026-10-14', '2026-10-21'] as $text) {
    $requested = new DateTimeImmutable($text . ' 00:00:00', $zone);
    if ($requested < $today) {
        echo $text, ': отказ — день уже прошёл', "\n";
    } elseif ($requested > $lastDay) {
        echo $text, ': отказ — ещё не открыт для заказа', "\n";
    } else {
        echo $text, ': доставку можно предложить', "\n";
    }
}
Ожидаемый результат
Доступно: 12.10.2026 — 19.10.2026
2026-10-11: отказ — день уже прошёл
2026-10-14: доставку можно предложить
2026-10-21: отказ — ещё не открыт для заказа

Объекты даты поддерживают сравнение моментов. При одинаковой зоне и00:00 оно соответствует порядку наших календарных дней. Границы включены: отклоняются только даты строго раньше today и строго позже lastDay. Наличие свободного курьера, выходные и праздничный календарь — отдельные проверки, которых этот пример не обещает.

Выбрать, что означает продление на месяц

Подписка началась31 января. У февраля нет31-го числа, поэтому фраза «на месяц» требует правила продукта. PHP умеет переносить лишние дни, но не знает, нужен ли вам3 марта,28 февраля или постоянное число суток. Сначала сравним два известных календарных правила.

Два правила от одного31 января
  1. start:31 января2026
  2. +1 month → условное31 февраля →3 марта
  3. last day of next month → последний день февраля →28 февраля
  4. Оба результата отдельно подписаны; исходная дата не изменена
Увидеть две разные даты продленияСамостоятельный пример
<?php
$start = new DateTimeImmutable('2026-01-31 10:00:00', new DateTimeZone('UTC'));
$overflow = $start->modify('+1 month');
$monthEnd = $start->modify('last day of next month');
echo 'Начало: ', $start->format('d.m.Y'), "\n";
echo 'Перенос лишних дней: ', $overflow->format('d.m.Y'), "\n";
echo 'Конец следующего месяца: ', $monthEnd->format('d.m.Y'), "\n";
Ожидаемый результат
Начало: 31.01.2026
Перенос лишних дней: 03.03.2026
Конец следующего месяца: 28.02.2026

Каждый modify вызывается у исходного start. Сдвиг месяца сохраняет номер дня и затем нормализует невозможное31 февраля. Второе правило прямо просит последний день следующего месяца. Это не две реализации одинаковой операции: по ним покупатель получит разные сроки.

Другой договор: «сохранить номер дня, но если его нет — взять последний». Он отличается от постоянного выбора конца месяца:30 апреля должно перейти в30 мая, хотя в мае31 день. Сначала найдём первый день следующего месяца, затем выберем меньший номер из исходного дня и длины следующего месяца.

Ограничить номер дня длиной следующего месяцаСамостоятельный пример
<?php
function nextMonthlyDate(DateTimeImmutable $start): DateTimeImmutable {
    $nextMonth = $start->modify('first day of next month');
    $originalDay = (int) $start->format('j');
    $daysInNextMonth = (int) $nextMonth->format('t');
    $day = min($originalDay, $daysInNextMonth);
    return $nextMonth->setDate((int) $nextMonth->format('Y'), (int) $nextMonth->format('n'), $day);
}
$zone = new DateTimeZone('UTC');
foreach (['2026-01-31 10:00:00', '2026-04-30 10:00:00'] as $text) {
    $start = new DateTimeImmutable($text, $zone);
    $renewal = nextMonthlyDate($start);
    echo $start->format('d.m.Y H:i'), ' → ', $renewal->format('d.m.Y H:i'), "\n";
}
Ожидаемый результат
31.01.2026 10:00 → 28.02.2026 10:00
30.04.2026 10:00 → 30.05.2026 10:00

Format всегда возвращает текст: j даёт номер дня, t — число дней в месяце, Y — год, n — номер месяца. (int) готовит целые для min и setDate. При первом вызове min(31,28) даёт28; при втором min(30,31) даёт30. SetDate получает год, месяц, выбранный день и возвращает новую дату; return передаёт её в renewal. Время10:00 сохранено. Это один шаг: повторный вызов от28 февраля уже считает исходным днём28. Если расписание должно навсегда помнить31-е, храните отдельный исходный номер дня, а не выводите его из предыдущего результата.

Для независимых календарей выходных и праздничных дней нужна дополнительная модель. Не называйте +1 month «30 днями», а +1 day — «рабочим днём». Полные add/sub, DateInterval и последствия DST собраны в календарной арифметике.

Различить ровно24 часа и завтра в09:00

Промокод действует24 часа от15:00, а напоминание нужно показать завтра в09:00 по местным часам. Получатся разные сроки. DateInterval хранит описание интервала: P начинает период, T отделяет временную часть, 24H задаёт24 часа. Add применяет интервал и возвращает новую дату. SetTime задаёт часы, минуты и необязательные секунды в уже выбранном календарном дне.

Две потребности от одного начала
  1. start:12 октября15:00, Europe/Moscow
  2. add(PT24H) →13 октября15:00: конец действия
  3. modify(+1 day), затем setTime(9,0) →13 октября09:00: напоминание
  4. Две переменные сохраняют два разных момента
Вычислить срок действия и отдельное напоминаниеСамостоятельный пример
<?php
$start = new DateTimeImmutable('2026-10-12 15:00:00', new DateTimeZone('Europe/Moscow'));
$duration = new DateInterval('PT24H');
$expiresAt = $start->add($duration);
$tomorrow = $start->modify('+1 day');
$remindAt = $tomorrow->setTime(9, 0);
echo 'Начало: ', $start->format('d.m.Y H:i P'), "\n";
echo 'Действует до: ', $expiresAt->format('d.m.Y H:i P'), "\n";
echo 'Напомнить: ', $remindAt->format('d.m.Y H:i P'), "\n";
Ожидаемый результат
Начало: 12.10.2026 15:00 +03:00
Действует до: 13.10.2026 15:00 +03:00
Напомнить: 13.10.2026 09:00 +03:00

ExpiresAt получил результат прибавления24 часов. RemindAt получил другой объект с часами09:00 следующего дня. Ни add, ни setTime не запланировали отправку: эти значения нужно передать реальному механизму уведомлений. Здесь нет ожидания или обращения к внешнему сервису.

DateTime::diff возвращает календарное описание разницы с учётом зон, а разница getTimestamp даёт число прошедших секунд для моментов с целой секундой. Это разные ответы. Для точного измерения времени выполнения кода нужен монотонный hrtime: системные часы могут корректироваться. Доли секунды и переполнение timestamp на32-битной платформе имеют отдельные ограничения в справочнике.

Посмотреть, почему календарный день иногда короче24 часов

В Europe/Berlin весной2026 часы переходят вперёд. От28 марта12:00 до29 марта12:00 проходит23 часа. Для встречи «завтра в то же местное время» это верно, а для срока «ровно24 часа» требуется другой момент. Здесь правила берутся из установленной базы часовых поясов PHP; обновление базы может изменить будущие региональные расписания.

Переход региональных часов
  1. 28 марта12:00 +01:00
  2. Следующий местный день:29 марта12:00 +02:00
  3. Между моментами23 часа
  4. Полные24 часа закончатся29 марта13:00 +02:00
Сравнить встречу на следующий день со сроком24 часаСамостоятельный пример
<?php
$start = new DateTimeImmutable('2026-03-28 12:00:00', new DateTimeZone('Europe/Berlin'));
$sameLocalTime = $start->modify('+1 day');
$after24Hours = $start->add(new DateInterval('PT24H'));
$calendarHours = ($sameLocalTime->getTimestamp() - $start->getTimestamp()) / 3600;
$elapsedHours = ($after24Hours->getTimestamp() - $start->getTimestamp()) / 3600;
echo 'Начало: ', $start->format('d.m.Y H:i P'), "\n";
echo 'Завтра по часам: ', $sameLocalTime->format('d.m.Y H:i P'), '; прошло ', $calendarHours, ' ч', "\n";
echo 'Через24 часа: ', $after24Hours->format('d.m.Y H:i P'), '; прошло ', $elapsedHours, ' ч', "\n";
Ожидаемый результат
Начало: 28.03.2026 12:00 +01:00
Завтра по часам: 29.03.2026 12:00 +02:00; прошло 23 ч
Через24 часа: 29.03.2026 13:00 +02:00; прошло 24 ч

GetTimestamp возвращает int секунд для каждого готового момента. Вычитание даёт82800 или86400 секунд; деление на3600 переводит их в часы. Метод setTimezone здесь не вызывался: изменилось региональное смещение на новой календарной дате. При осеннем переводе есть обратная ситуация с повторением местных часов. Несуществующее или повторённое время встречи требует отдельного правила принятия ввода, а не молчаливого согласия с парсером.

Для самостоятельного опыта выберите обычные12 и13 октября2026 в том же регионе: и следующий местный день, и24 часа дадут13 октября12:00. Смысл операций остаётся разным даже тогда, когда результаты временно совпадают.

Проверить истечение срока с заданным «сейчас»

В рабочем приложении часы меняются постоянно. Для объяснения и теста передадим текущее значение аргументом: тогда можно проверить момент до границы и саму границу. Принято правило «в момент expiresAt срок уже закончился», поэтому сравнение использует >=.

Получить подпись до истечения и ровно на границеСамостоятельный пример
<?php
function expiryLabel(DateTimeImmutable $expiresAt, DateTimeImmutable $now): string {
    if ($now >= $expiresAt) { return 'Срок закончился'; }
    $secondsLeft = $expiresAt->getTimestamp() - $now->getTimestamp();
    return 'Осталось ' . $secondsLeft . ' секунд';
}
$zone = new DateTimeZone('Europe/Moscow');
$expiresAt = new DateTimeImmutable('2026-10-13 15:00:00', $zone);
$before = new DateTimeImmutable('2026-10-13 14:00:00', $zone);
$atBoundary = new DateTimeImmutable('2026-10-13 15:00:00', $zone);
echo 'В14:00: ', expiryLabel($expiresAt, $before), "\n";
echo 'В15:00: ', expiryLabel($expiresAt, $atBoundary), "\n";
Ожидаемый результат
В14:00: Осталось 3600 секунд
В15:00: Срок закончился

Первый вызов получает срок15:00 и местный now14:00; сравнение ложно, разница3600 становится частью возвращённой строки в echo. Второй вызов получает now15:00, первая ветка возвращает «Срок закончился» и остальные строки тела пропускаются. Текущая дата компьютера нигде не читается. В рабочей точке входа создают now один раз и передают дальше; сама функция остаётся проверяемой.

Следующий шаг — расписание из нескольких дат: DatePeriod и границы повторения. Для передачи срока API документируйте формат, обязательную зону и точность долей секунды. Для формы показывайте ожидаемый формат рядом с полем и различайте «невозможный день» и «пока недоступная дата доставки».

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

DateTimeImmutable::createFromFormatРазобрать дату в заранее известном формате, не полагаясь на свободное угадывание.

PHP 5.5+ · date

public static function createFromFormat(string $format, string $datetime, ?DateTimeZone $timezone = null): DateTimeImmutable|false

Разобрать дату в заранее известном формате, не полагаясь на свободное угадывание.

Часть вызоваДоговор
formatШаблон разбирается слева направо. ! сначала сбрасывает поля; | в конце сбрасывает непрочитанные. Полный список обозначений находится ниже и в отдельной таблице главы.
datetimeВходная строка, соответствующая шаблону. Числовые поля читаются жадно и могут переполняться.
timezoneDateTimeZone или null. Если строка задаёт зону/Unix timestamp, он имеет приоритет; иначе берётся переданная или текущая зона PHP.
ВозвратDateTimeImmutable или false. Объект может существовать при предупреждениях; это не автоматическая валидация календаря.
ОшибкиNULL-байт во входе вызывает ValueError с PHP 8.0.21 / 8.1.8 / 8.2.0. Обычная ошибка разбора возвращает false; диагностические массивы читаются через getLastErrors.
Путь даты в первом примере
  1. input содержит 15.06.2024
  2. DateTimeImmutable::createFromFormat читает !d.m.Y и получает UTC
  3. date содержит объект 2024-06-15 00:00:00
  4. format возвращает строку для подписи заявки

Шаблон ввода описывает, какую часть строки читать: d — день, m — месяц, Y — год. ! предотвращает наследование случайных часов текущего момента. Без !/| незаданные поля берутся из текущего времени; если разобрано хотя бы одно поле времени, остальные временные поля обнуляются. Не переносите буквально все символы форматирования вывода: наборы похожи, но договоры различаются.

Из даты поля получить подпись для заявкиСамостоятельный пример
<?php
$input = '15.06.2024';
$date = DateTimeImmutable::createFromFormat('!d.m.Y', $input, new DateTimeZone('UTC'));
if ($date === false) {
    echo "Заявка не принята: дату не удалось разобрать\n";
} else {
    echo 'День заявки: ', $date->format('Y-m-d H:i:s e'), "\n";
}
Ожидаемый результат
День заявки: 2024-06-15 00:00:00 UTC

Format !d.m.Y связывает первые две цифры с днём15, следующие — с месяцем6, последние — с годом2024. ! даёт предсказуемые нулевые часы. Фабрика возвращает DateTimeImmutable в date, else вызывает format и использует его строку как подпись заявки. Этот первый вход задан автором; проверка false ещё не достаточна для произвольного текста формы.

Показать исходный и исправленный день перед отказомСамостоятельный пример
<?php
$input = '30.02.2024';
$date = DateTimeImmutable::createFromFormat('!d.m.Y', $input, new DateTimeZone('UTC'));
$issues = DateTimeImmutable::getLastErrors();
$valid = $date !== false
    && ($issues === false || ($issues['warning_count'] === 0 && $issues['error_count'] === 0))
    && $date->format('d.m.Y') === $input;
echo 'В поле: ', $input, "\n";
if ($date !== false) { echo 'Парсер построил: ', $date->format('d.m.Y'), "\n"; }
if ($valid) { echo "День можно использовать в заявке\n"; }
else { echo "Заявка остановлена: исправьте исходную дату\n"; }
Ожидаемый результат
В поле: 30.02.2024
Парсер построил: 01.03.2024
Заявка остановлена: исправьте исходную дату

30 февраля превратилось в1 марта, поэтому предупреждение делает valid=false. Нормализованный объект показан для объяснения, но заявка не использует его как согласованный день. && прекращает проверку после false, поэтому исходный date=false не получил бы вызов format. Дополнительно проверяются точный обратный формат, тип исходного поля, допустимая длина и нулевые байты; полный самостоятельный обработчик есть в базовом маршруте дат.

Позиция ! меняет уже прочитанные поляСамостоятельный пример
<?php
declare(strict_types=1);

$date = DateTimeImmutable::createFromFormat('Y-m-!d', '2024-06-15', new DateTimeZone('UTC'));
echo $date->format('Y-m-d H:i:s'), "\n";
$kept = DateTimeImmutable::createFromFormat('Y-m-d|', '2024-06-15', new DateTimeZone('UTC'));
echo $kept->format('Y-m-d H:i:s'), "\n";
Ожидаемый результат
1970-01-15 00:00:00
2024-06-15 00:00:00

! посередине сбросил уже разобранные год/месяц. | сохранил прочитанные поля. Обычно для поля даты удобнее ! в самом начале.

Не принять лишний хвост как часть согласованного поляСамостоятельный пример
<?php
$input = '2024-06-15 imported';
$date = DateTimeImmutable::createFromFormat('!Y-m-d+', $input, new DateTimeZone('UTC'));
$issues = DateTimeImmutable::getLastErrors();
echo 'Получено поле: ', $input, "\n";
if ($date !== false) { echo 'Распознан день: ', $date->format('Y-m-d'), "\n"; }
if ($issues !== false && $issues['warning_count'] > 0) {
    echo "Импорт отложен: в поле остался лишний хвост\n";
}
Ожидаемый результат
Получено поле: 2024-06-15 imported
Распознан день: 2024-06-15
Импорт отложен: в поле остался лишний хвост

Плюс в конце шаблона разрешил создать объект при лишнем хвосте, заменив ошибку предупреждением. Он не проверил смысл слова imported и не сделал его допустимым по договору импорта. Здесь выбран отказ. Если ваш формат намеренно хранит дату и отметку, разберите и проверьте оба поля по отдельности вместо добавления+ ради исчезновения false.

NULL-байт не игнорируетсяСамостоятельный пример
<?php
declare(strict_types=1);

try { DateTimeImmutable::createFromFormat('!Y-m-d', "2024-06-15\0"); }
catch (ValueError $error) { echo "Недопустимый байт во входе\n"; }
Ожидаемый результат
Недопустимый байт во входе

Это отдельный путь исключения поверх возвращаемого false. Проверка типов, ограничение длины и контроль допустимых байтов делают обработчик предсказуемым.

Полный набор символов: таблица разбора формата. Если нужны русские названия месяцев, используйте осмысленный локализованный парсер, например IntlDateFormatter, с явной локалью; замена регистра не превращает русский текст в английский.

Сравните: DateTimeImmutable::__construct, DateTimeImmutable::getLastErrors, date_parse_from_format, checkdate.

Время, часовые пояса и календарь. Практические задачи с датами.

Если разбирается день рождения или другая календарная дата, UTC может быть техническим носителем её компонентов: не переводите такую полночь в зону посетителя как момент оплаты. Checkdate проверяет три отдельных календарных компонента; конструктор со свободной строкой подходит для контролируемого текста, а не обещанного формата поля.

DateTimeImmutable::formatПолучить строку даты для человека или согласованного внешнего формата.

PHP 5.5+ · date

public function format(string $format): string

Получить строку даты для человека или согласованного внешнего формата.

Часть вызоваДоговор
formatСтрока шаблона. Y/m/d — год/месяц/день, H/i/s — часы/минуты/секунды. Полная таблица ниже по ссылке; константы DATE_* — готовые строки шаблонов.
ВозвратСтрока; исходный объект и момент не меняются. format сам ничего не выводит и не переводит дату в другую зону.
ЯзыкНазвания дней и месяцев английские, независимо от setlocale. Для локализованного вывода есть IntlDateFormatter.
Доли секундыu/v работают с долей, сохранённой объектом. Целый timestamp в date() не содержит её.
Путь даты в первом примере
  1. date = 15 июня 2024, 10:30 UTC
  2. Шаблон d.m.Y H:i выбирает день, месяц, год и часы
  3. format возвращает строку 15.06.2024 10:30 в label
  4. echo выводит подтверждение встречи

Шаблон — не регулярное выражение и не перевод даты. Каждый распознанный символ заменяется полем объекта. Например, m означает месяц, а i — минуты. Чтобы вывести буквальную T, используйте \T. Перед отправкой HTTP-заголовка с буквальным GMT сначала действительно переведите объект в UTC: подпись зоны сама по себе часы не переводит. Полная таблица всех букв формата.

В подтверждении нужна удобная подпись, а в заявке — календарный день. Создаём одну известную дату в UTC; затем выбираем, какие её поля показать. Запись date->format(...) вызывает метод у даты; внутри скобок передаётся шаблон. Возвращаемое значение — обычная строка: её можно сохранить, соединить с подписью или передать в дальнейший код.

Вывести время встречи в подтвержденииСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-06-15 10:30:00', new DateTimeZone('UTC'));
$label = $date->format('d.m.Y H:i');
echo 'Встреча: ', $label, ' UTC', "\n";
echo 'День для заявки: ', $date->format('Y-m-d'), "\n";
Ожидаемый результат
Встреча: 15.06.2024 10:30 UTC
День для заявки: 2024-06-15

Первый format возвращает строку 15.06.2024 10:30 в label. Echo добавляет назначение и зону. Второй вызов читает тот же объект и возвращает 2024-06-15 прямо в ожидающий echo. Точки, дефисы, двоеточие и пробел — разделители; d/m/Y означают день/месяц/год, H/i — час/минуту. Объект и его часы не изменились.

Исправить подпись, которая выглядит как неверные часыСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-06-15 23:47:09', new DateTimeZone('UTC'));
echo 'Ошибочный шаблон h:m:s: ', $date->format('h:m:s'), "\n";
echo 'Время закрытия H:i:s: ', $date->format('H:i:s'), "\n";
Ожидаемый результат
Ошибочный шаблон h:m:s: 11:06:09
Время закрытия H:i:s: 23:47:09

h вывел 11 часов по 12-часовой шкале, m — месяц 06, а не 47 минут. Исправление H:i:s возвращает 23:47:09. Часовой пояс менять не нужно: дата всё время хранила правильные часы. Для 12-часового вывода добавьте a или A, чтобы отличать утро от вечера.

Назвать отчёт по неделе на границе годаСамостоятельный пример
<?php
$date = new DateTimeImmutable('2021-01-01 10:00:00', new DateTimeZone('UTC'));
$week = $date->format('o-\WW');
$filename = 'sales-' . $week . '.csv';
echo 'Продажа от: ', $date->format('d.m.Y'), "\n";
echo 'Имя недельного отчёта: ', $filename, "\n";
echo 'Неверная группа Y-W: ', $date->format('Y-W'), "\n";
Ожидаемый результат
Продажа от: 01.01.2021
Имя недельного отчёта: sales-2020-W53.csv
Неверная группа Y-W: 2021-53

W — номер ISO-недели, o — год этой же недели. Первый W после обратного слеша становится буквальной буквой, второй даёт 53. Точка соединяет sales-, полученную строку 2020-W53 и .csv. Пример готовит имя, файл не создаёт. Пятница 1 января относится к последней неделе 2020; Y-W отправил бы её в ошибочную группу 2021-53.

Согласовать точность отметки событияСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-06-15 10:30:00.125456', new DateTimeZone('UTC'));
$apiTime = $date->format(DateTimeInterface::RFC3339_EXTENDED);
echo 'EVT-41 для API: ', $apiTime, "\n";
echo 'EVT-41 для журнала: ', $date->format('Y-m-d H:i:s.u P'), "\n";
Ожидаемый результат
EVT-41 для API: 2024-06-15T10:30:00.125+00:00
EVT-41 для журнала: 2024-06-15 10:30:00.125456 +00:00

DateTimeInterface::RFC3339_EXTENDED — именованная константа с готовым шаблоном. Его v оставляет три цифры миллисекунд 125; шаблон с u сохраняет шесть 125456. API получает явно заданную зону и точность, журнал — более подробное время. Это подготовка строк, без сетевой отправки и записи файла. Нужны целые секунды — договоритесь о DATE_ATOM или c; для большой даты за 9999 год нужен расширенный год.

Подготовить строку с честной отметкой GMTСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-06-15 10:30:00', new DateTimeZone('Europe/Moscow'));
echo 'Время редакции: ', $date->format('d.m.Y H:i P'), "\n";
$utc = DateTimeImmutable::createFromInterface($date)->setTimezone(new DateTimeZone('UTC'));
$headerValue = $utc->format('D, d M Y H:i:s \G\M\T');
echo 'Значение Last-Modified: ', $headerValue, "\n";
Ожидаемый результат
Время редакции: 15.06.2024 10:30 +03:00
Значение Last-Modified: Sat, 15 Jun 2024 07:30:00 GMT

CreateFromInterface создаёт независимый Immutable, setTimezone показывает тот же момент в UTC. Только после этого шаблон добавляет буквальные G/M/T: каждый символ защищён обратным слешем от распознавания. D и M дают английские сокращения для этого протокола. Строка готова, но HTTP-заголовок здесь не отправляется. Просто написать GMT к московским 10:30 означало бы сообщить другой момент.

Сравните: DateTimeImmutable::createFromFormat, DateTimeImmutable::setTimezone, date, gmdate.

Время, часовые пояса и календарь. Практические задачи с датами.

DateTimeImmutable::modifyИзменить дату по понятному парсеру текстовому правилу, например «следующий понедельник».

PHP 5.5+ · date

public function modify(string $modifier): DateTimeImmutable

Изменить дату по понятному парсеру текстовому правилу, например «следующий понедельник».

Часть вызоваДоговор
ВозвратНовый объект; исходный не меняется. Сохраните результат.
modifierСтрока поддерживаемой грамматики: +1 day, next monday, last day of next month, абсолютная дата, время. Английская грамматика PHP не является распознаванием произвольного человеческого текста.
Ошибкиdate_modify: warning и false при неразбираемой строке. Методы с PHP 8.3 бросают DateMalformedStringException; раньше тоже warning/false.
Тип возвратаС 8.4 tentative return type методов больше не содержит false; процедурный date_modify продолжает возвращать DateTime|false.
МесяцыНесуществующий день после сдвига месяца переносится дальше; автоматического ограничения последним днём нет.
Путь даты в первом примере
  1. date:15.06.2024 10:30 UTC
  2. modify получает строку +1 day
  3. next:16.06.2024 10:30 UTC
  4. format → подпись нового окончания брони

Сначала определите правило задачи: «календарный день», «ровно 24 часа», «последний день месяца» означают разное. Сравнение календарной и прошедшей длительности. Здесь каждый результат сохраняется в отдельную переменную: immutable не изменяет исходник.

Бронь уже заканчивается 15 июня в 10:30 UTC. Нужно оставить часы и перенести окончание на следующий календарный день. Modify получает строку с правилом; сама строка не является PHP-выражением. Вызываем метод у конкретной даты через ->.

Продлить бронирование на один календарный деньСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-06-15 10:30:00', new DateTimeZone('UTC'));
echo 'До продления: ', $date->format('d.m.Y H:i'), "\n";
$next = $date->modify('+1 day');
echo 'Новое окончание брони: ', $next->format('d.m.Y H:i'), "\n";
echo 'Дата в исходной переменной: ', $date->format('d.m.Y H:i'), "\n";
Ожидаемый результат
До продления: 15.06.2024 10:30
Новое окончание брони: 16.06.2024 10:30
Дата в исходной переменной: 15.06.2024 10:30

В строке +1 day знак+ означает движение вперёд,1 — число дней, day — английское слово грамматики даты. Правило применяется к date; возвращённая дата попадает в next и затем в подпись нового срока. Исходный date остаётся 15 июня; next — новый объект. Результат Immutable надо сохранить: в PHP8.5 это дополнительно отмечено атрибутом NoDiscard.

Сравнить перенос месяца и конец следующего месяцаСамостоятельный пример
<?php
$date = new DateTimeImmutable('2023-01-31 10:30:00', new DateTimeZone('UTC'));
$plain = $date->modify('+1 month');
$monthEnd = $date->modify('last day of next month');
echo 'Начало расчёта: ', $date->format('d.m.Y H:i'), "\n";
echo 'Обычное +1 month: ', $plain->format('d.m.Y H:i'), "\n";
echo 'По правилу конца месяца: ', $monthEnd->format('d.m.Y H:i'), "\n";
Ожидаемый результат
Начало расчёта: 31.01.2023 10:30
Обычное +1 month: 03.03.2023 10:30
По правилу конца месяца: 28.02.2023 10:30

В первом расчёте январь заменяется февралём при дне 31; лишние три дня переносятся в март. Второе правило сразу выбирает последний день февраля. Оба вызова начинают с неизменённого date. Для подписки результат выбирает договор. «Сохранить номер дня, ограничив концом месяца» — ещё одно правило: для 15 января оно даст 15 февраля, а не 28-е.

Выбрать день встречи и затем времяСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-01-01 00:00:00', new DateTimeZone('UTC'));
$day = $date->modify('2024-06-17');
$meeting = $day->modify('14:30');
echo 'Подтверждение: встреча ', $meeting->format('d.m.Y H:i P'), "\n";
Ожидаемый результат
Подтверждение: встреча 17.06.2024 14:30 +00:00

Первый modifier заменяет календарный день на 17 июня, второй — часы и минуты на 14:30. Каждый возврат используется следующим вызовом. Это управляемые автором строки; для поля ввода с обещанным форматом нужен createFromFormat и проверка warnings/errors, а не надежда на строгость гибкого парсера.

Сохранить прежнюю бронь при ошибочном правилеСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-06-15 10:30:00', new DateTimeZone('UTC'));
try {
    $next = $date->modify('not a date at all');
    echo 'Продление подтверждено до ', $next->format('d.m.Y H:i'), "\n";
} catch (DateMalformedStringException $error) {
    echo "Продление отменено: правило не разобрано\n";
}
echo 'Действующая бронь до ', $date->format('d.m.Y H:i'), "\n";
Ожидаемый результат
Продление отменено: правило не разобрано
Действующая бронь до 15.06.2024 10:30

PHP8.3+. Неверная грамматика бросает исключение до присваивания next, поэтому подтверждение внутри try не печатается. Catch выводит отказ; дальнейший код показывает прежний срок. Валидное, но не соответствующее договору +1 month не обязано бросать исключение: его календарный результат проверяют отдельно.

Сравните: DateTimeImmutable::add, DateTimeImmutable::sub, DateTimeImmutable::createFromFormat, strtotime.

Время, часовые пояса и календарь. Практические задачи с датами.

Построить три следующих окончания месяцаСамостоятельный пример
<?php
$date = new DateTimeImmutable('2024-01-31 10:00:00', new DateTimeZone('UTC'));
for ($number = 1; $number <= 3; $number++) {
    $date = $date->modify('last day of next month');
    echo 'Списание ', $number, ': ', $date->format('d.m.Y H:i'), "\n";
}
Ожидаемый результат
Списание 1: 29.02.2024 10:00
Списание 2: 31.03.2024 10:00
Списание 3: 30.04.2024 10:00

Договор здесь явно задаёт последний день каждого месяца. На каждом шаге for number увеличивается, modify создаёт следующий месяц с его настоящим последним днём, присваивание date сохраняет новый объект для следующего шага. Часы 10:00 остаются. Это подготовка расписания, деньги не списываются. Для договора «30-е число, а в коротком месяце последний день» нужно хранить исходное число 30 отдельно: после февраля его нельзя восстановить из одного объекта даты.

Подготовить начало следующего расчётного периодаСамостоятельный пример
<?php
$current = new DateTimeImmutable('2024-06-15 10:00:00', new DateTimeZone('UTC'));
$day = $current->modify('first day of next month');
$startsAt = $day->setTime(0, 0);
echo 'Следующий период начинается: ', $startsAt->format('d.m.Y H:i'), "\n";
echo 'Исходная проверка: ', $current->format('d.m.Y H:i'), "\n";
Ожидаемый результат
Следующий период начинается: 01.07.2024 00:00
Исходная проверка: 15.06.2024 10:00

First day of next month выбирает 1 июля, сохраняя 10:00. Отдельный setTime(0,0) устанавливает полночь, потому что договор периода требует её явно. Первый возврат хранится в day, второй — в startsAt; current остался 15 июня 10:00. Так не теряется полезное различие между выбором календарного дня и выбором времени начала.

DateTimeImmutable::diffПолучить календарный интервал, переводящий исходную дату в целевую.

PHP 5.5+ · date

public function diff(DateTimeInterface $targetObject, bool $absolute = false): DateInterval

Получить календарный интервал, переводящий исходную дату в целевую.

Часть вызоваДоговор
targetObjectЦелевая дата через общий DateTimeInterface.
absoluteПо умолчанию false. true только сбрасывает знак invert; это не переключатель UTC/прошедших секунд.
ВозвратDateInterval с календарными компонентами и общим числом полных дней days. Даты не изменяются.
DST и обратимостьРазность учитывает часовые переходы. Для абсолютной длительности переведите обе даты в UTC; для секунд можно вычесть timestamps. Календарная операция не всегда обратима.
Путь даты в первом примере
  1. checkIn:15 июня, checkOut:18 июня, обе 00:00 UTC
  2. diff возвращает DateInterval в stay
  3. stay->days =3 →nights
  4. 3 ×3000 =9000 →сумма бронирования

Не путайте format даты с format интервала: у DateInterval используются %a, %d, %h и другие обозначения. %a — полные дни из diff; d — остаточная дневная часть после лет/месяцев. DateInterval::format.

Для известного правильного диапазона «заезд 15 июня, выезд 18 июня» нужно рассчитать три ночи. Diff получает конечную дату и возвращает объект интервала. Запись stay->days читает его поле, а не вызывает метод: скобок после days нет. Для пользовательского ввода сначала проверяют порядок дат; ниже такой выбор показан отдельно.

Получить число ночей из дат заезда и выездаСамостоятельный пример
<?php
$checkIn = new DateTimeImmutable('2024-06-15 00:00:00', new DateTimeZone('UTC'));
$checkOut = new DateTimeImmutable('2024-06-18 00:00:00', new DateTimeZone('UTC'));
$stay = $checkIn->diff($checkOut);
$nights = $stay->days;
$total = $nights * 3000;
echo 'Заезд: ', $checkIn->format('d.m.Y'), '; выезд: ', $checkOut->format('d.m.Y'), "\n";
echo 'Ночей: ', $nights, '; сумма: ', $total, ' руб.', "\n";
Ожидаемый результат
Заезд: 15.06.2024; выезд: 18.06.2024
Ночей: 3; сумма: 9000 руб.

Diff возвращает DateInterval в stay; его свойство days для результата diff содержит общее число полных дней. Между полуночами 15 и 18 июня их 3; nights умножается на цену 3000 и даёт 9000. Это правило бронирования календарных ночей, выезд 18-го не добавляет четвёртую ночь. UTC здесь носитель выбранных календарных дней; реальные местные часы заезда/выезда задают отдельно. Ни одна исходная дата не изменилась.

Сообщить просрочку, сохранив направление интервалаСамостоятельный пример
<?php
$now = new DateTimeImmutable('2024-06-18 00:00:00', new DateTimeZone('UTC'));
$deadline = new DateTimeImmutable('2024-06-15 00:00:00', new DateTimeZone('UTC'));
$signed = $now->diff($deadline);
$positive = $now->diff($deadline, true);
if ($signed->invert === 1) {
    echo 'Срок пропущен на ', $signed->days, ' дня', "\n";
}
echo 'Интервал от проверки к сроку: ', $signed->format('%R%a'), "\n";
echo 'Тот же интервал с absolute=true: ', $positive->format('%R%a'), "\n";
Ожидаемый результат
Срок пропущен на 3 дня
Интервал от проверки к сроку: -3
Тот же интервал с absolute=true: +3

От 18 июня к 15-му нужно двигаться назад: invert=1, days=3. Эта пара позволяет вывести понятную просрочку. У DateInterval шаблон %R печатает обязательный знак, %a — общее число дней. Absolute=true сбрасывает направление, поэтому получится+3, но срок от этого не стал будущим. Флаг не превращает календарный интервал в абсолютные прошедшие секунды.

Отличить ежедневное расписание от прошедшего времениСамостоятельный пример
<?php
$start = new DateTimeImmutable('2024-03-09 12:00:00', new DateTimeZone('America/New_York'));
$end = new DateTimeImmutable('2024-03-10 12:00:00', new DateTimeZone('America/New_York'));
$calendar = $start->diff($end);
$utcStart = DateTimeImmutable::createFromInterface($start)->setTimezone(new DateTimeZone('UTC'));
$utcEnd = $end->setTimezone(new DateTimeZone('UTC'));
$elapsed = $utcStart->diff($utcEnd);
echo 'По местному расписанию: ', $calendar->format('%a дн. %h ч.'), "\n";
echo 'Фактически прошло: ', $elapsed->format('%a дн. %h ч.'), "\n";
echo 'Для расчёта секунд: ', $end->getTimestamp() - $start->getTimestamp(), "\n";
Ожидаемый результат
По местному расписанию: 1 дн. 0 ч.
Фактически прошло: 0 дн. 23 ч.
Для расчёта секунд: 82800

Две встречи идут в полдень соседних местных дней. Весенний перевод часов сократил прошедшее время до 23 часов. Первый diff описывает местный календарный путь; после перевода обеих дат в UTC второй diff показывает прошедший интервал без перехода зоны. Разность timestamp даёт 82800 секунд. Для замера скорости программы используют монотонные часы hrtime: календарные часы системы могут корректироваться.

Не потерять месяц в расчёте оплаты по днямСамостоятельный пример
<?php
$start = new DateTimeImmutable('2024-01-01 00:00:00', new DateTimeZone('UTC'));
$end = new DateTimeImmutable('2024-02-03 00:00:00', new DateTimeZone('UTC'));
if ($end <= $start) {
    echo "Период аренды должен заканчиваться позже начала\n";
} else {
    $period = $start->diff($end);
    $total = $period->days * 100;
    echo 'Календарное описание: ', $period->format('%m мес. %d дн.'), "\n";
    echo 'Оплачиваемых дней: ', $period->days, "\n";
    echo 'При 100 руб. за день: ', $total, ' руб.', "\n";
}
Ожидаемый результат
Календарное описание: 1 мес. 2 дн.
Оплачиваемых дней: 33
При 100 руб. за день: 3300 руб.

Оператор <= сравнивает сами даты и не допускает обратный или нулевой период. В интервале m=1 и d=2 — компоненты после выделения полного месяца; days=33 — весь промежуток. Поэтому счёт равен 3300, а не 200 и не 3200. Месяц нельзя автоматически заменить 30 днями. %d у DateInterval означает остаток дней, %a — полный счёт из diff; это отдельный язык формата относительно DateTime::format.

Сравните: DateInterval::format, DateTimeImmutable::add, DateTimeImmutable::sub, DateTimeImmutable::getTimestamp.

Время, часовые пояса и календарь. Практические задачи с датами.

timeЗафиксировать момент создания записи и вычислить простой срок действия.

PHP 4+ · date

time(): int

Зафиксировать момент создания записи и вычислить простой срок действия.

Часть вызоваДоговор
АргументыНет.
ВозвратТекущая Unix-секунда с эпохи 1970-01-01 00:00 UTC, int. Разрешение ответа — целая секунда.
ГраницыНи миллисекунды, ни местное время, ни гарантия точности/монотонности системных часов.
Путь даты в первом примере
  1. time() →createdAt с текущей Unix-секундой
  2. expiresAt=createdAt+300
  3. checkAt=createdAt+120 —модель проверки
  4. remaining=180 →код ещё можно предъявить

Скобки time() пусты: функция читает текущие системные часы и возвращает int секунд. $createdAt сохраняет один снимок. Не вызывайте time снова для каждой части одной записи, если они должны иметь общий момент; вторая секунда может уже отличаться.

Для кода подтверждения нужен момент создания и срок действия. Пустые скобки time() означают, что аргументы не передаются; результат —целое число секунд от Unix-эпохи, а не текст даты. Например, число 1704067200 обозначает 1 января 2024 00:00 UTC. В первом расчёте используем настоящее чтение часов, а следующая функция получает фиксированный снимок, чтобы показать точные даты.

Рассчитать оставшееся время кода подтвержденияСамостоятельный пример
<?php
$createdAt = time();
$expiresAt = $createdAt + 5 * 60;
$checkAt = $createdAt + 2 * 60;
$remaining = max(0, $expiresAt - $checkAt);
echo 'Полный срок кода: ', $expiresAt - $createdAt, " секунд\n";
echo 'При проверке через две минуты осталось: ', $remaining, " секунд\n";
if ($remaining > 0) { echo "Код ещё можно предъявить\n"; }
Ожидаемый результат
Полный срок кода: 300 секунд
При проверке через две минуты осталось: 180 секунд
Код ещё можно предъявить

Time прочитал системные часы один раз и вернул целую Unix-секунду в createdAt. Её конкретное число меняется при запуске; относительный результат не меняется. ExpiresAt —срок через 300 секунд. CheckAt явно моделирует проверку через 120 секунд, это не второй вызов часов и не ожидание. Вычитание даёт 180, max не допускает отрицательный остаток. Пример только рассчитывает срок: он не отправляет код и не реализует весь процесс подтверждения.

Проверить заранее сохранённый срок на точной границеСамостоятельный пример
<?php
function codeStatus(int $expiresAt, int $now): string
{
    if ($now >= $expiresAt) { return 'Код истёк; запросите новый'; }
    return 'Осталось ' . ($expiresAt - $now) . ' секунд';
}
$createdAt = 1704067200;
$expiresAt = $createdAt + 300;
echo 'Создан: ', gmdate('d.m.Y H:i:s', $createdAt), " UTC\n";
echo 'Действует до: ', gmdate('d.m.Y H:i:s', $expiresAt), " UTC (граница исключена)\n";
echo 'Проверка 00:04:30: ', codeStatus($expiresAt, 1704067470), "\n";
echo 'Проверка 00:05:00: ', codeStatus($expiresAt, 1704067500), "\n";
Ожидаемый результат
Создан: 01.01.2024 00:00:00 UTC
Действует до: 01.01.2024 00:05:00 UTC (граница исключена)
Проверка 00:04:30: Осталось 30 секунд
Проверка 00:05:00: Код истёк; запросите новый

CreatedAt здесь намеренно фиксированный снимок, не новое чтение текущих часов. В рабочем месте создания вместо него приходит time(), а в месте проверки отдельный time() передают вторым аргументом codeStatus. В первом вызове локальные параметры получают 1704067500 и 1704067470: return возвращает строку с 30 секундами прямо во внешний echo. Во втором now равен expiresAt, поэтому ранний return выбирает отказ. Такая явная передача now позволяет проверять границы без sleep.

Timestamp не содержит timezone и не меняется от date_default_timezone_set. Системные часы могут корректироваться: для длительности измерения используйте монотонный hrtime, а не обещайте неубывание time. Значение не уникальный ID и не источник случайности. $_SERVER["REQUEST_TIME"] — снимок начала запроса при соответствующей SAPI, а не новый вызов часов. На 32-bit действует диапазон PHP int около 1901…2038; на 64-bit он шире, но тоже конечен.

Сравните: microtime, gettimeofday, date, DateTimeImmutable::getTimestamp.

Время, часовые пояса и календарь. Практические задачи с датами.

Возвращает момент по системным часам. У timestamp нет собственного часового пояса; пояс применяется при представлении календарной даты.

Выдать локальный срок кешаСамостоятельный пример
<?php
declare(strict_types=1);

$now = time();
$expiresAt = $now + 300;
echo $expiresAt - $now, "\n";
Ожидаемый результат
300

Получаем один now и строим TTL5 минут. Повторный time посреди расчета мог бы дать другую секунду и сделать граничный тест нестабильным.

Проверить истечение токена через явное nowСамостоятельный пример
<?php
declare(strict_types=1);

function isExpired(int $expiresAt, int $now): bool
{
    return $now >= $expiresAt;
}
$now = time();
echo isExpired($now - 1, $now) ? "Истек\n" : "Действует\n";
Ожидаемый результат
Истек

Проверка границы>= задана явно. В тесте можно заменить now фиксированным числом; реальный токен дополнительно проверяет подпись и назначение.

Сравните. DateTimeImmutable удобен для календарной логики; time — простой timestamp. В примерах текущее значение не печатается, чтобы результат был воспроизводимым.

checkdateОтклонить несуществующую дату доставки до её сохранения.

PHP 4+ · date

checkdate(int $month, int $day, int $year): bool

Отклонить несуществующую дату доставки до её сохранения.

Часть вызоваДоговор
month1…12.
day1…число дней именно в этом месяце/году.
year1…32767; год 0 и отрицательные не принимаются.
Возврат / ошибкиtrue/false для допустимых int; неправильный тип аргумента — TypeError. Календарь григорианский, зона не нужна.
Путь даты в первом примере
  1. month=2,day=29,year=2024
  2. checkdate(month,day,year) →true
  3. if выбирает подтверждение 29.02.2024
  4. Для 2023 возврат false →доставка не назначается

checkdate принимает отдельные целые числа в порядке месяц, день, год. Она не разбирает строку "29.02.2024" и не проверяет часы. В boolean-выводе используйте условие ? "да" : "нет": сам false при echo не даст видимого слова.

Подтвердить существующий день доставкиСамостоятельный пример
<?php
$year = 2024;
$month = 2;
$day = 29;
if (checkdate($month, $day, $year)) {
    echo 'Дата доставки принята: ', sprintf('%02d.%02d.%04d', $day, $month, $year), "\n";
} else { echo "Выберите существующий день доставки\n"; }
$year = 2023;
if (!checkdate($month, $day, $year)) {
    echo 'Доставка 29.02.', $year, " не назначена: такого дня нет\n";
}
Ожидаемый результат
Дата доставки принята: 29.02.2024
Доставка 29.02.2023 не назначена: такого дня нет

В месте вызова значения 2,29,2024 передаются в параметры month,day,year именно в таком порядке. True выбирает ветку с подписью даты. После изменения year на 2023 ответ false и знак! превращает его в условие отказа. Sprintf только собирает уже проверенные числа в строку; функция checkdate сама ничего не выводит и не переносит день на март.

Проверять календарь до нормализацииСамостоятельный пример
<?php
declare(strict_types=1);

function calendarDate(int $year, int $month, int $day): ?string
{
    if (!checkdate($month, $day, $year)) { return null; }
    return sprintf('%04d-%02d-%02d', $year, $month, $day);
}
echo calendarDate(2024, 4, 30) ?? 'Ошибка', "\n";
echo calendarDate(2024, 4, 31) ?? 'Ошибка', "\n";
Ожидаемый результат
2024-04-30
Ошибка

! отрицает ответ; ?string разрешает null. sprintf формирует строку уже проверенной даты. Эта функция принимает int: HTTP-поля сначала нужно проверить как строки и безопасно преобразовать.

Дата — ещё не момент: день рождения можно хранить без timezone и timestamp. mktime/strtotime нормализуют часть ошибочных дат, поэтому их успешный результат не заменяет checkdate. Для фиксированного строкового формата используйте createFromFormat с проверкой warnings и обратного форматирования. Попробуйте годы 1900 и 2000 с 29 февраля: первый false, второй true.

Сравните: mktime, strtotime, date_parse_from_format.

Время, часовые пояса и календарь. Практические задачи с датами.

Возвращает bool с учетом длины месяца и високосных годов. Не разбирает строки и не проверяет бизнес-диапазон дат.

29 февраля в разные годыСамостоятельный пример
<?php
declare(strict_types=1);

echo checkdate(2, 29, 2024) ? "Да\n" : "Нет\n";
echo checkdate(2, 29, 2026) ? "Да\n" : "Нет\n";
Ожидаемый результат
Да
Нет

Удобная граничная проверка для трех отдельных полей формы. Сами поля сначала должны пройти проверку целых и диапазона.

Отвергнуть 31 апреляСамостоятельный пример
<?php
declare(strict_types=1);

$month = 4;
$day = 31;
$year = 2026;
echo checkdate($month, $day, $year) ? "Дата принята\n" : "Нет такого дня\n";
Ожидаемый результат
Нет такого дня

Проверка не нормализует 31 апреля в 1 мая, что удобно для пользовательского ввода. Для прибавления периода применяется другая операция.

Сравните. Для строгой строки Y-m-d используйте createFromFormat с getLastErrors и обратной проверкой формата.

Во втором полном примере вызов calendarDate(2024,4,30) передаёт год, месяц и день в одноимённые локальные параметры. Внутри порядок меняется для checkdate(month,day,year). При отказе return null сразу завершает функцию; ?? в месте вызова выбирает «Ошибка». Для 30 апреля sprintf возвращает 2024-04-30, этот return попадает прямо в echo. Функция не печатает сама, поэтому возвращённую строку можно вместо echo сохранить в поле заявки.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.