beste/clock
A collection of PSR-20 clock implementations for PHP, including SystemClock, LocalizedClock, UTCClock, FrozenClock for tests, MinuteClock, and WrappingClock. Freeze time, set time zones, or wrap any object with a now() method. Install via composer require beste/clock.
Architecture fit. beste/clock is a thin, framework-agnostic set of implementations of Psr\Clock\ClockInterface (PSR-20) — a single method, now(): DateTimeInterface. That is the entire contract, which is exactly what makes it a good architectural seam: domain and application services can depend on an interface they own conceptually while the concrete source of time is bound at the container level. The package deliberately does not compete with Carbon; it sits underneath it, and every implementation returns DateTimeImmutable, so Carbon/Illuminate\Support\Carbon consumers can continue to operate on the returned values. For a Laravel codebase already committed to Carbon for formatting and date math, this is a complementary abstraction, not a replacement.
The substantive gap versus a "Laravel package" is that there is no service provider and no auto-discovery. That is a deliberate design choice consistent with PSR-20 (the spec is framework-neutral), but it means the integration work — binding ClockInterface to a concrete implementation, choosing per-environment behavior — is ours, and it is roughly one class and one provider registration.
Integration feasibility. High, with low effort. composer require beste/clock plus a binding in a dedicated provider is the whole of it. The available implementations map cleanly onto our needs: UTCClock as the production default (avoids ambient date.timezone drift), LocalizedClock where a tenant or regional timezone must be honored, FrozenClock for deterministic tests, MinuteClock for coarse-grained idempotency buckets, and WrappingClock to adapt a legacy object that already exposes now() without rewriting it. Notably, the README's implementations are all now()-only — there is no sleeping, advancing, or drift simulation built in, so FrozenClock::setTo() plus our own test helpers is the extent of the time-travel toolkit.
Technical risk. Moderate, concentrated in adoption and longevity rather than code quality:
stella-maris/clock → psr/clock → reverted → psr/clock again in 3.0). That history is now resolved and stabilized on the PSR, but it signals a maintainer who iterated publicly. Pin the constraint and review before major bumps.stella-maris/clock may produce a Composer-level disagreement with psr/clock. Cheap to check, but must be checked before we commit.Key questions for the team.
Carbon::setTestNow() cover the flakiness we currently experience? If the answer is "only in tests," the ROI case weakens considerably.FrozenClock versus Carbon::setTestNow() in the test suite — one, the other, or a documented hybrid?Stack fit. PHP ^8.0 per the 2.3.0 changelog note, with the PSR-20 contract stable across 3.x. That covers Laravel 9 through current. No Laravel version constraint to negotiate, no published config, no published migrations, no facade — nothing that couples us to a framework release cycle. In practice this is the lowest-friction category of dependency: an interface plus six small classes.
Migration path. Adoption is naturally incremental and should be sequenced that way:
stella-maris/clock), lock the constraint.ClockServiceProvider (or a block in AppServiceProvider) registering SystemClock/UTCClock as a singleton, with an environment-conditional switch to FrozenClock for the test environment.TestCase helper that freezes and resets the clock, plus a convention that feature tests set time explicitly rather than relying on wall-clock behavior. This is where the QA-cycle benefit is actually realized.ClockInterface. No retrofitting in the same PR.Carbon::now(), time(), and date() calls inside domain/application layers.Compatibility notes. Keep Eloquent's created_at/updated_at on the framework's own handling — overriding those through an injected clock is a well-known source of inconsistency and is not worth the purity. Where a value must be an actual DateTimeImmutable at a persistence boundary, convert explicitly rather than passing DateTimeInterface through. Be aware that ClockInterface::now() is declared to return DateTimeInterface while these implementations return DateTimeImmutable; type-hint the concrete return only if we control the call site, since a future alternative implementation is free to return a mutable instance.
Timezone sequencing. Make UTCClock the production default and require any LocalizedClock usage to be an explicit, justified decision at a display or regional-policy boundary. This prevents the most common failure mode in multi-region applications, where "now" means different things in different code paths depending on ambient PHP configuration.
Maintenance. Low, and the low-maintenance property is structural rather than a function of upstream activity: the interface is a spec we don't control and can't break, and the implementations are a few dozen lines each. Even in the worst case — upstream abandons the package — the replacement cost is trivial. The corollary is that we should not expect upstream to fix our problems; the practical posture is "adopt with a documented fork trigger," not "adopt and wait for support."
Support model. There is no Laravel-idiomatic documentation, no facade, no config, and few enough adopters that community answers will be scarce. Internal documentation is a first-class deliverable here, not an afterthought: how to bind, how to freeze in tests, when to use UTCClock versus LocalizedClock, and how to handle legacy now()-bearing objects via WrappingClock. Budget a short internal write-up and an onboarding note; the package itself is easy, the convention is what needs documenting.
Scaling. Negligible runtime cost — one object allocation per resolution if registered as a singleton, no I/O, no reflection at call time. The scaling concern is organizational: as teams and services multiply, inconsistent clock usage compounds. The mitigation is the container binding plus a lint rule, established early. MinuteClock deserves a note because it truncates rather than rounds, which makes boundary equality assertions subtly wrong; if we use it for idempotency buckets, that behavior must be covered by tests.
Failure modes to design against.
FrozenClock on a misconfigured APP_ENV would silently halt time for business logic. Make production the default branch and require an explicit opt-in for freezing.Carbon::setTestNow() and an injected FrozenClock can disagree, producing passes that don't reflect production. Pick one primary mechanism and document the exception cases.SystemClock resolves timezone from ambient PHP configuration, which can differ between web, queue worker, and CLI. Standardizing on UTCClock in application code eliminates an entire incident class.Carbon::now() yields tests that pass while production behavior diverges. This is the most likely failure mode and the one most worth a policy for.WrappingClock as a permanent shim. Useful for migration, but wrapping a legacy now()-bearing object indefinitely preserves the coupling we were trying to remove. Time-box it.Ramp-up. Genuinely low. One interface, one method, six implementations, all covered by copy-pasteable README examples. A mid-level engineer is productive within an hour. The real ramp is the team norm — knowing when to inject a clock, which implementation to choose, and how to freeze time in tests — which is a documentation and code-review problem, not a training problem.
Recommendation. Adopt for the specific purpose of making time-dependent business logic testable and timezone-explicit, with eyes open that we are adopting a low-adoption library and therefore own the integration, the documentation, and the contingency. Do not adopt it as a wholesale replacement for Carbon, and do not attempt a big-bang retrofit — the value accrues from consistent use in the narrow set of code paths that actually cause incidents, and the risk accrues from spreading it thinly across code that doesn't need it.
How can I help you explore Laravel packages today?