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.
What it is: A ~4-file library that defines a PSR-20 Clock interface plus two implementations (SystemClock, FrozenClock). Its entire value proposition is replacing direct new DateTimeImmutable() / now() calls with an injected seam so time becomes deterministic in tests. There is no I/O, no state, no configuration, and no service provider.
Architecture fit: Poor-to-neutral for a typical Laravel codebase, and this is the central finding. Laravel already ships a time abstraction: Carbon::setTestNow(), the now() helper, and Carbon's CarbonImmutable all solve the same testability problem with zero new dependencies and zero new concepts for the team. Adopting lcobucci/clock means running two time abstractions in parallel, and the team must consciously decide which is canonical. The package is a better architectural fit for framework-agnostic libraries and DDD-style domain layers that must not depend on Laravel's helpers — that is its real niche, not "a Laravel app."
Integration feasibility: High mechanical feasibility, low political cost. Composer install, one container binding, and injection into services. The friction is not installation, it is retrofitting: every now(), Carbon::now(), new DateTimeImmutable(), and time() call in domain code must be converted before the abstraction pays for itself. Partial adoption yields the worst outcome — a clock that is injected in some paths and bypassed in others, so tests freeze time that production logic never reads.
Technical risk — the PHP floor is the headline risk. The release history shows an unusually aggressive support policy: 3.2.0 added PHP 8.3, 3.4.0 dropped PHP 8.2, and 3.6.0 requires PHP 8.4. This library tracks the newest PHP release and does not maintain a wide-version LTS line. If the application (or any deploy target, CI image, or shared hosting tenant) runs PHP 8.2 or 8.3, you are pinned to an older minor and will be forced into a PHP upgrade on the maintainer's schedule, not yours. For a TPM this is a recurring upgrade tax on a dependency that delivers maybe 200 lines of logic you could vendor yourself.
Secondary risks: a single primary maintainer; version 3.0.0 was a BC break (stella-maris/clock → psr/clock); 2.0.0 was a BC break (mandatory timezone argument); FrozenClock shipped with a bug fixed in 3.3.1. The trajectory is stable now, but this is a package that has broken compatibility three times in its history.
Data caveat: "Dependents: 0" should not be read as "nobody uses this." lcobucci/jwt — one of the most-installed PHP JWT libraries — depends on this package, so the real deployment footprint is orders of magnitude above the reported figure. Treat the metric as an artifact of the scanner, not as an adoption signal. Conversely, "Last release 2026-04-13" combined with a near-future dating convention means the freshness signal is only as good as the snapshot.
Key questions to resolve before committing:
Carbon::setTestNow() is sufficient?now()/new DateTimeImmutable() in the affected namespace)? If no, do not adopt.FrozenClock guaranteed never to be reachable in a production code path, and how is that enforced?Stack fit: Composer-native, PSR-20-compliant, framework-agnostic — so it slots into Laravel cleanly but with no framework integration offered. There is no service provider, no facade, no config file; we own the wiring. Recommended bindings:
Clock::class → SystemClock::fromUTC() as a singleton (the class is readonly/immutable as of 3.4.0, so this is safe and cheap).FrozenClock::fromUTC() or new FrozenClock(new DateTimeImmutable('...')), with adjustTime() available (3.3.0+) for advancing time mid-test.SystemClock::fromUTC() over fromSystemTimezone() — fromSystemTimezone() reintroduces the ambient-environment dependency the package exists to remove, and it silently changes behavior when php.ini date settings differ between local, CI, and production.Migration path — incremental, evidence-driven, opt-in:
Carbon::setTestNow() globally and drop this package.Clock; framework glue, Eloquent timestamp columns, and Laravel's scheduler continue to use Carbon. Document that boundary; ambiguity here is how you end up with two half-adopted abstractions.sleep().Compatibility notes:
psr/clock (^1.0) — a two-method interface with no runtime weight. Many other libraries type-hint the same interface, so interop is genuine, not theoretical. Confirm no conflicting PSR-20 implementation is pinned elsewhere in the lockfile.lcobucci/jwt is already installed, lcobucci/clock may already be in composer.lock as a transitive dependency at a version pinned by JWT, not by us. Promote it to a direct dependency deliberately (and align the constraint) rather than relying on an incidental transitive copy.Sequencing recommendation: Do the PHP-baseline check and the time-read-call-site count first — both are hours of work and either can kill the initiative. Commit to the container binding and one pilot only if both come back clean. Budget the retrofit, not the install: the install is minutes, the conversion of call sites and the enforcement discipline is where the effort lives.
Maintenance burden: Near zero at runtime, non-trivial at upgrade time. The dependency is dependency-light (Composer, PSR-20) but the maintainer ships frequent releases that track the newest PHP and bump dev tooling aggressively — the 3.6.0 notes are almost entirely Renovate bot PRs plus a dependency upgrade. Practically, expect a forced PHP minimum bump roughly every 12–18 months and a major version every few years. Plan for this in the platform upgrade calendar; it is a small but inescapable recurring commitment, and it will occasionally be the package that gates a Laravel major upgrade.
Support model: Single primary maintainer, active, MIT licensed, GitHub issues only, no commercial backing and no security-response commitment (though the surface is tiny — two methods returning DateTimeImmutable). Bus-factor risk exists but is mitigated by the fact that the entire library is small enough to fork or vendor in an afternoon. If this becomes load-bearing, capture a fork-and-maintain contingency in the architecture decision record. Do not treat it as a supply-chain critical dependency requiring ongoing vendor management; do treat it as a package you may one day own.
Scaling: Not a factor. No I/O, no caching, no shared state, immutable objects, single shared singleton instance. Cost is one object allocation per request. There is no horizontal or vertical scaling consideration and it will never appear in a performance profile. This is genuinely the strongest part of the case.
Failure modes and mitigations:
FrozenClock yields a permanently static "now," silently breaking expiry checks, token validity, TTLs, and time-window logic. It fails quietly and can persist undetected. Mitigation: bind FrozenClock only inside test bootstrap, never keyed off a runtime env flag; add an assertion or health check that Clock::class resolves to SystemClock outside the testing environment; alert on any production path returning a monotonic timestamp.SystemClock requires an explicit timezone (since 2.0.0), which is good design, but a team that reaches for fromSystemTimezone() reinstates the ambient php.ini dependency and gets divergent behavior between containers. Mitigation: mandate fromUTC() in the binding and convert to display timezones at the presentation edge only.now(). Tests freeze one and exercise the other, producing false confidence. This is the most likely failure and the hardest to detect. Mitigation: the enforcement rule in step 5 above; treat any new direct now() in the domain namespace as a review defect.composer update raises the minimum PHP and breaks CI or deploy. Mitigation: pin with a caret constraint on the current major, gate dependency updates through CI's PHP matrix, and never accept a transitive bump to this package without checking the required PHP version.Ramp-up: Trivial technically — a mid-level PHP developer understands the injected-clock pattern in under an hour, and the README's example is sufficient documentation. The real ramp-up cost is cultural, not technical: convincing a team accustomed to now() and Carbon::setTestNow() to consistently inject a clock, and keeping them consistent after the initial advocate moves on. Budget for that inertia explicitly, and be honest that for many Laravel teams it is the deciding factor. If the team will not sustain the discipline, the correct decision is to skip this package and standardize on Carbon's test-time facilities — the dependency is cheap, but a half-implemented abstraction is worse than none.
How can I help you explore Laravel packages today?