Mastering Time in Symfony with the Clock Component
Time is everywhere in software systems. Tokens expire, subscriptions have grace periods, retries wait a few seconds before running again, background jobs are delayed, scheduled tasks run at specific times. Whether we notice it or not, a lot of business logic depends on time.
And yet, in most PHP applications, time is accessed in the simplest possible way:
new \DateTimeImmutable();It looks perfectly harmless. In fact, we’ve all written code like this hundreds of times (yes, I know you did it too 👀).
However this tiny line introduces a subtle design issue: your code now depends directly on the system clock. That dependency is implicit, impossible to control, and surprisingly hard to test.
If you’ve ever written a test that relies on the current time, struggled with expiration logic, or added a sleep() in a test just to make an assertion pass, you’ve probably encountered this problem.
Fortunately, there is a better approach. With the introduction of PSR-20, PHP now has a standard abstraction for time: ClockInterface. Symfony provides a full implementation through its Clock component, making it easy to inject and control time inside your services.
In this article, we’ll explore why treating time as a dependency matters and how Symfony’s Clock component can make your code significantly easier to test. To keep things concrete, we’ll start with a very common example: generating a token that expires after one hour.
The hidden dependency: Time ⏱️
In most Symfony applications, dependencies are explicit.
We inject repositories, loggers, caches, message buses, HTTP clients… everything our services need to do their job. This makes the code easier to reason about and, more importantly, much easier to test.
But there is one dependency that almost every application relies on and that we rarely treat as such: time.
Take a simple example:
$now = new \DateTimeImmutable();Most of the time, it works fine. Until you try to test the behavior.
Imagine logic that depends on:
- token expiration
- retry delays
- subscription periods
- rate limiting windows
All of these rely on the current time. But since the time is read directly from the system, your tests have no way to control it.
This leads to tests that:
- make vague assertions (“greater than now”)
- rely on small timing differences
- introduce
sleep()just to wait for time to pass
None of these approaches are ideal. Tests become fragile, harder to read, and sometimes flaky.
The root of the problem is simple: time is a dependency, but we rarely treat it like one.
Just like we inject a cache or a logger, we should be able to inject the current time. That’s exactly the idea behind the ClockInterface introduced in PSR-20.
Making time-dependent code testable
Let’s take a simple TokenService service that generates a token with an expiration date.
It’s worth noting that the implementation shown here is intentionally simplified. The goal is to illustrate how to work with
ClockInterfaceandMockClock, not to design a production-ready token system.
readonly class Token
{
public function __construct(
public string $token,
public \DateTimeInterface $expiresAt
) {
}
}
final readonly class TokenService
{
public function generate(): Token
{
$token = \bin2hex(\random_bytes(16));
$expiresAt = new \DateTimeImmutable()->modify('+1 hour');
return new Token($token, $expiresAt);
}
public function isExpired(Token $token): bool
{
return new \DateTimeImmutable() > $token->expiresAt;
}
}This implementation works perfectly in production. However, it introduces a hidden dependency on the system clock. Because the current time is created internally, it becomes harder to control in tests.
Refactoring with ClockInterface
The Symfony Clock Component provides a simple abstraction for time through ClockInterface. By injecting this dependency, the service no longer needs to access the system clock directly.
use Symfony\Component\Clock\ClockInterface;
final readonly class TokenService
{
public function __construct(private ClockInterface $clock)
{
}
public function generate(): Token
{
$token = \bin2hex(\random_bytes(16));
$expiresAt = $this->clock->now()->modify('+1 hour');
return new Token($token, $expiresAt);
}
public function isExpired(Token $token): bool
{
return $this->clock->now() > $token->expiresAt;
}
}This very small change makes the service more flexible and testable! The service no longer decides how time is retrieved, it simply asks the clock. Small, simple and so powerfull!
Testing with MockClock
Ok, now, let’s testing!
When writing tests, we can now replace the real clock with a MockClock from the Symfony Clock Component. This allows us to control the current time explicitly.
use Symfony\Component\Clock\MockClock;
class TokenServiceTest extends TestCase
{
public function testTokenIsStillValidOneSecondBeforeExpiration(): void
{
$clock = new MockClock(new \DateTimeImmutable('2026-09-16 00:55:00'));
$tokenService = new TokenService($clock);
$token = $tokenService->generate();
// Move time forward to 1 second before expiration
$clock->sleep(3599);
$this->assertFalse($service->isExpired($token));
}
public function testTokenIsExpiredOneSecondAfterExpiration(): void
{
$clock = new MockClock(new \DateTimeImmutable('2026-09-16 00:55:00'));
$tokenService = new TokenService($clock);
$token = $tokenService->generate();
// Move time forward past expiration
$clock->sleep(3601);
$this->assertTrue($service->isExpired($token));
}
}By controlling time explicitly with MockClock, we can test scenarios that would otherwise be difficult or unreliable with the real system clock. This makes time-dependent code predictable, fast to test, and easy to reason about.
Conclusion
Handling time correctly in an application is harder than it seems. When time is accessed directly through DateTimeImmutable, it becomes tightly coupled to the system clock, which makes testing more difficult.
The Symfony Clock Component, aligned with the PSR-20 Clock Standard, provides a simple and elegant solution: treat time as a dependency.
By injecting ClockInterface, your services become easier to test and easier to reason about. With tools like MockClock, you can simulate any moment in time without waiting or relying on the real system clock.
The change is small (replacing a direct call to DateTimeImmutable with a clock dependency), but the benefits are significant: cleaner design, more reliable tests, and better control over time-dependent logic.
Sometimes, improving code quality doesn’t require a big architectural change.
Sometimes, it just means taking control of time. ⏱️
Thank you for taking the time to read this article, I hope you enjoyed it and learned something! If so, please feel free to put a clap 👏 on it and any feedback is welcome!
