Перейти к содержимому
Поиск
Разработка

Carbon в PHP: даты, часовые пояса и сроки — мастерская и 10 сценариев

Учимся считать сроки и показывать время без ошибок. Десять сценариев Carbon, 720 проверенных комбинаций, настоящий PHP-проект и размещение на Linux VPS.

Александр Некрасов
Carbon — даты, часовые пояса и сроки в PHP

Carbon помогает работать с датами и временем в PHP: рассчитывать сроки, переводить встречи между часовыми поясами, формировать периоды отчёта, показывать даты на языке клиента и проверять пограничные ситуации. Библиотека подключается через Composer и подходит обычному PHP-проекту без Laravel или Symfony. Польза появляется там, где за датой стоит правило: когда заканчивается подписка, какой день попадает в выгрузку и что именно означает «через сутки».

На этой странице можно попробовать десять ситуаций, сравнить результаты и скачать работающий пример. Мы разберём привычные операции вместе с ошибками, которые заметны уже пользователю: исчезнувшим последним днём отчёта, неожиданным переносом 31 января на март, встречей не в том часовом поясе и тестом, который проходит утром, но падает ночью. Исходники содержат настоящий PHP API, зафиксированные зависимости, тесты и путь от локального запуска до закрытой мастерской на Linux VPS.

Что выполняется на странице. Встроенная мастерская выбирает из 720 заранее рассчитанных ответов настоящего Carbon 3.15.1. Все допустимые комбинации условий записаны из PHP и проверены. PHP в браузере не запускается. В скачиваемом проекте тот же интерфейс обращается к реальному PHP API и разрешает ввод собственной даты.

Где Carbon полезен и что получит пользователь сервиса

Представим CRM сервисной компании. Клиент выбирает время визита, оператор видит срок ответа, руководитель смотрит заявки за месяц, а система отправляет напоминание. Во всех четырёх местах встречается дата, но смысл различается. Визит привязан к моменту времени, срок зависит от рабочего графика, отчёт использует границы диапазона, а напоминание должно быть отправлено один раз. Carbon упрощает вычисления; правила и ответственность приложения остаются явными.

В личном кабинете библиотека помогает показать «7 ноября, 14:30» вместо сырого timestamp. В подписках — определить следующую дату продления. В интеграции — привести ISO-строку партнёра к UTC. В административной таблице — отделить просроченные заявки от будущих. В тестах — подставить фиксированное «сейчас» и проверить пограничную секунду без ожидания настоящего времени. Эти задачи полезны даже в маленьком проекте с одним PHP-контроллером.

Начинайте с понятного результата: например, оператор должен видеть срок по своему поясу, а клиент — по своему. Затем отдельно определите, какое значение хранится и какое показывается. Так код не будет случайно использовать форматированную русскую строку в сравнении или считать разные локальные представления двумя встречами. Мастерская демонстрирует именно этот переход от правила к результату, а не каталог всех методов библиотеки.

Carbon, DateTimeImmutable и timestamp: что выбрать

Стандартный PHP уже предоставляет DateTime, DateTimeImmutable, DateTimeZone, DateInterval и DatePeriod. Для нескольких простых преобразований этого достаточно. Carbon расширяет привычный API удобными методами, локализацией и тестовыми средствами. Он не заменяет базу часовых поясов собственными предположениями и не превращает неоднозначное требование в однозначное: сначала нужно определить смысл операции, затем выбрать метод.

$native = new DateTimeImmutable('2026-11-07 12:00', new DateTimeZone('UTC'));
$nativeNext = $native->modify('+3 days');

use Carbon\CarbonImmutable;
$carbon = CarbonImmutable::parse('2026-11-07 12:00', 'UTC');
$carbonNext = $carbon->addDays(3);

Если весь проект уже использует DateTimeInterface, Carbon можно подключить постепенно на границе отдельного сервиса. Не обязательно переписывать все значения сразу. Timestamp удобен для передачи абсолютного момента, но сам по себе не описывает правило «каждую среду в 09:00 по Берлину». Для такого расписания требуется исходная локальная договорённость и идентификатор зоны. Строка с датой рождения вообще не нуждается в timestamp, пока бизнес-задача не требует конкретного момента.

Источник: Carbon и стандартные классы PHP.

Мини-мастерская: десять ситуаций и 720 проверенных комбинаций

Выберите задачу, затем дату, часовой пояс и язык. Для операций со сроком доступны один, три или пять дней. Нажмите «Показать результат»: ниже появятся исходные условия, вычисленные значения, объяснение и PHP-код операции. После изменения настройки кнопки экспорта блокируются до нового расчёта. Это позволяет избежать неприятной ошибки интерфейса, когда параметры уже новые, а скачивается ответ предыдущего запроса.

