Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Chronos Laravel Package

cakephp/chronos

Chronos is an immutable date and datetime library for PHP. It offers Chronos and ChronosDate objects that prevent accidental modification, keeping data predictable, and adds handy conveniences beyond native DateTime. Install via Composer with cakephp/chronos.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pick your version deliberately

composer require cakephp/chronos

Chronos 3.x is the current line and is a different beast from 2.x: Chronos no longer extends DateTimeImmutable, no longer implements DateTimeInterface, and the mutable classes and Carbon compatibility shims are gone. Check the version map on the wiki before pinning, especially if you are stuck on an older PHP. If you inherit a 2.x codebase, budget an upgrade pass (see Gotchas) before writing new code.

Where to look first

  1. README.md — installation, immutability, ChronosDate, ChronosTime, setTestNow(), ClockFactory. It is short and covers 90% of daily use.
  2. book.cakephp.org/chronos/3/ — the narrative docs, including the 2.4 → 3.0 upgrade guide.
  3. api.cakephp.org/chronos — authoritative method list. Do not assume a Carbon method exists just because it existed in 2015; the shims were removed.

Your first real use case

Anywhere you currently mutate DateTime in service or domain code — billing periods, trial expiry, reminders, "business hours" logic. Start by replacing the mutable flow with an immutable one:

use Cake\Chronos\Chronos;

$trialStart = Chronos::parse('2024-03-01 09:00:00', 'UTC');

// Wrong: result is discarded
$trialStart->modify('+14 days');

// Right: reassign
$trialEnd = $trialStart->addDays(14);

Choose the class before you write the method

Need Use
An instant in time (timestamps, expiries, ordering) Chronos
A calendar day with no meaningful time ChronosDate
A time-of-day with no date (opening hours, schedules) ChronosTime
use Cake\Chronos\ChronosDate;
use Cake\Chronos\ChronosTime;

$birthday = new ChronosDate('1990-04-12');
echo $birthday;                       // 1990-04-12
echo $birthday->modify('+3 hours');   // still 1990-04-12 (time is frozen)

$opening = ChronosTime::create(9, 0, 0);
$closing = $opening->addHours(8)->addMinutes(30);
echo $closing->format('g:i A');       // 5:30 PM

In a Laravel app, treat Chronos as a boundary library

Laravel's now(), Eloquent date casts, and validation rules speak Carbon. Chronos will not satisfy a DateTimeInterface type hint in 3.x. Use Chronos inside your domain/service layer and convert at the edges rather than trying to replace the framework's date stack.

Implementation Patterns

Wrap Chronos in a domain type instead of passing it around

final class BillingPeriod
{
    public function __construct(
        public readonly Chronos $start,
        public readonly Chronos $end,
    ) {
        if ($end->lessThanOrEqualTo($start)) {
            throw new InvalidArgumentException('End must follow start.');
        }
    }

    public function days(): int
    {
        return (int) $this->start->diffInDays($this->end);
    }
}

This keeps Chronos out of your public API, which matters because its type relationship with DateTimeInterface changed in 3.0 and may change again.

Persist UTC, present local

// Write
$row->starts_at = $event->start->setTimezone('UTC')->format('Y-m-d H:i:s');

// Read (explicit input timezone, never rely on the server default)
$start = Chronos::createFromFormat('Y-m-d H:i:s', $row->starts_at, 'UTC');
$local = $start->setTimezone($user->timezone);

Note the two timezone methods and use them for what they mean:

  • setTimezone($tz) — same instant, different representation.
  • shiftTimezone($tz) — same wall-clock numbers, new zone. Use this when a stored "2024-03-01 09:00" is really a local schedule, not an instant.

Recurrence and schedules with ChronosTime

$standup = ChronosTime::create(9, 30, 0);
$next = $standup->nextOccurrenceOf(Chronos::now('UTC')); // next 09:30
$prev = $standup->previousOccurrenceOf(Chronos::now('UTC'));

$window = [$standup->startOfHour(), $standup->endOfHour()];

ChronosTime also carries the usual comparison helpers (isBefore, isAfter, etc.), so sorting a list of business hours does not require inventing a dummy date.

Wall-clock vs elapsed arithmetic — pick the right one

$start = Chronos::parse('2024-03-31 01:30:00', 'Europe/Berlin');

$wall  = $start->addHours(2);         // may cross a DST jump; wall-clock semantics
$exact = $start->addElapsedHours(2);  // Unix-timestamp arithmetic, DST-safe

Rule of thumb:

  • Durations that must equal a diffInHours() result (timeouts, TTLs, SLA clocks, "N hours from now") → addElapsedHours() / addElapsedMinutes() / addElapsedSeconds().
  • Human calendar intent ("meeting at 9am tomorrow", "+1 month renewal") → addHours() / addDays() / addMonths().

Calendar ranges using ChronosDate

$month = new ChronosDate('2024-03-01');
$first = $month->startOfMonth();
$last  = $month->endOfMonth();

foreach ($first->to($last) as $day) {   // DatePeriod-style iteration
    // $day is a ChronosDate; time components never leak in
}

Because ChronosDate freezes time at 00:00:00, comparisons and array keys behave predictably — no "off by 23 hours" bugs from a stray now().

Inject a clock, don't call Chronos::now() in constructors

use Cake\Chronos\ClockFactory;
use Psr\Clock\ClockInterface;

$clock = new ClockFactory('UTC');

final class TrialService
{
    public function __construct(private ClockInterface $clock) {}

    public function start(): Chronos
    {
        return $this->clock->now();
    }
}

This is the cleanest test seam and works with any PSR-20 container binding. Reserve the static setTestNow() for legacy code and quick tests.

Scoped time freezing in tests

