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

symfony/clock

Decouples your PHP applications from the system clock by injecting a ClockInterface, so you can get the current time, force a timezone, or pause execution with sleep, and swap in a mock clock for testing.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (usually already present). Recent Laravel releases pull symfony/clock in transitively — check with composer show symfony/clock before adding it explicitly. If it is missing:

composer require symfony/clock

Read these five files first (they are small, ~50–150 lines each):

File Why
Symfony\Component\Clock\ClockInterface The whole contract: now(), sleep(), withTimeZone()
Symfony\Component\Clock\NativeClock Production implementation
Symfony\Component\Clock\MockClock Test implementation (modify(), setTo())
Symfony\Component\Clock\DatePoint DateTimeImmutable subclass with createFromTimestamp(), setMicroseconds()
Symfony\Component\Clock\Clock Static facade (Clock::get() / Clock::set()) used by the test trait

Also bookmark https://symfony.com/doc/current/components/clock.html — the README is a teaser, the docs page is the real reference.

Minimal wiring in Laravel. Register a default binding once, then inject everywhere:

// app/Providers/AppServiceProvider.php
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\NativeClock;

public function register(): void
{
    $this->app->bind(ClockInterface::class, NativeClock::class);
}
// app/Actions/StartTrial.php
final class StartTrial
{
    public function __construct(private ClockInterface $clock) {}

    public function handle(User $user): void
    {
        $user->trial_ends_at = $this->clock->now()->modify('+30 days');
        $user->save();
    }
}

First use case to ship: the "disable after N days" / trial-expiry check. It is the highest-value, lowest-risk migration — one class, one test that runs in milliseconds instead of waiting 30 days.

public function test_trial_expires_after_thirty_days(): void
{
    $clock = new MockClock('2025-01-01 00:00:00');
    $this->app->instance(ClockInterface::class, $clock);

    $user = User::factory()->create();
    $this->post("/trial/{$user->id}/start");

    $clock->sleep(60 * 60 * 24 * 30 + 1); // time travel, zero real delay

    $this->assertTrue($user->fresh()->trialExpired($clock));
}

Implementation Patterns

1. Bind ClockInterface, never a concrete class

Always type-hint ClockInterface. That is the single decision that makes everything else (MockClock, UTC-pinned clock, decorators) swappable in one line of container config.

// production: UTC-pinned clock for internal services
$this->app->bind(ClockInterface::class, fn () => (new NativeClock())->withTimeZone('UTC'));

Note withTimeZone() returns a new instance — it is immutable, so it must be chained/assigned, never called for side effects.

2. Per-test override with instance()

The cheapest, most explicit testing pattern. No trait magic, no globals:

protected function setUp(): void
{
    parent::setUp();
    $this->clock = new MockClock('2025-06-15 12:00:00');
    $this->app->instance(ClockInterface::class, $this->clock);
}

Because MockClock::sleep() advances the mocked time rather than blocking, tests that simulate hours/days of drift run in microseconds.

3. Route the global Clock facade through the container (best of both worlds)

ClockSensitiveTrait (from Symfony\Component\Clock\Test) manipulates the static Clock::set() — it will not touch a NativeClock instance already bound in your container. Bind through the facade instead of to a concrete class:

// AppServiceProvider
$this->app->bind(ClockInterface::class, fn () => Clock::get());

Then in a test:

use Symfony\Component\Clock\Test\ClockSensitiveTrait;

class ExpiryTest extends TestCase
{
    use ClockSensitiveTrait;

    public function test_grace_period(): void
    {
        $clock = static::mockTime('2025-01-01 00:00:00');
        // container-resolved ClockInterface is now the same mock
        $clock->sleep(3600);
    }
}

Use bind (resolved on every resolution), not singleton — a singleton caches the first Clock::get() result and your later mockTime() will be ignored.

4. Replace sleep() / usleep() inside jobs and workers

// before
usleep(500_000);

// after
$this->clock->sleep(0.5);

This makes backoff/retry logic testable and gives you sub-second precision without sleep() integer truncation. It also makes it obvious when a job is intentionally delaying.

5. Custom clock as a decorator (observability extension point)

ClockInterface is a three-method interface — wrapping it is trivial and is the sanctioned way to add logging, metrics, or a "fixed offset" clock for staging:

final class LoggingClock implements ClockInterface
{
    public function __construct(
        private ClockInterface $inner,
        private LoggerInterface $logger,
    ) {}

    public function now(): DateTimeImmutable
    {
        $now = $this->inner->now();
        $this->logger->debug('clock.now', ['at' => $now->format(DATE_ATOM)]);
        return $now;
    }

    public function sleep(float|int $seconds): void { $this->inner->sleep($seconds); }
    public function withTimeZone(string|\DateTimeZone $tz): static { /* ... */ }
}

6. Use DatePoint for value objects

When you need timestamp construction from an int (event sourcing, replaying logs), DatePoint::createFromTimestamp($ts) avoids (new DateTimeImmutable())->setTimestamp() gymnastics, and setMicroseconds() is available where the polyfill is not.