Открыть мастерскую на весь экран ↗Скачать PHP-проект и исходники ↓

Для первого знакомства попробуйте три маршрута. В сценарии суток выберите Берлин и 29 марта 2026 года, затем 25 октября: получится 23 и 25 часов. В сценарии месяца сравните прибавление к 31 января с переполнением и без него. В проверке формы выберите 30 февраля и посмотрите, как объясняется отклонение. Каждый пример показывает результат вместе с причиной, поэтому метод проще связать с собственной задачей.

Кнопка JSON сохраняет условия и ответ. Кнопка PHP создаёт файл для запуска в корне скачанного проекта после composer install. Это не самостоятельная библиотека в одном файле: пример использует класс Lab из архива. В серверной версии появляется дополнительное поле собственной даты, а каждое нажатие действительно вызывает Carbon. Статическая страница не делает приближённых пересчётов на JavaScript и не выдаёт их за ответ PHP.

Установка через Composer и воспроизводимый проект

В примерах зафиксирован Carbon 3.15.1. Готовый проект рассчитан на PHP 8.3 и новее, хотя требования отдельной версии библиотеки могут быть шире. Такой выбор позволяет проверить один понятный набор зависимостей на Windows и в Linux-контейнере. Файл composer.lock включён в архив: команда install устанавливает проверенный набор, а update предназначена для намеренного обновления с последующим повторением тестов.

composer require nesbot/carbon:3.15.1
composer check-platform-reqs
composer show nesbot/carbon

Если создаёте новый проект, команда require добавит пакет и автозагрузку. Если распаковали наши исходники, выполните composer install и не повторяйте require без причины. При ошибке платформы сначала посмотрите php -v и composer check-platform-reqs: PHP в терминале и PHP веб-сервера иногда оказываются разными установками. Замена версии пакета наугад обычно скрывает причину, вместо того чтобы исправить окружение.

<?php
require __DIR__.'/vendor/autoload.php';
use Carbon\CarbonImmutable;
echo CarbonImmutable::now('UTC')->toIso8601String();

Подробнее установка зависимостей, lock-файл и автозагрузка разобраны в руководстве по Composer. В архиве также есть Dockerfile: он устанавливает зависимости при сборке, а не скачивает их при каждом запросе посетителя. Проверка пакета по официальной странице и точная фиксация версии помогают воспроизвести примеры после выхода следующего релиза.

Источник: пакет nesbot/carbon, версия и требования.

CarbonImmutable: исходная дата не должна неожиданно измениться

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

$created = CarbonImmutable::parse('2026-11-07 10:00', 'UTC');
$replyDue = $created->addDays(1);
$finishDue = $created->addDays(3);
echo $created->toDateString(); // 2026-11-07
echo $finishDue->toDateString(); // 2026-11-10

У неизменяемости есть обратная сторона, которую легко пропустить: вызов $created->addDay() без присваивания не меняет переменную. Если вы привыкли к Carbon, проверьте подобные места при переходе. Для существующего mutable-объекта можно получить копию или преобразовать его через toImmutable. Не смешивайте эти подходы в одной цепочке без причины: читателю кода должно быть понятно, какой объект считается исходным.

В мастерской одновременно видны исходный CarbonImmutable, новый результат и изменённый Carbon. Это удобно для объяснения поведения коллегам: поменяйте количество дней и посмотрите, какая строка остаётся прежней. В прикладном коде безопаснее передавать в расчёт неизменяемые даты и возвращать новый результат, чем полагаться на то, что вызывающая функция не заметит изменения аргумента.

Строгая дата из формы: почему 30 февраля нельзя молча исправлять

Пользователь ввёл 2026-02-30. Для формы заявки нормальный результат — понятная ошибка, а не автоматический перенос на март. Универсальные парсеры удобны для заранее известных строк разработчика, но могут нормализовать переполнения или понимать относительные выражения. Поэтому контракт формы должен быть узким: точный формат, допустимый диапазон и проверка существования календарной даты.

$input = '2026-02-30';
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/D', $input)) {
    throw new InvalidArgumentException('Ожидается ГГГГ-ММ-ДД');
}
[$year, $month, $day] = array_map('intval', explode('-', $input));
if (!checkdate($month, $day, $year)) {
    throw new InvalidArgumentException('Такой даты нет');
}
$date = CarbonImmutable::createSafe($year, $month, $day, 0, 0, 0, 'UTC');

