tezmanian / utils-clock
Testable clock abstractions and a high-resolution stopwatch for PHP — ClockInterface, SystemClock, FrozenClock for deterministic tests, and Stopwatch with lap support. PSR-20 compatible, PHP 8.3+.
Requires
- php: >=8.3
- psr/clock: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- jetbrains/phpstorm-attributes: ^1.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Testable clock abstractions and a high-resolution stopwatch for PHP — ClockInterface, SystemClock, FrozenClock for deterministic tests, and Stopwatch with lap and stop/resume support. PSR-20 compatible, PHP 8.3+.
Installation
composer require tezmanian/utils-clock
Package: tezmanian/utils-clock · Namespace: Tezmanian\Utils\Clock
Components
| Class | Purpose |
|---|---|
ClockInterface |
Extends Psr\Clock\ClockInterface (PSR-20) |
SystemClock |
Returns the actual current time |
FrozenClock |
Returns a fixed time — for deterministic tests |
Stopwatch |
High-resolution elapsed time and lap measurement |
ClockInterface
interface ClockInterface { public function now(): \DateTimeImmutable; }
Extends Psr\Clock\ClockInterface (PSR-20), so every clock here works wherever a PSR-20 clock is expected. Inject ClockInterface into services instead of calling new \DateTimeImmutable() directly — this makes time controllable in tests without mocking.
SystemClock
Returns the actual current time, honoring the system timezone.
use Tezmanian\Utils\Clock\SystemClock; $clock = new SystemClock(); $now = $clock->now(); // \DateTimeImmutable // With an explicit timezone $tokyo = new SystemClock(new \DateTimeZone('Asia/Tokyo'));
FrozenClock
Returns a fixed DateTimeImmutable — always the same instant until set() is called. Use it in tests to control time without relying on the system clock.
use Tezmanian\Utils\Clock\FrozenClock; $clock = new FrozenClock(new \DateTimeImmutable('2026-01-01 12:00:00')); $clock->now(); // 2026-01-01 12:00:00 $clock->now(); // 2026-01-01 12:00:00 — always the same // Advance to a new instant $clock->set(new \DateTimeImmutable('2026-06-15 08:30:00')); $clock->now(); // 2026-06-15 08:30:00 // Move relative to the current instant (DateInterval or relative string) $clock->advance('+5 minutes'); $clock->advance(new \DateInterval('P1D')); $clock->advance('-1 hour'); // rewind
advance() throws InvalidArgumentException for strings that are not a valid relative time; the clock stays unchanged in that case.
Testing example
final class OrderServiceTest extends TestCase { public function testOrderExpiresAfterDeadline(): void { $clock = new FrozenClock(new \DateTimeImmutable('2026-01-01 12:00:00')); $service = new OrderService($clock); self::assertFalse($service->isExpired($order)); // Advance the clock past the deadline $clock->advance('+9 days'); self::assertTrue($service->isExpired($order)); } }
Stopwatch
High-resolution elapsed time measurement using hrtime() (nanosecond precision). Not affected by NTP adjustments or system clock changes.
use Tezmanian\Utils\Clock\Stopwatch; $sw = Stopwatch::start(); // ... do work ... $sw->elapsed(); // total seconds since start (float) $sw->elapsedMs(); // total milliseconds since start (float)
Lap timing
lap() returns the elapsed seconds since the last lap() call (or since start if no lap yet), then moves the lap marker forward.
$sw = Stopwatch::start(); // ... first stage ... $stage1 = $sw->lap(); // seconds for stage 1 // ... second stage ... $stage2 = $sw->lap(); // seconds for stage 2 only $total = $sw->elapsed(); // total since start (unaffected by laps)
Stop and resume
$sw->stop(); // freezes elapsed() and lap() $sw->isRunning(); // false $sw->resume(); // continues; the stopped time is not counted
stop() and resume() are idempotent.
Reset
$sw->reset(); // back to zero (timer and lap marker) and running
Requirements
- PHP 8.3+
psr/clock^1.0
Development
make build && make install make ci # audit, PHPStan, code style, tests
License
MIT — see LICENSE.
Author: René Halberstadt