symfony/clock
Symfony Clock decouples your app from the system clock via a ClockInterface. Swap real and test clocks, get DateTimeImmutable “now()” values, control time zones, and pause execution with sleep()—ideal for time-sensitive code and reliable testing.
Installation:
composer require symfony/clock
No additional configuration is required for basic usage.
First Use Case:
Replace direct system time calls (e.g., time(), Carbon::now()) with dependency-injected ClockInterface.
Example:
use Symfony\Component\Clock\NativeClock;
use Symfony\Component\Clock\ClockInterface;
class SubscriptionService {
public function __construct(private ClockInterface $clock) {}
public function isExpired(\DateTimeImmutable $subscriptionDate): bool {
return $this->clock->now() > $subscriptionDate;
}
}
Register the Clock in Laravel:
Bind NativeClock in a service provider (e.g., AppServiceProvider):
use Symfony\Component\Clock\NativeClock;
public function register() {
$this->app->singleton(ClockInterface::class, fn() => new NativeClock());
}
First Test Case:
Use MockClock to freeze time in unit tests:
use Symfony\Component\Clock\MockClock;
public function testSubscriptionExpiry() {
$clock = new MockClock(new \DateTimeImmutable('2023-01-01'));
$service = new SubscriptionService($clock);
$this->assertFalse($service->isExpired(new \DateTimeImmutable('2024-01-01')));
}
ClockInterface, NativeClock, MockClock, and Stopwatch.ClockSensitiveTrait: Laravel-specific helper for quick integration (see Laravel’s ClockSensitiveTrait).symfony/clock Tests: Test suite demonstrates usage patterns (e.g., MockClock, sleep()).NativeClock (default system time).
$this->app->bind(ClockInterface::class, fn() => new NativeClock());
MockClock for deterministic time.
$clock = new MockClock(new \DateTimeImmutable('2023-01-01'));
$this->app->instance(ClockInterface::class, $clock);
RedisClock for distributed time sync).
$this->app->bind(ClockInterface::class, fn() => new RedisClock());
$clock = (new NativeClock())->withTimeZone('UTC');
$this->app->instance(ClockInterface::class, $clock);
sleep() or usleep() with clock-aware pauses:
// In a background job or command
$this->clock->sleep(2.5); // Pauses for 2.5 seconds
sleep(1) may run faster/slower in CI).sleep() throws \InvalidArgumentException for negative values (use MockClock for time rewinding).$stopwatch = $this->clock->stopwatch();
$stopwatch->start('process_order');
// ... business logic ...
$duration = $stopwatch->lap('process_order'); // Returns \DateInterval
ClockInterface (Laravel 10+):
use Illuminate\Clock\ClockSensitive;
class MyService {
use ClockSensitive;
public function doWork() {
$now = $this->clock->now();
// ...
}
}
travel() alongside MockClock:
use Illuminate\Testing\Concerns\InteractsWithTime;
public function testTimeTravel() {
$clock = new MockClock(new \DateTimeImmutable('2023-01-01'));
$this->app->instance(ClockInterface::class, $clock);
// Now use Laravel's travel() for Carbon compatibility
Carbon::setTestNow($clock->now());
}
sleep() in jobs with $this->clock->sleep() for deterministic delays:
public function handle() {
$this->clock->sleep(5); // 5-second delay
// ...
}
MockClock to fast-forward job processing:
$clock = new MockClock(new \DateTimeImmutable('2023-01-01 00:00:10'));
$this->app->instance(ClockInterface::class, $clock);
$clock = new MockClock(new \DateTimeImmutable('2023-01-01 12:00:00'));
event(new OrderPlaced($order, $clock->now()));
ClockInterface to generate timestamps for Eloquent models:
$model = new Post();
$model->created_at = $this->clock->now();
now() vs. freshTimestamp() mismatches).$clock = new MockClock(new \DateTimeImmutable('2023-01-01 10:00:00'));
$response = $this->client->get('/api/rate-limited-endpoint', [
'clock' => $clock,
]);
class FeatureFlagService {
public function isEnabled(string $flagName): bool {
$now = $this->clock->now();
$flag = Flag::where('name', $flagName)
->where('starts_at', '<=', $now)
->where('ends_at', '>=', $now)
->first();
return $flag !== null;
}
}
$clock = new MockClock(new \DateTimeImmutable('2023-01-01 14:00:00'));
$this->assertTrue($service->isEnabled('new_checkout_flow'));
Negative sleep() Values
NativeClock::sleep(-1) throws \InvalidArgumentException.MockClock to rewind time:
$clock = new MockClock(new \DateTimeImmutable('2023-01-01 10:00:00'));
$clock->sleep(-5); // Rewinds 5 seconds (allowed in MockClock)
Timezone Mismatches
withTimeZone() does not modify the DateTimeImmutable’s timezone retroactively. Always set it upfront:
// Wrong: Assumes UTC after setting timezone
$clock = new NativeClock();
$now = $clock->now(); // System timezone
$clock = $clock->withTimeZone('UTC'); // Too late!
// Correct:
$clock = (new NativeClock())->withTimeZone('UTC');
Immutable DateTimeImmutable
DateTimeImmutable (e.g., $now->modify('+1 day')) does not affect the clock’s state. Clocks are stateless:
$now = $this->clock->now();
$now->modify('+1 day'); // Only affects $now, not future calls to $this->clock->now()
MockClock Precision
MockClock advances time in second increments by default. For microsecond precisionHow can I help you explore Laravel packages today?