В готовом Lab::strictDate дополнительно ограничены годы 2000–2100. Это предел учебного приложения, а не ограничение Carbon. Если вашему сервису нужны исторические даты, измените контракт и тесты осознанно. На сервере повторяйте проверку независимо от HTML input: запрос можно отправить напрямую, а браузерная форма не является источником доверия. Ошибку связывайте с конкретным полем, сохраняя остальные введённые данные.

Источник: PHP: нормализация значений createFromFormat.

Источник: Carbon: createSafe и создание дат.

Дата без времени и момент времени — разные данные

Дата документа, день рождения и день отпуска обычно имеют смысл календарного дня. Встреча, платёж и запись журнала имеют смысл момента. Если превратить день рождения в полночь UTC, а затем автоматически показать в Америке, календарная дата может стать предыдущей. Пользователь увидит неправильные личные данные, хотя математически преобразование выполнено корректно. Ошибка возникла при выборе модели, до вызова библиотеки.

Для календарной даты храните каноническую строку YYYY-MM-DD или подходящий тип DATE в базе. Для момента храните UTC, timestamp или нормализованный формат с явно понятной зоной. Отдельно сохраните IANA-пояс, если он нужен для будущего расписания. Не называйте все поля просто date: created_at_utc, appointment_zone и document_date дают разработчику больше информации о том, какие преобразования допустимы.

$documentDate = '2026-11-07'; // календарный день, без перевода поясов
$createdAt = CarbonImmutable::now('UTC')->toIso8601String();
$appointmentZone = 'Europe/Berlin';

В API описывайте это различие в контракте. При показе даты документа форматируйте сам календарный день. При показе созданной заявки переводите момент в выбранную зону. Для напоминания о ежегодном событии нужны ещё правила повторения, включая 29 февраля; автоматическое прибавление года не должно незаметно определять политику продукта.

Одна встреча в разных поясах: setTimezone и shiftTimezone

Созвон назначен на 12:00 UTC. В Москве он отображается как 15:00, в другом регионе — по местному смещению. Это один момент, поэтому timestamp должен оставаться неизменным. Для такого представления используется setTimezone. Метод shiftTimezone решает другую задачу: сохраняет цифры локальных часов, но интерпретирует их в новой зоне, следовательно меняет момент. При обычном отображении встречи такая подмена будет ошибкой.

$meeting = CarbonImmutable::parse('2026-11-07 12:00', 'UTC');
$moscow = $meeting->setTimezone('Europe/Moscow');
assert($meeting->timestamp === $moscow->timestamp);

$reinterpreted = $meeting->shiftTimezone('Europe/Moscow');
assert($meeting->timestamp !== $reinterpreted->timestamp);

Пользователь может сказать «перенести встречу на 12:00 по Москве», и это уже изменение расписания. Тогда приложение должно явно подтвердить новый момент и уведомить участников. Не прячьте такую операцию в настройке часового пояса профиля: смена языка или зоны отображения не должна переносить существующие встречи. В мастерской рядом показаны оба timestamp, чтобы отличие было видно без чтения исходного кода.

Используйте названия вроде Europe/Berlin, если нужны региональные правила. Постоянное +01:00 не описывает будущие переходы на летнее время. Для исторических и будущих дат библиотека опирается на данные зоны в окружении; при обновлении PHP и системных пакетов повторяйте тесты важных для вашего бизнеса регионов.

Источник: Carbon: локальное представление и часовые пояса.

Подписка на месяц: переполнение и сохранение якорной даты

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

$start = CarbonImmutable::parse('2026-01-31', 'UTC');
echo $start->addMonthWithOverflow()->toDateString(); // 2026-03-03
echo $start->addMonthNoOverflow()->toDateString();   // 2026-02-28
$second = $start->addMonthNoOverflow()->addMonthNoOverflow();
echo $second->toDateString(); // 2026-03-28

Последняя строка показывает ещё одну тонкость: после ограничения до 28 февраля следующий шаг начинается с 28-го. Если обещано «в последний день каждого месяца», храните исходное правило отдельно и вычисляйте конец целевого месяца. Для цепочки подписок полезно сохранять якорную дату, номер периода и выбранную политику. Иначе каждое последующее списание может постепенно расходиться с первоначальным ожиданием клиента.

В интерфейсе перед подтверждением показывайте конкретную дату следующего продления. Пользователю полезнее увидеть «следующее продление 28 февраля», чем абстрактное «+1 месяц». В тестах держите короткий февраль, високосный год и месяцы с 30/31 днём; одна обычная дата в середине месяца не проверяет выбранную политику.

Источник: Carbon: арифметика месяцев и overflow.

Рабочие дни и SLA: что делает addWeekdays

