Imagine this. You push some code, and boom, an unrelated unit test fails in CI: Off by one second. You rerun the tests: boom, fails again. Another rerun: finally, green. You eyeball the logic, and it holds up, so what’s going on?

The most-likely answer: time. The time it takes to set up a fixture, hit a mock (don't), and run a couple of assertions can easily change a "now + 30 seconds" check into "now + 31", and suddenly your "reliable" test is a coin flip. In this post, we'll fix those issues in no time.

Time is a dependency

First, we need a mental shift. You might not have ever realized it, but time is an external dependency. Even though date(), time(), and DateTimeImmutable are natively available in PHP, they still rely on the operating system to retrieve the current time. That makes “now” just as external as a database query or an HTTP request.

And whenever we deal with external services, we don’t hard-wire them. We abstract them. We put a database behind a repository. We put an API behind an interface. That indirection is what makes our code testable, replaceable, and predictable. And especially when it comes to time, predictability is key.

So why should time be any different? Every time you call new DateTimeImmutable('now'), you're coupling to the system clock. You can’t control it in tests, and you can’t swap it out for more granular control.

A minute change, instant impact

To decouple our code from the system time, we need to be able to inject a service that tells the time; we need a Clock. This is where PSR-20: Clock comes in. This simple interface contains only a single method. Here is the entire code:

namespace Psr\Clock;
 
interface ClockInterface
{
/**
* Returns the current time as a DateTimeImmutable object.
*/
public function now(): \DateTimeImmutable;
}

As you can see, this interface really isn't anything special. You can install it in your composer project with composer require psr/clock. (Personally, I don't like the Interface part in the name, which is why I often just create a Clock in my projects. Unless I'm building a public package, in that case, I will use it for interoperability.)

What it allows us to do, though, is special.

Imagine you have this TokenFactory that generates a token that expires 30 minutes from now.

final class TokenFactory
{
public function generateToken(): Token
{
$now = new DateTimeImmutable('now');
 
return new Token(
bin2hex(random_bytes(16)),
$now,
$now->modify('+30 minutes'),
);
}
}

You might test it like this:

$token = $factory->generateToken();
$now = new DateTimeImmutable('now');
assert($token->expires_at->format('c') === $now->modify('+30 minutes')->format('c'));

While this looks innocent enough, this assertion may occasionally fail because of time drift during the execution of the test. $now might be off by a tiny bit from the original time in the Token. Secondly, we are asserting a non-fixed value, because how could we possibly know the exact time?

To have a reliable test, we need to be 100% sure that $now matches the time in the Token exactly, and ideally, we would know the exact time. Watch how we fix this with a Clock.

final class TokenFactory
{
public function __construct(private ClockInterface $clock) {}
 
public function generateToken(): Token
{
$now = $this->clock->now();
 
return new Token(
bin2hex(random_bytes(16)),
$now,
$now->modify('+30 minutes'),
);
}
}

In this example, we inject a ClockInterface, which we use to retrieve the current time. And since we now control the clock, we... control time itself! MUHAHWHAHAHAaa... *ahem* sorry.

Different times, different implementations

Since an Interface isn't an actual implementation, we still need a class to implement and use it. There are two or three types of Clocks that you will need, based on your project requirements.

SystemClock

A SystemClock (or you might call it a WallClock) is the real-time implementation. It will always return the current system time, just like new DateTimeImmutable('now').

final class SystemClock implements ClockInterface {
public function now(): DateTimeImmutable {
return new DateTimeImmutable('now');
}
}

This is the implementation you would use in your production code, or wire up to ClockInterface on your service container. Once you do, your code will behave just like it did before; only now it's testable.

MockClock/FrozenClock/FixedClock

Whatever you want to call it, a MockClock is a clock that lets you control time. This implementation is what you would use in your testing environment. You create an instance, and you set a fixed time. This time is what the now() method will return on every call.

final class MockClock implements ClockInterface {
public function __construct(private DateTimeImmutable $now) {}
 
public function now(): DateTimeImmutable {
return $this->now;
}
 
public function travelTo(DateTimeImmutable $now) {
$this->now = $now;
}
}

Notice how we don't have to return a clone of the date? This is because DateTimeImmutable is, well... immutable, so we can safely reuse the same instance!

Usually, these Clocks have specific helper methods that can change the internal time. This makes testing things that happen over time easier by simply advancing the clock and making another assertion.

$now = new DateTimeImmutable();
$clock = new MockClock($now);
 
$factory = new TokenFactory($clock);
$validator = new TokenValidator($clock);
 
$token = $factory->generateToken();
assert(true === $validator->isValidToken($token));
 
$clock->travelTo($now->modify('+31 minutes'));
assert(false === $validator->isValidToken($token));

MonotonicClock

When it comes to checking elapsed time, the SystemClock may work, but it has a significant drawback: the system time can change. This can happen through a Clock Sync event, the user can change the system time, or Daylight Saving Time (DST) can influence the current time; however, PHP has amazing DST support, so that is very unlikely to cause an issue.

High Resolution Time

So even if it is unlikely to occur, clock changes are still a real problem, especially when you rely on accurate timing. To combat this, PHP 7.3 introduced hrtime(), which gives you a monotonic and high-resolution time that is not affected by changes to the system clock; it will only move forward. Under the hood, it defers to the OS's native monotonic clock.

A good example of a MonotonicClock was implemented in Symfony 6.2. Internally, it uses hrtime() to calculate precise datetimes, which are not affected by time changes.

Why not just use Microtime?

Historically, elapsed time has been measured using microtime(). But since that references the system clock, it is vulnerable to time changes, and can therefore even be negative.

$start = microtime(true);
sleep(2);
$end = microtime(true);
 
echo "Elapsed: " . ($end - $start); // Might be wrong if system time shifts.
 
$start = hrtime(true);
sleep(2);
$end = hrtime(true);
 
echo "Elapsed: " . ($end - $start) / 1_000_000_000; // Always accurate.

Note: The value returned by hrtime() is not a timestamp! It’s a relative internal counter, which is not comparable across systems (or boots). It is meaningless as a clock time. Only use it to measure durations.

Fun fact: sleep() is not influenced by the system time. Meaning it internally references a monotonic clock.

While we are in the zone

PSR-20 doesn’t say anything about time zones, by design. The interface just gives you a DateTimeImmutable. It’s up to you what time zone that datetime comes in.

Personally, I would recommend this:

  • Normalize everything to UTC in your application, because it doesn't shift through daylight saving, so it has no surprises.
  • Convert to local time only at the edges, like: view layer, logs, emails.
  • Be explicit: if your clock returns time in Europe/Amsterdam, say so. Don't let wrong time zones sneak in through date_default_timezone_set().

Note: If you're dealing with future user-specified datetimes (like event scheduling), don't store them in UTC immediately. Store the original local time and its timezone. Convert to UTC at runtime, when the rules are known, to avoid surprises from DST or policy changes.

Before you _at me

You might have read this and had a thought similar to one of the following:

This feels like overengineering!

If you are writing a throwaway script, yes, I agree. Just use new DateTimeImmutable(). But when it comes to production code and real test coverage, I highly recommend using a Clock and treating time like the dependency it is. Your future self will be grateful.

I don’t want to inject a clock everywhere!

Then don’t. Start with where it hurts: the failing tests. Once you see the benefits, you’ll reach for it by default. Having the ClockInterface makes it very clear that the code depends on time, and honestly, with auto-wiring in frameworks like Laravel or Symfony, it's hardly "injecting".

Just use Laravel / Carbon!

Yes, Carbon and Laravel (which uses Carbon under the hood) provide a way to change the time during testing. However, this means coupling your code to yet another dependency. A Clock provides a framework- and tool-agnostic way to make your code more testable.

If you really like Carbon, you can still use it with a Clock by simply wrapping the clock in a Carbon instance:

CarbonImmutable::instance( $clock->now() );

Or, because CarbonImmutable implements DateTimeImmutable, your Clock can directly return a Carbon instance.

I’ve never had this problem!

That is very lucky. Maybe you haven't written or worked with a lot of timing-related components like tokens or rate limiters. Or maybe, time has always been on your side.

The ClockInterface isn't about fixing something that isn't broken; it's a way to prevent things from breaking down the line.

If you haven't run into problems yet, it's a matter of time.