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

Clock Laravel Package

tempest/clock

A lightweight clock abstraction for the Tempest framework, offering a testable interface for reading the current time. Swap in a frozen or mock clock during tests so time-dependent code stays deterministic instead of relying on native PHP date functions.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require tempest/clock

What you actually get is a one-job package: a Clock abstraction over "what time is it right now", plus an implementation backed by the system clock. Everything else — a test clock, a sleep helper — is a thin layer on top. The value isn't features; it's that now() stops being an ambient global and becomes a dependency you can control.

Look here first, in this order:

  1. The interface (src/Clock.php or equivalent). This is the entire contract — expect two or three methods (now(), maybe sleep()). Reading it takes thirty seconds and tells you 90% of what the package does.
  2. The concrete implementation(s). Specifically: how now() constructs the value, and which timezone it uses.
  3. The tests. For a package this small, the test suite is the documentation. It will show you the intended freeze/advance semantics, which the README almost certainly doesn't spell out.

Your first use case is mechanical: find every new DateTimeImmutable(), date(), time(), or Carbon::now() inside a service, and replace it with an injected clock.

final readonly class TokenService
{
    public function __construct(private Clock $clock) {}

    public function issue(): Token
    {
        $issuedAt = $this->clock->now();

        return new Token($issuedAt, $issuedAt->modify('+15 minutes'));
    }
}

That's the whole migration. If you do nothing else with this package, this is the 80% win: expiry, TTL, created_at, last_seen, and rate-limit windows all become assertable and deterministic without touching the system clock.

Implementation Patterns

Inject at the edge, pass DateTimeImmutable through the core. Let controllers, console commands, and queue jobs take the Clock, read now() once, and hand the resulting DateTimeImmutable down to domain code. Your domain services then become pure functions of an explicit timestamp — trivially testable and reusable.

Freeze time in tests at the arrange step. This is the pattern you'll write every day:

$clock = new FrozenClock(new DateTimeImmutable('2025-01-15 09:00:00 UTC'));
$service = new TokenService($clock);

$token = $service->issue();

// assertions against a known instant
$this->assertSame('09:15', $token->expiresAt->format('H:i'));

Advance instead of sleep. Backoff loops, retry logic, and polling workers should call the clock's sleep() rather than sleep()/usleep() directly. In tests a frozen clock should advance virtual time and return instantly; in production it delegates to usleep(). That single substitution turns a test suite that takes minutes into one that takes milliseconds.

public function retry(callable $operation): mixed
{
    $attempt = 0;

    while (true) {
        try {
            return $operation();
        } catch (TransientFailure $e) {
            if (++$attempt >= self::MAX_ATTEMPTS) {
                throw $e;
            }
            $this->clock->sleep(2 ** $attempt);
        }
    }
}

Bind the interface in your container. Register Clock → the system implementation in your normal bootstrapping, and swap it for a frozen one in your test bootstrap. Tempest's container autowires constructor arguments, so in a Tempest app this is usually free. Outside Tempest, a two-line factory or your existing PSR-11 binding is enough.

Model expiry as pure functions over DateTimeImmutable. isExpired(DateTimeImmutable $now, DateTimeImmutable $expiresAt): bool is easier to reason about than isExpired(Clock $clock) and composes better with historical/future data (replaying events, simulating "what if it's next month").

Use separate clocks when you have separate notions of time. A Clock skewed to a business timezone, or a clock pinned to the database server's time for multi-node consistency, is a legitimate custom implementation. The interface is the extension point.

Keep Clock out of entities and value objects. Persisting or serializing a clock into a queued job or cache payload is a mistake — resolve it again on the other side.

Gotchas and Tips

Treat it as alpha. Zero stars, zero dependents, a zero score. This is early-stage infrastructure. Pin the exact version in composer.json, read the source before depending on behavior, and accept that you may be the one fixing it. The upside: the surface area is tiny, so forking into app/Support/Clock.php is a genuinely cheap escape hatch if the package stalls. Don't let a 200-line dependency become a blocker.

Check whether it implements Psr\Clock\ClockInterface. If it does, you get interop with the wider PHP ecosystem for free and can accept either type in third-party libraries. If it doesn't, and you're not otherwise committed to Tempest, weigh the PSR interface — one method, real adoption, no vendor risk.

Timezones: verify, don't assume. Determine whether now() respects date_default_timezone_get(), forces UTC, or is configurable. Then normalize: store UTC everywhere, convert to the user's zone only at the rendering boundary. Mixing an ambient-timezone clock with UTC-stored timestamps is the classic source of "off by an hour" bugs that only appear for some users.

Wall clock ≠ monotonic clock. now() can move backwards — NTP correction, DST transitions, a sysadmin with root. Never measure elapsed duration with it; use hrtime() for benchmarks and timeouts. Use Clock for "when did this happen", not "how long did it take".

Verify sleep() under a frozen clock before you trust it. If the frozen implementation actually blocks rather than advancing virtual time, your tests will silently get slow and flaky. This is the most common failure mode in clock packages and it isn't always documented. Write one test that asserts sleep() returns immediately.

modify('+1 day') is not 24 hours. Across a DST boundary it's 23 or 25. If you're doing calendar math (days, weeks, "same time tomorrow"), use modify() or DateInterval; if you're doing durations (TTL, timeouts), do arithmetic in seconds. Mixing the two produces bugs that surface twice a year.

DateTimeImmutable::modify() returns a new object. Writing $now->modify('+1 hour'); as a bare statement is a no-op. Easy to do while refactoring from the mutable DateTime API.

Flaky time test? Grep for ambient time first. When a time-dependent test fails intermittently, the cause is almost always a stray time(), date(, or new DateTime( somewhere in the call path that bypassed the injected clock. Grep the code under test before you start suspecting the package.

Don't wrap it in your own TimeProvider interface "just in case". The package interface is the seam. A second abstraction is a second thing to bind, fake, and explain to new hires, and it buys nothing.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core