В учебном сценарии заявка поступила в пятницу, а ответ нужен через один рабочий день. addWeekdays переносит срок на понедельник, пропуская субботу и воскресенье. Но слово «рабочий» часто означает больше: праздники, переносы выходных, регион, сокращённый день, сменный график и время приёма заявки. Наличие метода в библиотеке не означает наличие актуального производственного календаря вашей компании.

$received = CarbonImmutable::parse('2026-11-06 17:00', 'Europe/Moscow');
$due = $received->addWeekdays(1);
echo $due->format('Y-m-d H:i'); // 2026-11-09 17:00

В нашей мастерской правило сформулировано прямо: учитываются понедельник–пятница, начальный день не считается, праздники не загружаются. Для коммерческого SLA сначала согласуйте календарь и граничное время. Заявка после 18:00 может начинать отсчёт со следующей смены; пять рабочих часов могут переноситься через ночь. Такие правила обычно оформляют отдельным сервисом календаря с версией данных и тестами на известных датах.

Carbon удобно использовать внутри этого сервиса для перебора дней, сравнения границ и форматирования ответа. В интерфейсе показывайте клиенту получившийся срок и краткое правило расчёта. Если календарь изменился, не пересчитывайте уже подтверждённые обязательства бесследно: приложению может потребоваться сохранить версию правила и причину переноса. Это уже логика предметной области, а не свойство addWeekdays.

DST: почему сутки бывают короче или длиннее 24 часов

Отчёт за день и интервал длиной 24 часа — не одно и то же. В Берлине при весеннем переводе часов между двумя местными полуночами проходит 23 часа, осенью — 25. Если строить суточный диапазон прибавлением постоянных 86400 секунд, на границе смены смещения получится другой локальный час. В результате запись может попасть не в тот отчёт или уведомление уйдёт позже ожидаемого.

$start = CarbonImmutable::parse('2026-03-29 00:00', 'Europe/Berlin');
$nextMidnight = $start->addDay();
echo $start->diffInHours($nextMidnight); // 23

$autumn = CarbonImmutable::parse('2026-10-25 00:00', 'Europe/Berlin');
echo $autumn->diffInHours($autumn->addDay()); // 25

Для календарного отчёта вычисляйте следующую местную полночь, затем переводите обе границы в UTC. Для таймера с длительностью ровно час сравнивайте моменты. Название переменной должно помогать различать эти модели: nextLocalDay и expiresAtUtc лучше, чем общий endDate. На странице смените пояс на Москву: те же даты дадут обычные 24 часа, поэтому нельзя делать универсальное правило из одного регионального примера.

Весной некоторые локальные показания часов не существуют, осенью одно время встречается дважды. В форме записи на такие моменты нужно либо запретить неоднозначный слот, либо попросить выбрать смещение. createSafe помогает обнаружить пропущенный час, но решение о повторяющемся осеннем времени должно быть частью контракта приложения.

Источник: Carbon: разница времени и DST.

Разница дат в Carbon 3: знак, дробь и округление

Вычисление «сколько осталось» легко испортить невидимым округлением. В Carbon 3 семейство diffIn возвращает дробное число и учитывает направление. Это отличается от привычных примеров для старых версий, где разработчик ожидал положительное целое. Для интерфейса срока отрицательная разница полезна: она отделяет просрочку от будущего. Если убрать знак слишком рано, обе ситуации будут выглядеть одинаково.

$now = CarbonImmutable::parse('2026-11-07T12:00:00+00:00');
$due = CarbonImmutable::parse('2026-11-07 17:30', 'Europe/Moscow');
$hours = $now->diffInHours($due, false); // 2.5
$expired = $now->gte($due);
$fullHours = (int) floor(abs($hours)); // 2

Выберите округление по смыслу. Для количества полностью прошедших часов подойдёт floor абсолютного значения. Для предупреждения «менее трёх часов» полезнее сравнить исходную дробь с порогом. Для тарификации нельзя автоматически применить математический приём из интерфейса: правило оплаты может считать начатые интервалы. На границе срока заранее определите, считается ли равенство уже просрочкой; в нашем примере now >= due означает наступивший срок.

В мастерской «сейчас» зафиксировано на 7 ноября 2026 года, 12:00 UTC. Благодаря этому результаты остаются воспроизводимыми и через месяц. Реальное приложение получает текущее время при выполнении запроса или через внедрённый Clock. Не копируйте учебную фиксированную дату в рабочий расчёт без замены источника времени.

Источник: Carbon 3: изменения diffIn и timestamp.

Периоды отчёта: включённое начало и исключённый конец

