lcobucci/clock
Small PHP clock abstraction to decouple your code from direct DateTimeImmutable instantiation. Depend on the Clock interface and use SystemClock for real time or FrozenClock for deterministic tests, with explicit timezone support.
composer require lcobucci/clock
Clock interface into your services:
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
class MyService {
public function __construct(private Clock $clock) {}
}
Clock interface in AppServiceProvider:
public function register(): void {
$this->app->bind(Clock::class, function () {
return new SystemClock(new DateTimeZone('UTC'));
});
}
Replace new DateTimeImmutable() calls with $clock->now() in:
FrozenClock to mock time).Service Container Binding:
// For production (UTC)
$this->app->singleton(Clock::class, fn() => SystemClock::fromUTC());
// For testing (frozen time)
$this->app->when(TestCase::class)
->needs(Clock::class)
->give(FrozenClock::fromUTC());
Constructor Injection:
class OrderService {
public function __construct(
private Clock $clock,
private OrderRepository $orders
) {}
public function cancelExpiredOrders(): void {
$now = $clock->now();
$this->orders->cancelWhere('expires_at < ?', $now);
}
}
Frozen Time for Tests:
$clock = new FrozenClock(new DateTimeImmutable('2023-01-01'));
$this->app->instance(Clock::class, $clock);
// Advance time in tests
$clock->adjustTime(new DateTimeImmutable('2023-01-02'));
Time-Based Assertions:
$this->assertEquals(
new DateTimeImmutable('2023-01-01'),
$clock->now()
);
SystemClock for scheduled jobs (e.g., app(Clock::class)->now() in job handlers).$clock->now() for consistency:
return response()->json([
'created_at' => $clock->now()->format(DateTimeInterface::ATOM),
]);
Timezone Mismatches:
SystemClock (e.g., SystemClock::fromUTC()).Immutable SystemClock:
SystemClock is marked as @immutable in PHP 8.3+. Avoid modifying it after creation.Thread Safety:
SystemClock is thread-safe, but FrozenClock is not. Use FrozenClock only in single-threaded contexts (e.g., tests).Time Drift in Tests:
FrozenClock is properly reset between tests:$this->app->forgetInstance(Clock::class);
Performance:
SystemClock::now() is lightweight, but avoid calling it in tight loops. Cache results if needed.Custom Clock Implementations:
class DatabaseClock implements Clock {
public function now(): DateTimeInterface {
return new DateTimeImmutable(
DB::table('clock')->value('current_time')
);
}
}
Time Adjustment:
FrozenClock::adjustTime() for time-traveling tests:$clock->adjustTime(new DateTimeImmutable('+1 hour'));
PSR-20 Compliance:
PSR\Clock\ClockInterface. Prefer this interface for future-proofing:use PSR\Clock\ClockInterface;
Configurable Timezone:
Add a config option in config/app.php:
'timezone' => env('APP_TIMEZONE', 'UTC'),
Then bind dynamically:
$this->app->singleton(Clock::class, fn() =>
SystemClock::from(new DateTimeZone(config('app.timezone')))
);
Testing Helpers: Create a test trait for frozen clocks:
trait UsesFrozenClock {
protected function freezeTime(string $time = 'now'): FrozenClock {
return $this->app->instance(
Clock::class,
$time === 'now'
? FrozenClock::fromUTC()
: new FrozenClock(new DateTimeImmutable($time))
);
}
}
How can I help you explore Laravel packages today?