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

lcobucci/clock

lcobucci/clock is a lightweight clock abstraction for PHP that decouples code from DateTimeImmutable. Use SystemClock for real time or FrozenClock for tests, making time-dependent logic easy to control and verify.

View on GitHub
Deep Wiki
Context7

Getting Started

lcobucci/clock is a PSR-20 implementation with a tiny surface area: one interface (Lcobucci\Clock\Clock, which extends Psr\Clock\ClockInterface) and two implementations — SystemClock (real time, constructed with a DateTimeZone) and FrozenClock (a fixed instant, for tests). It has no Laravel service provider, no config file, and no facades. You wire it yourself in about ten lines.

composer require lcobucci/clock

Where to look first: the README (~60 lines) is the whole user manual, and src/ is four small files. Read SystemClock and FrozenClock and you're done — everything else is discipline about where you call now().

First use case: replace every new DateTimeImmutable(), date(), time(), and Carbon::now() in domain/business code with an injected clock.

// app/Providers/AppServiceProvider.php
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use Psr\Clock\ClockInterface;

public function register(): void
{
    $this->app->singleton(Clock::class, fn () => SystemClock::fromUTC());
    $this->app->alias(Clock::class, ClockInterface::class);
}

Then in a service:

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

    public function isExpired(User $user): bool
    {
        return $user->trial_ends_at <= $this->clock->now();
    }
}

Laravel's container resolves the interface automatically because of the alias. Start with one high-value class — trial expiry, token validation, or a "publish at" rule — rather than converting the whole codebase at once.

Implementation Patterns

Type-hint Psr\Clock\ClockInterface, bind to the concrete Clock. In application code the two are interchangeable; in packages you share across projects, the PSR interface keeps you library-agnostic (and future-proof if you swap to symfony/clock). The alias above means both resolve to the same singleton instance.

Use named constructors instead of new DateTimeZone(...) in app code. SystemClock::fromUTC() and SystemClock::fromSystemTimezone() document intent and avoid drifting timezones across the codebase. Reserve the explicit new SystemClock(new DateTimeZone('America/Sao_Paulo')) for genuinely localized presentation logic.

Swap the binding in tests, not the call sites. The container instance() call replaces the singleton for the rest of the test, and any service already resolved (or resolved later) receives the frozen clock:

protected function setUp(): void
{
    parent::setUp();

    $this->clock = new FrozenClock(new DateTimeImmutable('2024-03-01 12:00:00', new DateTimeZone('UTC')));
    $this->app->instance(Clock::class, $this->clock);
}

public function test_trial_expires_after_fourteen_days(): void
{
    $user = User::factory()->create(['trial_ends_at' => $this->clock->now()->modify('+14 days')]);

    $this->clock->adjustTime(new DateInterval('P13D'));       // 3.3.0+
    $this->assertFalse($this->service->isExpired($user));

    $this->clock->adjustTime(new DateInterval('P2D'));
    $this->assertTrue($this->service->isExpired($user));
}

adjustTime() (added in 3.3.0) mutates the frozen instant in place, which means the same object held by already-built services moves forward too — you get time travel without rebuilding the container. This is the single biggest reason to prefer FrozenClock over mocking the interface.

Bridge to Laravel's Carbon test helpers if you keep both. If your team already writes $this->travel(1)->days() / Carbon::setTestNow(), bind a clock that reads it, so there is one source of truth:

$this->app->instance(Clock::class, new FrozenClock(
    Carbon::now()->toDateTimeImmutable()
));

Do this once in a base TestCase::setUp() rather than in individual tests.

Freeze "now" from an existing clock when 3.4+ is available. FrozenClock::fromSystemClock($systemClock) snapshots whatever the given clock reports — handy in staging smoke tests, or to freeze at the instant a container boots.