Пользователь просит заявки за ноябрь. Удобный контракт — диапазон от первого ноября включительно до первого декабря исключительно. Тогда соседний декабрьский отчёт начинается ровно на той же границе, и запись не учитывается дважды. Подход не зависит от точности базы: секунды, миллисекунды и микросекунды не заставляют подбирать «последний возможный момент дня».

use Carbon\CarbonPeriod;
$start = CarbonImmutable::parse('2026-11-02', 'Europe/Moscow');
$end = $start->addDays(3);
$period = CarbonPeriod::create($start, '1 day', $end)->excludeEndDate();
foreach ($period as $day) {
    echo $day->toDateString(), PHP_EOL;
}
// 2026-11-02, 2026-11-03, 2026-11-04

Для базы это превращается в условия created_at >= startUtc AND created_at < endUtc. Обе границы сначала строятся в зоне отчёта, потом преобразуются в UTC. Если вместо этого взять начало месяца на UTC-сервере, пользователь из другой зоны может увидеть потерянные записи в первый или последний день. В мастерской диапазон ограничен, чтобы ошибка параметра не создавала бесконечный цикл.

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

Источник: CarbonPeriod и исключение границ.

Локализация: дата на языке клиента и точное значение для API

Интерфейс и обмен данными предъявляют разные требования к формату. Посетителю удобна строка «7 ноября 2026 — 14:30», интеграции нужна однозначная машинная запись. Сохраняйте точный момент отдельно от красивой подписи: локализованный текст может меняться вместе с языком, контекстом и версией переводов. Его нельзя надёжно использовать как ключ или сортировать как дату обычным строковым сравнением.

$date = CarbonImmutable::parse('2026-11-07 14:30', 'Europe/Moscow')->locale('ru');
$label = $date->isoFormat('D MMMM YYYY [—] HH:mm');
$apiValue = $date->toIso8601String();
$weekday = $date->isoFormat('dddd');

Применяйте locale к конкретному объекту, если обслуживаете клиентов с разными языками. Глобальная настройка в долгоживущем процессе может повлиять на следующий запрос или сторонний компонент. Не смешивайте токены format и isoFormat: похожие буквы принадлежат разным системам. Литералы в isoFormat заключайте в квадратные скобки, чтобы текст не интерпретировался как элементы формата.

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

Источник: форматирование строк Carbon.

Замороженное время: тесты без ожидания и случайных полуночей

Тест срока не должен зависеть от того, в какую минуту его запустили. Подмена часов позволяет проверить состояние до дедлайна, ровно на границе и после него за доли секунды. В Carbon для этого есть тестовые средства. В примере используется withTestNow, который ограничивает подмену callback и возвращает прежнее состояние после выполнения. Так тестовая дата не остаётся в дальнейших вычислениях.

$fixed = CarbonImmutable::parse('2026-11-07 10:00', 'Europe/Moscow');
$result = CarbonImmutable::withTestNow($fixed, function () {
    return CarbonImmutable::now('Europe/Moscow')->addDays(3);
});
assert(!CarbonImmutable::hasTestNow());

В большом приложении удобно передавать Clock как зависимость сервиса. Тогда тесты управляют временем явно, а бизнес-код не читает системные часы из десятков мест. Особенно внимательно относитесь к worker: один процесс выполняет много заданий, поэтому случайно оставленная глобальная подмена может затронуть следующую задачу. Если применяете setTestNow вручную, очищайте его в finally, даже когда тест завершается исключением.

В наших проверках отдельно подтверждено, что после сценария замороженного времени hasTestNow возвращает false. Это небольшая, но полезная проверка изоляции. Она защищает не конкретный ожидаемый день, а свойство тестовой среды: каждый следующий сценарий должен начинаться без остаточного состояния предыдущего.

Источник: Carbon: тестовая подмена времени.

Как устроены исходники и настоящий PHP API

Расчёты собраны в Lab::run. Метод принимает небольшой объект условий, проверяет его и возвращает данные для интерфейса: заголовок, строки результата, объяснение и фрагмент кода. API не выполняет присланный PHP и не выбирает произвольный метод по имени пользователя. Сценарии перечислены заранее. Это позволяет перенести вычисления в свой контроллер, сохранив проверяемый контракт и понятные ошибки.

$result = Aen\CarbonLab\Lab::run([
    'scenario' => 'dst',
    'date' => '2026-10-25',
    'zone' => 'Europe/Berlin',
    'locale' => 'ru',
    'amount' => 3,
]);
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

