A few months ago a new standard was proposed to PSR and then accepted. Its name is PSR-20: Clock
.
And here is a short introduction from that document:
Creating a standard way of accessing the clock would allow interoperability during testing, when testing behavior that has timing-based side effects. Common ways to get the current time include calling
\time()
ornew \DateTimeImmutable('now')
. However, this makes mocking the current time impossible in some situations.
Basically, it explains everything you need to know. I came across an extremely similar solution on my previous work. “Great minds think alike.”
The solution is really simple. Just look at the proposed interface:
namespace Psr\Clock;
interface ClockInterface {
/** * Returns the current time as a DateTimeImmutable Object */
public function now(): \DateTimeImmutable;
}
Let’s briefly consider how to use it in our “application”.
First, we need to implement that interface:
namespace ExampleApp\Clock;
use Psr\Clock\ClockInterface;
class DateTimeClock implements {
public function now(): \DateTimeImmutable {
return new DateTimeImmutable();
}
}
And now we can use it in our application code:
namespace ExampleApp\Log;
use ExampleApp\Clock\DateTimeClock;
use Psr\Clock\ClockInterface;
class Entry {
public function __construct(
public \DateTimeImmutable $time,
public string $message
) {}
}
class Logger {
public function __construct(private ClockInterface $clock) {}
public function createEntry(string $message): Entry {
return new Entry(
$this->clock->now(),
$message
);
}
}
class LoggerFactory {
public function __invoke(): Logger {
return new Logger(
new DateTimeClock()
);
}
}
Without ClockInterface
we would probably just create a new instance directly: new \DateTimeImmutable()
. And that would be really hard to test. Tests for calls to system API become tricky and flaky. But with the new interface, they become much easier and stable:
namespace ExampleAppTest\Log;
use ExampleApp\Log\Logger;
use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
class LoggerTest extends TestCase {
public function testCreateEntry_MessageGiven_ReturnsMatchingEntry(): void {
$clock = $this->createMock(ClockInterface::class);
$clock
->method('now')
->willReturn(
new \DateTimeImmutable('@1')
);
$logger = new Logger($clock);
$entry = $logger->createEntry('a');
$expected = [
'time' => 1,
'message' => 'a',
];
self::assertSame($expected, $this->exportEntry($entry));
}
public function exportEntry(Entry $entry): array {
return [
'time' => $entry->time->getTimestamp(),
'message' => $entry->message,
];
}
}
Just imagine how you’re trying to catch a second or testing the period without this interface and tests fail in CI, because they run too slow or for any other reason. Probably you decide not to test time then and at some point it appears that a bug was introduced exactly in this place.
I suggest considering using this new interface and make your life just a bit happier.