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.
Architecture fit
symfony/clock is a micro-abstraction, not a framework: one interface (ClockInterface with now(), sleep(), withTimeZone()) and a handful of implementations (NativeClock, MockClock, MonotonicClock). It plugs into Laravel's container cleanly — a single bind(ClockInterface::class, NativeClock::class) in a service provider is the whole integration surface.created_at/updated_at bookkeeping, which Eloquent already owns.Carbon::setTestNow(), travel()/freezeTime() in Illuminate\Foundation\Testing, and (11+) the Illuminate\Support\Sleep facade. symfony/clock does not replace these; it competes for the same developer habit. The architectural value is the injectable interface (constructor-visible dependency, swappable in prod, not only under test), which Carbon's static test-now does not give you. If we adopt it, we must say plainly that it is the standard for domain time while Carbon remains the standard for formatting and DB values — otherwise we get three time idioms in one codebase.Symfony\Component\Clock\ClockInterface is already consumed by several Symfony components; psr/clock is a hard dependency, so an app-level PSR-20 adapter is nearly free if we prefer the PSR interface over Symfony's (which adds sleep() and withTimeZone()).Integration feasibility
Psr\Clock\ClockInterface. No config file, no migrations, no publishes, no env vars.now() returns \DateTimeImmutable, and CarbonImmutable::createFromInterface($clock->now()) bridges into the existing ecosystem.Technical risk
MockClock bound in a non-test environment freezes application time globally. Mitigation: bind MockClock only inside the test case/ClockSensitiveTrait scope, never in a provider gated on APP_ENV.MockClock::sleep() negative-value divergence was fixed in 6.4.30 / 7.3.8. The new v8.1.9 patch release adds fixes for DatePoint being one hour off around the DST fall-back hour, relative-date DatePoint behavior, and MockClock::sleep() with dates before 1970. There are no breaking changes, new features, or deprecations in this release, but the 8.x floor should now be >=8.1.9 if we use that line. The 8.x line requires PHP >= 8.4; 7.4 remains the safe LTS target. If DST or pre-1970 MockClock behavior is relevant on 7.4, confirm equivalent fixes are present in that branch.NativeClock) must not be used to measure elapsed durations; MonotonicClock exists precisely because system time can jump (NTP, DST, VM suspend). Mixing the two in retry/timeout logic is a subtle, hard-to-reproduce bug class.Key questions
Symfony\Component\Clock\ClockInterface or Psr\Clock\ClockInterface? The PSR choice is more portable and survives a framework change.>=8.1.9 and accept PHP >= 8.4?Illuminate\Support\Sleep::fake() plus travel() already cover the queued-job and scheduling cases? Where it does, we should not add a second mechanism.now(), time(), date(), Carbon::now() inside the converted domain layer? Without enforcement, adoption decays.Stack fit
symfony/clock 7.4 LTS. PHP 8.4+ shops may take 8.x, pinned to >=8.1.9. Symfony 6.4 (PHP >= 8.1) remains viable for older Laravel.symfony/clock is frequently already present transitively (Symfony console/http-foundation-driven packages, rate limiter, messenger). Run composer why symfony/clock before adding a direct requirement — we may only be pinning a constraint, not adding a dependency.withTimeZone(), which returns a clone rather than mutating).Migration path
>=8.1.9 for 8.x; >=6.4.30/>=7.3.8 for the older lines where applicable); add the provider and bindings.now()/time()/Carbon::now() with the injected clock; replace usleep() with $clock->sleep() only where the sleep is domain-meaningful (retry backoff), not in test scaffolding.MockClock per test, with setNow() fixtures. Establish one shared helper so every team writes time tests the same way.Compatibility
DateTimeImmutable out, so anything currently accepting Carbon needs a conversion at the boundary — cheap, but it is per-call-site work and should be counted.Carbon::setTestNow(); they can coexist, but coexistence should be temporary and deliberate, not permanent. Pick one per layer.Illuminate\Support\Sleep; if we adopt ClockInterface::sleep(), we should route queue/cron delay logic through it (or through Sleep) consistently rather than mixing both.MockClock edge cases; it does not change the DateTimeImmutable boundary conversion cost or the coexistence model above.Sequencing
Maintenance
Support
Carbon::now() within a couple of quarters.Scaling
now() call in the same order as new DateTimeImmutable(). Compared with Carbon's heavier construction, this is neutral-to-cheaper.withTimeZone('UTC') at the boundary) and convert at the presentation edge.sleep() remains a blocking pause and must not be introduced into request paths.Failure modes
MockClock bound outside tests. Highest-severity, lowest-likelihood; prevented by binding scope discipline and a startup assertion in non-test environments.NativeClock and UTC-normalized clocks producing off-by-hours bugs at boundaries. Mitigated by normalizing at injection.DatePoint — pre-v8.1.9 8.x installs can be one hour off around DST fall-back; mitigated by pinning 8.x to >=8.1.9 and verifying the 7.4 LTS branch for equivalent fixes if those cases matter.MonotonicClock.MockClock negative-sleep, pre-1970 MockClock::sleep(), or DatePoint DST fixes. Mitigated by an explicit composer.json constraint, not a floating one.$clock->sleep() in a web request or a synchronous path, holding a worker. Mitigated by convention plus review.Ramp-up
Carbon::setTestNow(). The only genuine learning is the boundary convention — which clock for which purpose — and that should be a half-page ADR, not a training program.now(). That friction is the migration cost; it does not recur once the guardrail is in place.How can I help you explore Laravel packages today?