Веб-корнем служит public, а vendor, src, composer.lock и серверная конфигурация находятся выше. Браузер отправляет JSON в api.php, получает результат и выводит строки через textContent. Сохранённые ответы для статьи создаются bin/record.php тем же кодом, поэтому можно проверить соответствие демонстрации и настоящего PHP. Встроенная мастерская не содержит ключей сервера и не обращается к стороннему вычислительному API.

Приложение только считает и не сохраняет входные данные. Для такого калькулятора не нужны таблица пользователей, сессия и операция подтверждения записи. Когда добавите сохранение встречи, появится отдельная задача: авторизация, права на запись, CSRF при cookie-сессии, повторная валидация и обработка конфликта. Готовая дата сама по себе не означает разрешение изменить чужую заявку.

Ошибки API и границы безопасного ввода

Вход ограничен по размеру и работе: тело до 4096 байт, дата из понятного диапазона, четыре разрешённых пояса, два языка, количество от одного до тридцати. Массив вместо объекта, лишние поля и строка вместо целого числа отклоняются. Такие ограничения делают поведение калькулятора предсказуемым и не позволяют превратить небольшой период в огромный вычислительный цикл.

{
  "scenario": "period",
  "date": "2026-11-02",
  "zone": "Europe/Moscow",
  "locale": "ru",
  "amount": 3
}

Неправильный JSON возвращает 400, неподдерживаемый метод — 405, большой запрос — 413, другой Content-Type — 415, ошибочные условия — 422. При непредвиденной ошибке клиент получает короткое сообщение, а сервер пишет класс исключения в журнал. Внутренние пути и подробный stack trace не нужно показывать посетителю. В интерфейсе экспорт недоступен после ошибки: пользователь не скачает последний успешный ответ под видом нового.

Учебный сценарий «строгая проверка» специально возвращает 200 с объяснением «дата отклонена»: это успешная демонстрация правила. В остальных вычислительных сценариях невозможная дата — ошибка условий 422. Различие описано в контракте, чтобы интегратор не определял успех поиском слова «ошибка» внутри текста. В своём приложении придерживайтесь столь же явного разделения результата и ошибки запроса.

Windows: запуск в Docker Desktop и первая проверка

Если не хотите настраивать PHP вручную, используйте Docker Desktop в режиме Linux containers. Распакуйте архив в отдельную рабочую папку и откройте PowerShell. Перед запуском убедитесь, что команды Docker и Compose доступны. Приложение публикует порт только на 127.0.0.1, поэтому учебный API не открывается всей локальной сети. При занятом порте поменяйте левую часть сопоставления в compose.yaml.

Expand-Archive .\carbon-project.zip -DestinationPath .\carbon-demo
Set-Location .\carbon-demo\carbon-project
docker version
docker compose version
docker compose up -d --build --wait
Start-Process http://127.0.0.1:8112
docker compose exec -T web php tests/run.php

Вверху страницы должна быть надпись «Настоящий PHP + Carbon». Это отличает локальный серверный вариант от мастерской с сохранёнными ответами. Выберите собственную дату, затем проверьте сценарий DST. Если страница не открывается, посмотрите docker compose ps и logs –tail 50 web: сначала подтвердите, что сервис вообще запустился, и только потом ищите проблему в JavaScript.

docker compose logs --tail 50 web
docker compose stop
docker compose start --wait

Команда stop завершает работу стенда, сохраняя контейнер. При следующем старте повторная сборка не требуется, если исходники и зависимости не менялись. Для намеренного обновления используйте up -d –build –wait и вновь выполните тесты. Не удаляйте все Docker-данные ради освобождения одного занятого порта: это может затронуть другие ваши проекты.

Windows без Docker: PHP и Composer в обычном терминале

С установленными PHP 8.3+ и Composer пример можно запустить напрямую. Это удобно, если вы уже разрабатываете PHP-приложение локально или хотите отладить один метод. Команды выполняются в корне проекта. Проверка платформы полезна даже после успешной установки: разные терминалы могут использовать различные php.exe из PATH, а веб-сервер — ещё одну конфигурацию.

php -v
composer install --no-interaction --prefer-dist
composer check-platform-reqs
php tests/run.php
php bin/examples.php month 2026-01-31
php -S 127.0.0.1:8112 -t public

Последняя команда занимает терминал до Ctrl+C. Откройте адрес в браузере отдельно. Параметр -t public задаёт правильный веб-корень; если раздавать корень репозитория, рядом с приложением могут оказаться конфигурация и другие служебные файлы. Для скачанного carbon-example.php используйте CLI из корня, где находится vendor/autoload.php. Ошибка «autoload.php не найден» обычно означает неверный каталог или пропущенную установку зависимостей.