Chronos::withTestNow('2024-01-15 10:00:00', function () {
    $this->assertSame('2024-01-15', (string) new ChronosDate());
});
// clock restored automatically, even on exception

Prefer withTestNow() over setTestNow()/setTestNow(null) pairs: the callback form cannot leak into the next test if an assertion throws.

Integration tips for Laravel

  • Convert at the boundary: $model->starts_at = $chronos->toNative(); (3.x exposes a native DateTimeImmutable conversion; if your pinned version lacks it, use new DateTimeImmutable($chronos->format('Y-m-d H:i:s.u'), $chronos->getTimezone())).
  • Convert inward similarly — accept DateTimeInterface in your adapter and construct Chronos from it, rather than type-hinting Chronos on controllers or models.
  • Keep one "date adapter" class per app that owns parsing, timezone defaults, and DB formatting. Every createFromFormat call in scattered services is a future timezone bug.
  • Don't mix Carbon::setTestNow() and Chronos::setTestNow() and expect one to affect the other — they are independent static clocks. A feature test that freezes Carbon will still see real time in Chronos code.
  • For API output, format explicitly (->toIso8601String() or ->format('c')) instead of relying on implicit JSON serialization, so your contract survives version bumps.

Human-readable output and comparison

$invoice->due->diffForHumans();          // "in 3 days"
$invoice->due->isPast();
$invoice->due->between($start, $end);    // inclusive by default

Recent 3.x versions return floats from diffIn*, so cast deliberately when you need integer day counts for storage or display.

Gotchas and Tips

Immutability silently eats modifications. The single most common bug is $date->modify('+1 day'); with no assignment. Static analysis will not catch this. Code review rule: any Chronos call whose return value is unused is a bug. Same for setTimezone, addDays, startOfDay, and the setters.

ChronosDate ignores time modifications by design. modify('+3 hours'), addHours(5), and setTime() produce the same calendar day. If you find yourself wanting the time back, you picked the wrong class — use Chronos.

ChronosDate uses the server default timezone. It is not "timezone-free"; the zone is whatever PHP defaults to. In long-running workers or queue consumers, set date_default_timezone_set() in bootstrap or explicitly pass zones when converting to instants. A calendar date converted to an instant with an implicit zone is a classic off-by-one-day bug around midnight.

No DateTimeInterface in 3.x. Type hints against DateTimeInterface, DateTimeImmutable, or Carbon will reject a Chronos. This bites in Laravel date casts, Symfony validators, and third-party SDKs. Convert with toNative() at the edge, or write your own narrow interface and adapt. Do not add Chronos to an Eloquent $casts array expecting it to work.

ChronosDate and Chronos are not interchangeable types in 3.x, deliberately. A function accepting Chronos should not accept a ChronosDate, and vice versa. If you need a shared abstraction, define one yourself rather than reaching for the package's interface.

Don't trust createFromFormat() plus frozen time. Missing components in the format are filled from "now", which means tests can drift with the wall clock. Release 3.3.2 fixed setTestNow being ignored for missing components, and 3.3.3 reverted part of that for the 'U' timestamp format to avoid a BC break. Practical guidance: always supply every component you care about, or pass an explicit base time; avoid depending on the filler behavior, and never mix 'U' with a frozen clock.

Validate parsing failures explicitly. Wrap createFromFormat() and parse() in try/catch — success/failure signaling has changed across PHP and Chronos versions, and user-supplied input will hit it.

diffIn* vs add* must be paired correctly. diffInHours() measures elapsed time; addHours() moves the wall clock. Across a DST transition they disagree by an hour. If you cache a TTL as addHours() and later verify with diffInHours(), your expiry is wrong twice a year. Use addElapsedHours() when the diff must be the inverse.

Zero-interval DatePeriod loops. Fixed in 3.3.2 — an interval of PT0S used to spin forever. If you are on an older 3.x, guard interval construction (new DateInterval('PT1H') minimum) before iterating.

Fractional seconds in ChronosTime. Parsing of fractional seconds was corrected in 3.5.1; older versions could treat .5 incorrectly. If you store sub-second precision in time-only columns, pin >= 3.5.1 or normalize on input.

2.x → 3.x is not a drop-in. MutableDate/MutableDateTime are removed; Date became ChronosDate; Chronos stopped extending DateTimeImmutable; Carbon compatibility methods were dropped. Run the upgrade guide, enable deprecation logging on 2.5 first, and grep for Mutable, Date::, createFromTimestampUTC, and any DateTimeInterface hints touching Chronos.

Deprecated timestamp constructors. createFromTimestampUTC() was deprecated in the 2.x line; use createFromTimestamp() (and note the PHP 8.4 signature change in 2.5.1 for float timestamps). Keep this consistent across your adapter.

Prefer withTestNow() and DI over static state. setTestNow() is global and survives across tests within the same process if you forget to reset it — a common cause of "passes alone, fails in suite." The callback form is exception-safe. ClockFactory is the better long-term answer for production code.

Comparison via == on objects is unreliable. Two Chronos instances representing the same instant may not be identical. Use equals(), lessThan(), greaterThanOrEqualTo(), or compare formatted strings. Equality also depends on timezone representation, so normalize to UTC before asserting in tests.

Extension points worth knowing. Chronos::setToStringFormat() changes what (string) $date yields application-wide — useful for consistent logging, dangerous if libraries rely on ISO output. Chronos::now($tz) accepts a timezone (string or DateTimeZone), so avoid relying on the global default in services. toArray() (added in 3.4.0) gives you structured components for debugging and API envelopes.

Debugging tip. When a date looks wrong, print all of these at once: format('c'), getTimezone()->getName(), and getTimestamp(). Almost every Chronos bug is one of three things — an abandoned return value, a setTimezone/shiftTimezone mix-up, or a static clock that wasn't reset.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager