skip to content

In PHP, why inject a PSR-20 ClockInterface instead of calling time() or new DateTimeImmutable(), and how does that make a scheduler testable?

level: middleimportance: should knowfreq 38%

answer

  1. the hidden dependency on now
  2. Psr\Clock\ClockInterface::now()
  3. returns DateTimeImmutable, nothing else
  4. a frozen clock in tests
  5. timezone left to the implementation

basics

~20 s

time() and new DateTimeImmutable() read the real clock, so tests cannot control the current time. PSR-20's ClockInterface::now() returns a DateTimeImmutable from an injected clock: production passes a system clock, tests pass a frozen or adjustable one.

solid answer

~40 s

Code that calls `time()` or `new \DateTimeImmutable('now')` has a hidden dependency on the real clock: a test cannot say "it is 08:59" and then "it is 09:01", so it either sleeps, becomes flaky near boundaries, or is not written. PSR-20 standardises the seam: `Psr\Clock\ClockInterface` has one method, `now(): \DateTimeImmutable`, and the current Unix timestamp is `$clock->now()->getTimestamp()`. A reminder scheduler takes a `ClockInterface` in its constructor and asks it for the time wherever it would have called `time()`. Production wires a system clock returning `new \DateTimeImmutable()`; tests wire a frozen clock set to a fixed instant, and an adjustable one to move time forward between assertions. PSR-20 fixes no timezone, leaving it to the implementation, and defines no `sleep()` or waiting method.

code

php · 19 lines
php
<?php
declare(strict_types=1);

use Psr\Clock\ClockInterface;

final class ReminderScheduler
{
    public function __construct(private ClockInterface $clock) {}

    /** @param list<Reminder> $reminders @return list<Reminder> */
    public function due(array $reminders): array
    {
        $now = $this->clock->now(); // read once per decision
        return array_values(array_filter(
            $reminders,
            fn (Reminder $r): bool => $r->dueAt <= $now,
        ));
    }
}

go deeper

for a junior

Remember that PSR-20's ClockInterface has one method, now(), returning a DateTimeImmutable, and that injecting it lets tests choose the time.

for a middle

Explain why time() hides a dependency, how system, frozen and adjustable clocks differ, and why the timezone is left to the implementation.

for a senior

Remove direct clock reads from time-sensitive code, read now() once per decision, and settle a codebase-wide timezone policy for clocks.

for a principal

Decide how time is modelled across services, where clocks are injected, how skew between hosts is tolerated and which component owns timezone conversion.

## The problem: time as a hidden dependency Any code that decides something based on the current time, whether a reminder is due, a token has expired or a cache entry is stale, needs to know "now". The usual ways to get it in PHP are `time()` and `new \DateTimeImmutable('now')`. Both read the real system clock at the moment of the call. The PSR-20 specification says it directly: this makes mocking the current time impossible in some situations. For a test, that means: - it cannot fix the time, so assertions near a boundary, for example "due at 09:00", are flaky; - it cannot move time forward without actually waiting; - the dependency does not appear in any constructor, so nothing reminds you it exists. ## What PSR-20 defines **PSR-20** has one interface in the `Psr\Clock` namespace: ```php interface ClockInterface { public function now(): \DateTimeImmutable; } ``` - `now()` **must** return a `\DateTimeImmutable`. - To get a Unix timestamp, call `getTimestamp()` on the result; there is no separate timestamp method, because everything is available from the returned object. - The **timezone** of the returned object is an implementation detail. The meta document explains that PSR-20 does not force UTC; the implementation, and so the application, decides. - **Scheduling** methods such as `sleep()` or `wait()` are out of scope; the interface only reads the time. ## The scheduler, before and after | | Reads time with | Test can fix "now" | Dependency visible | |---|---|---|---| | Before | `new \DateTimeImmutable()` inside `due()` | no | no | | After | `$this->clock->now()` | yes, inject a frozen clock | yes, in the constructor | After the change the scheduler's constructor declares `ClockInterface $clock`, and `due()` compares each reminder's due time with `$this->clock->now()`. Nothing else about the logic changes. ## Clock implementations The PSR-20 meta document sketches two: 1. a **system clock**, whose `now()` returns `new \DateTimeImmutable()`; this is what production wires in; 2. a **frozen clock**, constructed with a fixed `\DateTimeImmutable` and always returning it. Tests often add an **adjustable** clock with a method that moves the stored instant forward, for example by calling `modify('+2 minutes')` on it. Because `\DateTimeImmutable` methods return new objects, the stored instant is replaced, never mutated in place. ## Writing the test 1. Create a clock frozen at 08:59 on a known date, in an explicit timezone. 2. Build the scheduler with it and assert that a 09:00 reminder is **not** due. 3. Advance the clock by two minutes. 4. Assert that the reminder **is** due now. No `sleep()`, no dependence on when the test suite runs, and the boundary is tested exactly. ## Timezone discipline Since PSR-20 leaves the timezone to the implementation, a codebase should settle it once: - the system clock returns times in a known zone, often UTC, set explicitly rather than inherited from `date.timezone`; - test clocks are built with an explicit zone too; - conversion to a user's local time happens at the edges, with `setTimezone()` on the returned object. ## Pitfalls - Calling `$this->clock->now()` several times in one decision: each call can return a different instant with a system clock. Read it once and reuse the value. - Leaving a stray `time()` or `date()` call somewhere in the class, which quietly bypasses the injected clock. - Making the clock a global static so tests can override it, which brings back hidden, shared state.

  • Why does PSR-20 not guarantee that now() returns UTC?
    The meta document treats the timezone as an implementation detail: the interface can only enforce the `\DateTimeImmutable` type, and forcing UTC would make callers who need another zone work around it. The application chooses, either through an implementation that always returns a known zone or by calling `setTimezone()` on the result.
  • What goes wrong if a method calls $this->clock->now() three times while deciding one thing?
    With a system clock, each call returns the instant at which it was made, so the three values can differ, and a decision near a boundary can use inconsistent times. Read `now()` once at the start of the decision, keep it in a variable and compare everything against that value.

Injecting a clock is like a film set's clock prop instead of a real wall clock: the director sets it to 8:59 for one scene and 9:01 for the next, and the actors react exactly as they would to the real time.

saying these in an interview costs you the question

  • Tests time-dependent code by calling sleep() until the boundary passes.
  • Believes PSR-20 requires now() to return UTC.
  • Expects ClockInterface to provide sleep() or a timestamp method.
  • Overrides the time through a global static that tests reset.
  • Calls now() repeatedly within one decision and compares the results.