Встроенный PHP-сервер предназначен для разработки. Публикация через него на внешний адрес не является готовой схемой production-размещения. Для VPS ниже используется Apache внутри контейнера и Caddy перед ним; для существующего хостинга можно перенести класс Lab в своё приложение и оставить привычную связку PHP-FPM с веб-сервером.

Источник: назначение встроенного PHP-сервера.

Linux: локальный запуск и запрос из консоли

Команды docker ниже предполагают, что текущему пользователю уже разрешён доступ к Docker daemon. На свежей Ubuntu, где такого доступа ещё нет, выполняйте их через sudo: например, sudo docker compose up -d –build –wait. Не меняйте права сокета Docker на общедоступные.

На Linux схема проекта остаётся той же. Нужны Docker Engine и Compose plugin, установленные по официальной инструкции для дистрибутива. В Ubuntu это обычно удобнее делать через официальный репозиторий Docker, где движок и plugin обновляются согласованно. Архив не содержит Windows-зависимостей: PHP-код, Composer lock и статические ресурсы одинаковы. Из различий команды проверки HTTP обычно используют python3 вместо python.

unzip carbon-project.zip -d carbon-demo
cd carbon-demo/carbon-project
docker compose up -d --build --wait
curl -fsS http://127.0.0.1:8112/health.php
docker compose exec -T web php tests/run.php
python3 tests/check_http.py
curl -fsS http://127.0.0.1:8112/api.php \
  -H 'Content-Type: application/json' \
  --data '{"scenario":"dst","date":"2026-10-25","zone":"Europe/Berlin","locale":"ru","amount":3}'

Ответ покажет 25 реальных часов между местными полуночами. Такой запрос полезен при диагностике: если API корректен из консоли, можно отдельно проверить форму и сеть браузера. Если API возвращает 422, прочитайте error — это обычно формат даты, тип amount или пояс вне списка. Если соединение отклонено, проверяйте состояние контейнера и адрес порта, а не правила календаря.

Без Docker используются те же composer install и php -S с loopback-адресом. Для серверной публикации этот режим не нужен. Локальная проверка в Linux-контейнере подтверждает работу конкретного окружения, но не заменяет проверку вашего firewall, DNS, каталога размещения и существующего reverse proxy на реальном сервере.

Источник: установка Docker Engine и Compose на Ubuntu.

Linux VPS: домен, закрытый доступ и HTTPS

В архиве есть отдельный deploy/compose.server.yaml. Он не объединяется с локальным Compose: веб-приложение доступно только внутри Docker-сети, наружу выходят порты Caddy 80 и 443. Весь стенд защищён Basic Auth, включая API и health. Для небольшого внутреннего учебного инструмента это понятная отправная точка. Если нужны роли, аудит действий и доступ к данным клиентов, используйте полноценную авторизацию вашего сервиса.

sudo mkdir -p /opt/carbon/releases/v1
sudo chown "$USER":"$USER" /opt/carbon/releases/v1
unzip carbon-project.zip -d /opt/carbon/releases/v1
cd /opt/carbon/releases/v1/carbon-project
cp deploy/server.env.example deploy/server.env
chmod 600 deploy/server.env
docker run --rm -it caddy:2.11.9-alpine caddy hash-password
nano deploy/server.env

Укажите свой домен, имя пользователя и полученный bcrypt-хеш в одинарных кавычках. Сам пароль не записывайте в командную строку или репозиторий. Настройте DNS A на VPS; AAAA добавляйте только при работающем IPv6. Проверьте доступность 80/443 и отсутствие другого сервиса на этих портах. Если сервер уже обслуживает сайты, добавьте маршрут в существующий прокси, а не запускайте второй поверх него.

docker compose --env-file deploy/server.env -f deploy/compose.server.yaml config --quiet
docker compose --env-file deploy/server.env -f deploy/compose.server.yaml build web
docker compose --env-file deploy/server.env -f deploy/compose.server.yaml up -d --wait

После запуска запрос без credentials должен получить 401, а curl –user reader https://ваш-домен/health.php после ввода пароля — ok. Caddy получает публичный сертификат при корректном DNS и сетевой доступности. Детальный путь и команды диагностики находятся в deploy/README.md. Публичный VPS и ACME в рамках подготовки статьи не разворачивались; локальные тесты не выдаются за проверку конкретного домена читателя.

Источник: Caddy: Basic Auth и хеш пароля.

Обновление, откат, журналы и резервная копия

Храните релизы отдельно, например releases/v1 и releases/v2, а образы помечайте соответствующими тегами. В server.env есть APP_RELEASE, IMAGE_PREFIX и DEPLOY_PROJECT. При обновлении меняется тег релиза, но имя проекта остаётся прежним: Compose переключает приложение и продолжает использовать тома Caddy. Сначала собирайте новый образ и запускайте тесты, затем меняйте работающий контейнер. Старый образ сохраняйте на период возможного отката.

