marko/clock
PSR-20 system clock --- inject ClockInterface instead of calling time() so time-dependent code can be tested deterministically. The package binds the standard Psr\Clock\ClockInterface to SystemClock, which reads the system time. There is no Marko-specific clock interface: PSR-20 is already a single explicit method, and depending on it keeps your code compatible with any other PSR-20 clock (Symfony’s or Lcobucci’s, for example). In tests, swap in FakeClock from marko/testing.
Installation
Section titled “Installation”composer require marko/clockThe module binds ClockInterface to SystemClock as a singleton. There is no configuration file.
Injecting the Clock
Section titled “Injecting the Clock”Type-hint Psr\Clock\ClockInterface in a constructor and call now() wherever you would have called time() or new DateTimeImmutable():
use DateTimeImmutable;use Psr\Clock\ClockInterface;
class InvitationService{ public function __construct( private ClockInterface $clock, ) {}
public function expiresAt(): DateTimeImmutable { return $this->clock->now()->modify('+7 days'); }
public function isExpired(Invitation $invitation): bool { return $invitation->expiresAt < $this->clock->now(); }}For a Unix timestamp, use $this->clock->now()->getTimestamp().
Timezone
Section titled “Timezone”By default SystemClock returns times in PHP’s default timezone (date_default_timezone_get()), read each time now() is called. To pin a timezone, construct the clock yourself and bind it in your module:
use Marko\Clock\SystemClock;use Marko\Core\Container\ContainerInterface;use Psr\Clock\ClockInterface;
return [ 'bindings' => [ ClockInterface::class => fn (ContainerInterface $container): ClockInterface => new SystemClock('UTC'), ],];The constructor accepts a DateTimeZone or a timezone name. An unknown name throws DateInvalidTimeZoneException.
You don’t need to pin the clock to get consistent stored times. Entity datetimes and the timestamps Marko packages write to the database (queue jobs, failed jobs, token expiry, notifications, webhook attempts) are converted to the database timezone before they are stored, whatever zone the clock returns.
Testing
Section titled “Testing”Pass a FakeClock wherever the code under test expects a ClockInterface. It is frozen until you move it, so boundaries can be checked exactly:
use Marko\Testing\Fake\FakeClock;
it('expires an invitation after seven days', function (): void { $clock = new FakeClock('2026-01-01 12:00:00 UTC'); $service = new InvitationService($clock); $invitation = new Invitation(expiresAt: $service->expiresAt());
$clock->travel('+7 days'); expect($service->isExpired($invitation))->toBeFalse();
$clock->travel('+1 second'); expect($service->isExpired($invitation))->toBeTrue();});Packages Using the Clock
Section titled “Packages Using the Clock”These packages take an injected ClockInterface and so require marko/clock:
marko/session--- session cookie expirymarko/session-fileandmarko/session-database---last_activityand garbage collectionmarko/authentication-token--- personal access token expiry checks, and the default expiry andcreated_atof new tokensmarko/authentication--- remember-token expiry and remember-cookie expirymarko/errors-simpleandmarko/errors-advanced---ErrorReporttimestamps (ErrorReport::fromThrowable()inmarko/errorstakes the timestamp as an argument)marko/log--- thelog:clearage cutoffmarko/log-file--- record times and the daily file namemarko/testing---TestClient’sREQUEST_TIMEand cookie expiry (falls back toSystemClockwhen no clock is bound)
marko/admin-auth event timestamps come from the marko/database repository’s current instant rather than a clock of its own.
marko/webhook--- signature timestamps, the freshness window, and attempt timesmarko/cache-arrayandmarko/cache-file--- cache entry expiry andcreated_atmarko/cache-redis--- the reportedexpiresAt()of a cache item (Redis itself enforces the TTL)marko/ratelimiter---Retry-Aftermarko/page-cache-file--- cached page expirymarko/notificationandmarko/notification-database---created_atandread_attimestampsmarko/broadcasting-amphp--- subscriber token expiry, event id timestamps, and replay buffer evictionmarko/broadcasting-mercure--- subscriber JWT and cookie expirymarko/broadcasting-pusher--- request signature timestampsmarko/media--- theYYYY/MMupload pathmarko/sse--- stream heartbeats and timeouts (SseStreamdefaults toSystemClockwhen you construct it without one)marko/database---#[Timestamps]values and generated migration file namesmarko/queue--- the worker’s failed-job timemarko/queue-database--- job availability, delays, reservation expiry and failed-job timemarko/scheduler--- which tasks are due, theschedule:workloop, and overlap-mutex expiry
API Reference
Section titled “API Reference”SystemClock
Section titled “SystemClock”use Marko\Clock\SystemClock;
public function __construct(DateTimeZone|string|null $timezone = null);public function now(): DateTimeImmutable;ClockInterface
Section titled “ClockInterface”use Psr\Clock\ClockInterface;
public function now(): DateTimeImmutable;Related Packages
Section titled “Related Packages”marko/testing---FakeClockfor deterministic tests