Share the clock with lcobucci/jwt. If you validate JWTs, Lcobucci\JWT\Validation\Constraint\ValidAt / SignedWith take a clock. Resolving the same container-bound clock means FrozenClock also freezes exp/nbf/iat checks — otherwise your test freezes time for your code but the token validator still uses real time and the test flakes near boundaries.

Custom clocks are a legitimate extension point. Need DB-authoritative time or an NTP source? Implement ClockInterface with a single now(): DateTimeImmutable method. It is one method; nothing else to satisfy.

Use half-open intervals in domain rules. $clock->now() < $expiresAt reads better than <= and avoids the "expires exactly now" ambiguity that makes boundary tests brittle. Freeze the clock exactly on the boundary in tests to pin the semantics.

Query binding just works. Connection::prepareBindings() converts DateTimeInterface to the grammar's date format, so ->where('expires_at', '>', $clock->now()) is safe — keep the clock and the DB session both in UTC so the string formatting round-trips correctly.

Gotchas and Tips

  • Two time sources is the classic failure. Mixing Carbon::now() (or now(), today(), Str::random-adjacent date helpers, Eloquent timestamp defaults) with an injected clock means Carbon::setTestNow() won't freeze your clock, and vice versa. Pick one and grep for the other. created_at/updated_at remain DB-side; that's fine, just don't compare them against a frozen clock and expect symmetry.
  • FrozenClock is mutable, SystemClock is readonly. Because you bound the clock as a singleton, a FrozenClock rebound in one test can leak into the next if the app instance is reused (e.g. some parallel/@depends setups). Prefer per-test instance() bindings, and call $this->app->forgetInstance(Clock::class) if you cache anything derived from it.
  • SystemClock::fromSystemTimezone() depends on date_default_timezone_get(), which Laravel sets from config('app.timezone') and which can differ between CLI, queue workers, and PHPUnit (check phpunit.xml <env>). If tests pass locally and fail in CI with hour-offset assertions, this is why. fromUTC() removes the variable entirely.
  • "Target [Psr\Clock\ClockInterface] is not instantiable." That's the message you get before you add the container binding — there is no auto-discovery. Add it in AppServiceProvider::register(), not boot().
  • Don't cache now() in a constructor. SystemClock::now() is live on every call, but private DateTimeImmutable $startedAt = $clock->now() in a singleton or long-running Octane/queue worker goes stale for the process lifetime. Call now() at the point of use, or inject the clock and pass instants explicitly.
  • Queued jobs are a separate process. A FrozenClock bound in a test doesn't travel into a dispatched job. Pass the relevant timestamp as a job property (or the job's own clock-independent inputs) if the job's behaviour is time-sensitive, otherwise it re-evaluates against real time on the worker.
  • now() returns DateTimeImmutable, not Carbon. You lose diffForHumans(), isWeekend(), mutators, and locale formatting. Wrap when you need them: CarbonImmutable::instance($clock->now()) — don't reach back for Carbon::now() inside the domain layer.
  • Version pinning by PHP, silently. 3.6.x requires PHP 8.4, 3.3–3.5 require ^8.2. composer require lcobucci/clock resolves to whatever your php constraint allows, so two projects can be on different majors-looking minors with the same composer.json line. If you depend on adjustTime() (3.3+) or FrozenClock::fromSystemClock() (3.4+), require ^3.4 explicitly.
  • Mocking the interface is an anti-pattern here. ->method('now')->willReturnOnConsecutiveCalls(...) produces tests that encode call order and break on harmless refactors. FrozenClock + adjustTime() gives the same control with one instance.
  • Don't measure durations with a clock. Wall-clock jumps (NTP, DST) make elapsed-time assertions lie; use hrtime() / microtime() for performance assertions. Clocks are for stating what time it is, not for timing.
  • Debugging tip: when a time assertion is confusing, dump(get_class($clock), $clock->now()) at the top of the test. Nine times out of ten you'll find the real SystemClock was resolved, or the frozen instant is in a different timezone than the value you're comparing against.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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