Skip to content

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:

$now = $this->clock->now();
$dueDate = $now->add(new DateInterval('P14D'));

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:

ClockInterface::class => fn (): MonotonicClock => new MonotonicClock(new SystemClock()),

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 seconds
  • advance(DateInterval $interval) - Advance time by interval
  • rewindSeconds(int $seconds) - Rewind time by seconds
  • rewind(DateInterval $interval) - Rewind time by interval
  • reset() - 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