docker compose --env-file deploy/server.env -f deploy/compose.server.yaml run --rm --no-deps web php tests/run.php
docker compose --env-file deploy/server.env -f deploy/compose.server.yaml logs --tail 80 caddy web
docker compose --env-file deploy/server.env -f deploy/compose.server.yaml ps

В калькуляторе нет пользовательской базы: восстановление приложения состоит из исходного архива, lock-файла и настроек. Отдельно сохраняйте защищённый server.env и тома Caddy с сертификатами. Эти тома содержат ключевой материал, поэтому резервная копия не должна попадать в public или открытый Git. Для согласованного снимка используйте процесс резервирования хостинга и проверяйте восстановление на изолированном стенде.

Журналы контейнеров ограничены тремя файлами по 10 MB на сервис. Контролируйте HTTP500, health, свободное место и срок сертификата. Правила часовых поясов могут меняться: обновление базового образа тоже требует проверки важных дат. Для отката вернитесь к каталогу предыдущего релиза с его настройками и выполните up -d –wait; не удаляйте тома и старые образы до завершения проверки.

Как перенести расчёт в CRM, подписку или отчёт

Выделите функцию, которая получает данные и возвращает результат без побочных действий. Например, calculateReplyDeadline принимает время создания, календарь команды и правило SLA; formatAppointment принимает момент, зону и язык клиента; reportBounds возвращает две UTC-границы. Когда эти обязанности смешаны с отправкой письма и записью в базу, трудно понять, почему пользователь получил неправильную дату и какой шаг нужно повторить.

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

При отправке уведомлений пригодится Symfony Mailer, а журналы ошибок можно связать с уже используемым логгером. Если операция должна выполниться позже, добавьте планировщик или очередь: вычисленный expiresAt не запускает задачу сам. Идентификатор действия и проверка повторного исполнения нужны независимо от того, насколько точно была рассчитана дата.

Проверки, которые стоит оставить в собственном проекте

Тестируйте бизнес-правило через наблюдаемый результат. Для подписки это следующая дата и сохранение исходного якоря; для отчёта — включение записи на начале и исключение на конце; для встречи — одинаковый timestamp после перевода пояса. Такие проверки полезнее теста, который просто повторяет вызов того же метода. Добавляйте реальные пограничные случаи своего сервиса, а не только сегодняшний день.

php tests/run.php
python3 tests/check_http.py
npm ci --ignore-scripts
npm test

В проекте проверены невозможные даты, високосный год, изменяемость объекта, переполнение месяца, пятница и следующий будний день, DST на 23/25 часов, дробная знаковая разница, исключённая граница периода и очистка тестовых часов. Все 720 сохранённых ответов сравниваются с повторным выполнением настоящего Carbon. Десять генерируемых PHP-примеров запускаются отдельно. HTTP-проверки проходят через реальный API и проверяют корректные ответы вместе с кодами ошибок.

DOM-тесты проверяют обработчики мастерской и запрет скачивания устаревшего результата. Они дополняют визуальную проверку, а не подтверждают удобство на любом телефоне. После обновления Carbon повторите свой набор дат и просмотрите migration notes: изменение знака или типа разницы может не вызвать исключение, но изменить поведение продукта. Приложение проверено в Windows PHP и Linux-контейнере; внешнее серверное окружение проверяйте после собственного развёртывания.

Исходники и дальнейшее изучение

Скачиваемый проект можно использовать как небольшой учебный калькулятор или разобрать на отдельные функции для своего приложения. Начните с одного сценария, который уже нужен пользователю: срок заявки, локальное время встречи или границы отчёта. Затем зафиксируйте правило текстом, перенесите код и сохраните тест, который объясняет сложный случай. Такой путь даёт практическую пользу без необходимости сначала изучать весь справочник Carbon.

Открыть мастерскую на весь экран ↗Скачать PHP-проект и исходники ↓

Архив содержит PHP-класс, CLI, HTTP API, Bootstrap-интерфейс, сохранённые ответы для автономной мастерской, Composer/npm lock, Docker, тесты и инструкции Windows/Linux/VPS. Собственный код распространяется под MIT; лицензии сторонних компонентов приложены отдельно. Перед использованием в рабочем сервисе замените учебные ограничения, правила и фиксированное время на согласованные условия вашего приложения, сохранив ясный контракт даты и момента.

Источник: официальный сайт Carbon.

Источник: справочник методов.

Источник: проверенная версия 3.15.1.