Clock
The Clock plugin provides a testable clock abstraction based on the PSR-20 Clock interface.
PSR-20 Compatibility
The plugin implements Psr\Clock\ClockInterface, making it interoperable with any PSR-20 compliant library. The ClockInterface port extends the PSR interface, allowing type-hints with either Phexium\Plugin\Clock\Port\ClockInterface or Psr\Clock\ClockInterface.
Adapters
Four adapters are available:
- SystemClock returns the actual system time for production use.
- MonotonicClock is a decorator that guarantees strictly increasing time, even if the system clock jumps backward.
- FrozenClock returns a fixed, predetermined time for testing.
- OffsetClock is a decorator that applies a time offset to any inner clock, allowing time manipulation in tests.
Why Use It
Direct calls to new DateTimeImmutable() create code that cannot be reliably tested. When business logic depends on the current time (loan due dates, event timestamps, scheduled tasks), tests become unpredictable or require complex workarounds.
The ClockInterface solves this problem by abstracting time access. In production, the real system time is returned. In tests, a frozen adapter allows fixing time at a known value.
Usage
The interface exposes a single method that returns the current time:
Monotonic Time
The MonotonicClock decorator guarantees that each call to now() returns a strictly greater timestamp than the previous call. When the underlying clock returns the same or an earlier time, MonotonicClock increments by one microsecond.
This is essential when using timestamp-based ID generators like TimestampIdGenerator, where duplicate timestamps would produce duplicate IDs:
The demo application uses this configuration by default.
Testing
The FrozenClock adapter makes time-dependent tests deterministic:
$clock = new FrozenClock('2024-03-01T10:00:00+00:00');
$clock->now(); // Always returns 2024-03-01 10:00:00
Advancing Time with OffsetClock
The OffsetClock decorator wraps any clock and applies a time offset, allowing tests to simulate time passing:
$clock = new OffsetClock(new FrozenClock('2024-03-01T10:00:00+00:00'));
$clock->advanceSeconds(60); // Advance 60 seconds
$clock->now(); // Returns 2024-03-01 10:01:00
$clock->advance(new DateInterval('PT1H')); // Advance 1 hour
$clock->now(); // Returns 2024-03-01 11:01:00
Available methods:
advanceSeconds(int $seconds)- Advance time by secondsadvance(DateInterval $interval)- Advance time by intervalrewindSeconds(int $seconds)- Rewind time by secondsrewind(DateInterval $interval)- Rewind time by intervalreset()- Reset offset to zero
This is useful for testing TTL-based cache expiration, scheduled tasks, or any time-dependent logic:
test('Cache entry expires after TTL', function (): void {
$clock = new OffsetClock(new FrozenClock('2024-03-01T10:00:00+00:00'));
$cache = new InMemoryCache($clock);
$cache->set('key', 'value', 60); // 60 seconds TTL
expect($cache->has('key'))->toBeTrue();
$clock->advanceSeconds(61);
expect($cache->has('key'))->toBeFalse();
});
See Also
- Domain Events - Events use occurredOn timestamps
- Unit Testing - FrozenClock for deterministic tests
- PSR-20 Clock - Interface specification