If you build DatePoint from a relative string (new DatePoint('tomorrow', $tz)), be aware that earlier 8.1.x patches kept the current time-of-day instead of resetting it; that was fixed in 8.1.9 (see Gotchas). Prefer passing an explicit date/time, or a reference plus a relative part, when the time-of-day matters.

7. Leave Eloquent's created_at/updated_at alone

Do not override freshTimestamp() or getDateFormat() to route through the clock. Laravel's timestamps are a persistence concern; the clock is a domain concern. Inject ClockInterface into the action/service and set domain columns (trial_ends_at, locked_until, next_retry_at) explicitly.

Gotchas and Tips

now() returns DateTimeImmutable, not Carbon. This is the #1 friction point in a Laravel codebase. CarbonImmutable and DateTimeImmutable don't satisfy each other's type hints even though == comparisons work. Either standardise on DateTimeImmutable/DatePoint for domain code, or wrap at the boundary: CarbonImmutable::instance($clock->now()). Never mix hints within one class.

MockClock does not affect Carbon::now(), now(), or date(). Laravel's now() helper reads the real system clock. If your test freezes the injected clock but asserts on $model->created_at, the assertion uses real time. Keep the two worlds separate, or call Carbon::setTestNow($clock->now()) at the top of the test in addition if you truly need both aligned.

ClockSensitiveTrait::mockTime() returns the mock. The return type changed to ClockInterface in 7.1 — don't type-hint MockClock if you support a range of versions; call modify()/setTo() off the interface only if you actually assert on the concrete class, or keep a property and assert with instanceof.

MockClock::sleep() is time travel, not a pause. A job that calls $clock->sleep(5) to back off from a rate-limited API will not slow down in tests — the mock just jumps forward 5 seconds. That is usually the desired behaviour, but it means you cannot test "this request takes at least 5s" via the mock.

Negative durations. MockClock::sleep() behaviour for negative values was aligned with NativeClock in 7.3.8 / 6.4.30 / 8.0.0-RC1 — don't rely on it to rewind. Use $mock->modify('-1 hour') or $mock->setTo(...) to move backwards; those are explicit and stable.

Pin >= 8.1.9 if you use DatePoint or pre-1970 mock dates. v8.1.9 is a pure bug-fix release (no API change, nothing deprecated), but the four fixes are exactly the kind that produce silently wrong timestamps rather than exceptions:

  • new DatePoint() was one hour off during the DST fall-back hour, and so was DatePoint constructed from a reference that itself fell in the fall-back hour. If your test suite runs with Europe/London, America/New_York or any DST zone and freezes time inside the repeated hour, results flip depending on the patch version.
  • Relative parts on DatePoint no longer keep the current time of day.
  • MockClock::sleep() with dates before 1970 was broken (negative epoch arithmetic).

Since these are wrong-value bugs, not crashes, bump the constraint (composer require symfony/clock:^8.1.9 or composer update symfony/clock) rather than assuming an old lockfile is fine. Verify with composer show symfony/clock — a transitive Laravel constraint can otherwise pin you below the fix.

withTimeZone() mutates nothing. $this->clock->withTimeZone('UTC') as a bare statement is a no-op. Always $this->clock = $this->clock->withTimeZone('UTC'); or chain at bind time. And remember it changes the timezone of the returned value, not the instant.

Never cache now() in a singleton. Long-running queue workers and Octane workers will hold a stale timestamp forever. Call $clock->now() at the point of use; NativeClock reads the system clock on every invocation and is effectively free.

Don't use Clock::get() in domain/application code. It is a service locator. It exists for legacy code and for the test trait. Constructor injection remains the thing that makes the abstraction worth having.

Boundary assertions are brittle. Asserting assertEquals($expected, $clock->now()->format('Y-m-d')) after a sleep() that crosses midnight will fail in surprising ways. Prefer assertions on relative state (trial_ends_at > now) or freeze at a comfortable time like 12:00:00 far from a date boundary.

Timezone in tests. Pass an explicit timezone to MockClock when assertions depend on offsets, and pin production clocks to UTC at the container level while rendering user-facing dates in the user's timezone downstream. Store UTC, display local, and the clock keeps that split honest.

sleep() precision on NativeClock. Sub-second floats work (it degrades to usleep()), but don't expect nanosecond guarantees — this is not a monotonic high-resolution timer. For WebSocket heartbeats or RT systems, this package is the wrong tool.

Check the Laravel–Symfony version matrix before pinning. Symfony 8 requires PHP ≥ 8.4; if you're on PHP 8.2/8.3 you'll be resolving a 7.x line (7.4.x is current LTS-ish). The API you actually use (now, sleep, withTimeZone, MockClock) has been stable across 6.3 → 8.x, so the risk is low, but composer why symfony/clock first saves a debugging session. Note that the 8.1.9 fixes are 8.1-branch fixes at time of writing — if you are pinned to 7.x/6.4, confirm the equivalent patch landed before relying on the corrected DatePoint DST behaviour.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator