Stop new \DateTime(): treat time as a dependency you inject
There's a line of code that shows up in almost every Drupal module and quietly makes it hard to test: time(), or new \DateTime(), or \Drupal::time()->getRequestTime() buried inside a method. Each one reaches out and grabs the real, moving "now" from the outside world. And the moment your logic depends on the real clock, your tests do too — which is how you end up writing sleep(2) in a test and hating your life. The fix is an old idea that Drupal and Symfony both support: treat time as a dependency you inject, not a global you grab.
The problem with grabbing "now"
Say you have a token-expiry check: is this token past its expiry? If the method calls time() directly, there is no way to test the boundary without either waiting for real seconds to pass or mocking PHP's built-in functions (don't). The behaviour you want to verify — "expired one second ago" versus "expires in an hour" — is entangled with the actual wall clock. Tests become slow, flaky, or both, and you write fewer of them.
Drupal's time service
Drupal already gives you the seam. There's a service, datetime.time, implementing Drupal\Component\Datetime\TimeInterface — getRequestTime(), getCurrentTime(), and their microsecond variants. \Drupal::time() is just the static wrapper around that same object. Inject the service instead of calling the static, and "now" becomes something you control in a test:
# my_module.services.yml
services:
my_module.token_checker:
class: Drupal\my_module\TokenChecker
arguments: ['@datetime.time']
<?php
declare(strict_types=1);
namespace Drupal\my_module;
use Drupal\Component\Datetime\TimeInterface;
final class TokenChecker {
public function __construct(
private readonly TimeInterface $time,
) {}
public function isExpired(int $expiresAt): bool {
return $expiresAt <= $this->time->getCurrentTime();
}
}
Now the test controls the clock — no waiting, no flakiness:
<?php
declare(strict_types=1);
namespace Drupal\Tests\my_module\Unit;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\my_module\TokenChecker;
use Drupal\Tests\UnitTestCase;
final class TokenCheckerTest extends UnitTestCase {
public function testIsExpired(): void {
$now = 1668611000; // frozen "now"
$time = $this->createMock(TimeInterface::class);
$time->method('getCurrentTime')->willReturn($now);
$checker = new TokenChecker($time);
$this->assertTrue($checker->isExpired($now - 1));
$this->assertFalse($checker->isExpired($now + 3600));
}
}
Where Drupal's version falls short
Here's the honest gap, because I don't want to oversell the built-in. Drupal's TimeInterface hands you back plain integers and floats — never a DateTimeImmutable — and it has no built-in "freeze" or "advance" mechanism. You mock it by hand, per test, as above. It also does not implement the PSR-20 clock standard. Core has looked at adopting a proper PSR-20 clock and, as of now, punted (the issue is postponed). So it's a real seam, but a basic one.
Symfony's Clock, when you want more
Symfony's Clock component is the fuller version of the same idea, and you can pull it into a Drupal project yourself (it's already a dependency of things like the Scheduler component, so it's well-worn). It gives you ClockInterface with a now() that returns a real DateTimeImmutable, a NativeClock for production, and a MockClock for tests that can freeze time and advance it instantly:
<?php
use Symfony\Component\Clock\MockClock;
$clock = new MockClock('2022-11-16 15:20:00');
$checker = new ExpirationChecker($clock); // injects ClockInterface
$validUntil = new \DateTimeImmutable('2022-11-16 15:25:00');
$this->assertFalse($checker->isExpired($validUntil));
$clock->sleep(600); // +10 minutes, instantly
$this->assertTrue($checker->isExpired($validUntil));
That $clock->sleep(600) advances frozen time by ten minutes with no real wait — exactly the thing you can't do cleanly with Drupal's bare interface.
Every new \DateTime() is a little hardcoded dependency on the one thing you can't pause: the real world. Inject the clock and the world holds still while you test.
The rule I follow
Never call time(), new \DateTime(), or \Drupal::time() inside logic you'd want to test. Inject TimeInterface for ordinary Drupal code — it's already there, no new dependency — and reach for Symfony's ClockInterface when you want a mockable DateTimeImmutable and freeze/advance in tests. It's a tiny discipline that pays off the first time you need to test "what happens at exactly midnight" without staying up until midnight to find out.