symfony/clock
Symfony Clock decouples your code from the system clock. Inject ClockInterface to get DateTimeImmutable via now(), control timezones, and pause execution with sleep(). Ideal for testable, time-sensitive services without relying on global time.
composer require symfony/clock
ClockInterface into services requiring time awareness. Use NativeClock in production:
use Symfony\Component\Clock\NativeClock;
use Symfony\Component\Clock\ClockInterface;
class OrderService {
public function __construct(private ClockInterface $clock) {}
public function checkExpiry(DateTimeInterface $orderDate): bool {
return $this->clock->now() > $orderDate;
}
}
new DateTimeImmutable() or Carbon::now() calls in a critical path (e.g., subscription validation) with $clock->now(). Test by injecting a MockClock in unit tests.ClockInterface: Core contract with now() and sleep() methods.NativeClock: Default implementation for production.MockClock: Pre-built test double for PHPUnit (see release notes for PHPUnit 10/11 support).Dependency Injection:
NativeClock as a singleton in Laravel’s service container (e.g., AppServiceProvider):
$this->app->singleton(ClockInterface::class, fn() => new NativeClock());
public function __construct(private ClockInterface $clock) {}
Timezone Isolation:
$utcClock = $clock->withTimeZone('UTC');
config/clock.php) for consistency.Testing Patterns:
NativeClock with MockClock to freeze or advance time:
use Symfony\Component\Clock\MockClock;
$clock = new MockClock();
$clock->setTime(new DateTimeImmutable('2023-11-15T12:00:00Z'));
$service = new OrderService($clock);
MockClock to simulate delays or time jumps:
$clock->setTime($clock->now()->modify('+1 hour')); // Fast-forward
$clock->sleep(0.5); // Simulate a 0.5s delay
Background Jobs:
usleep() or sleep() with $clock->sleep() for deterministic delays in queues:
$clock->sleep(2); // 2-second delay (testable!)
Event Sourcing:
$clock = new MockClock();
$clock->setTime(new DateTimeImmutable('2023-01-01'));
// Process events...
$clock->setTime($clock->now()->modify('+1 day'));
ClockSensitiveTrait (from Symfony) or build a custom trait to auto-inject ClockInterface:
use Symfony\Component\Clock\ClockSensitiveTrait;
class MyService {
use ClockSensitiveTrait;
// $this->clock is automatically injected
}
now() helper by binding ClockInterface to the helper:
$this->app->bind('now', fn() => $this->app->make(ClockInterface::class)->now());
DateTimeImmutable to Carbon if needed:
use Carbon\Carbon;
$carbonTime = Carbon::instance($clock->now());
$clock->now() in Eloquent model events or accessors:
protected static function boot() {
static::creating(fn($model) => $model->created_at = app(ClockInterface::class)->now());
}
Timezone Mismatches:
withTimeZone('UTC') for internal clocks and document the expected timezone in service contracts.$clock->now() in development:
$request->clockTimezone = $clock->now()->getTimezone()->getName();
MockClock vs. NativeClock Behavior:
MockClock::sleep() with negative values was historically inconsistent (fixed in v7.3.0). Ensure tests use recent versions.$clock->sleep(-1) in your test suite.Immutable Time:
$clock->now() returns a DateTimeImmutable, which can cause confusion if modified directly (e.g., $now->modify()).DateTime if mutability is needed:
$now = $clock->now(); // Immutable
$mutableNow = clone $now; // Safe to modify
Performance Overhead:
$clock->sleep() may introduce slight jitter in high-frequency loops.usleep()) and reserve ClockInterface for business logic.Circular Dependencies:
ClockInterface can create tight coupling between services.SubscriptionService, RateLimiter). Avoid injecting into repositories or DTOs.Log::debug('Clock time:', ['time' => $clock->now()->format('Y-m-d H:i:sP')]);
MockClock to simulate edge cases (e.g., "What if this timeout fired 10 minutes late?"):
$clock->setTime($clock->now()->modify('+10 minutes'));
use Symfony\Component\Clock\ClockTestTrait;
class MyTest extends TestCase {
use ClockTestTrait;
public function testTimeSensitiveLogic() {
$this->setClockTime('2023-11-15T12:00:00Z');
// Test logic...
}
}
Custom Clock Implementations:
RedisClock for distributed time synchronization:
class RedisClock implements ClockInterface {
public function now(): DateTimeImmutable {
return DateTimeImmutable::createFromFormat('U.u', Redis::get('clock_time'));
}
public function sleep(float $seconds): void {
usleep($seconds * 1_000_000);
}
}
MockClock as a base for domain-specific clocks (e.g., GameClock for turn-based games).Time Zone Providers:
$clock = $nativeClock->withTimeZone($user->timezone ?? 'UTC');
Clock Middleware:
$start = $clock->now();
// Process request...
$duration = $clock->now()->getTimestamp() - $start->getTimestamp();
Clock Events:
if ($clock->now()->diff($maintenanceTime)->s <= 3600) {
event(new MaintenanceWarning());
}
NativeClock with Laravel’s cache, ensure the cache driver doesn’t interfere with time-sensitive operations (e.g., rate limiting).RedisClock above).time(), date(), or Carbon::now() by wrapping them in a LegacyClock adapter:
class LegacyClock implements ClockInterface {
public function now(): DateTimeImmutable
How can I help you explore Laravel packages today?