Being in control of time in PHP

When developers talk about the infrastructural boundaries or external dependencies they often talk about databases and third-party integration. They're the first thing we'll put behind an interface so we can stub them out during our tests. This gives us some control over them. It's become relatively easy to spot these dependencies because we do it frequently. They're the usual suspects.

However, some "dependencies" are much harder to spot. They even live right inside the standard library of PHP and often manage to seep through the cracks. Date/Time handling is such a thing. So what's the problem and how do we fix it?


Dealing with time in software.

Dealing with dates and time in any language is difficult. It's not an exact science, it's clunky, riddled with exceptions. Don't even get me started on timezones. Nevertheless, date/time handling is a very common task. We use it for logging purposes, we check it, format it, and filter or group by it.

A lot of the problems we solve using code involve some kind of usage of time. But more often than not it's not treated the same way we treat our other external dependencies, and that can be problematic. The problem is that time isn't fixed, it's always moving forward, and it's not just that. Time is not predictable.

There's the leap year. There's the leap second, which is a one-second adjustment that is occasionally applied to Coordinated Universal Time. A second isn't even 1/60 of a minute but rather the duration of 9,192,631,770 "oscillations" of a caesium 133 atom, whatever that means.

Time is out of control.

When time gets the best of you, be more specific.

Every PHP developer has had a project where sometimes tests would fail. They wouldn't always fail. After hours of digging through your code you spot a common theme. The tests always fail at midnight. 😱

In cases like these the probable cause is date/time precision. For example, if something needs to expire in 14 days, what does that mean? Does that mean it expires at the beginning of the day? Does it expire at the same time the expiration date is created but then 14 days later? Does it mean the invitation is valid for the entire day of the 13th day? Is the expiration date inclusive or exclusive? The answers to these questions determine how we implement the solution.

$expirationDate = (new DateTimeImmutable())
    ->modify('+14 days')
    ->setTime(0, 0, 0);

Using setTime you can make these cases more precise. It adds clarity and guides future-you (or your colleague) when the code needs to change. This also gets rid of some flakiness, but it still has some room to improve.

The example above uses DateTimeImmutable instead of the regular DateTime class. This adds an additional layer of predictability, especially when you're passing objects around your application.

While we're very much aware our programs take time to execute, the fact that time passes by is often overlooked. If our tests starts to run at 2022-01-01 23:59:59.999999 and the next line we set the expected date to now + 14 days we have a problem. The start time and expected time are not what we expect, they'll actually be 15 days apart.

For this problem we'll need a different approach.

Getting grips with time.

We can't control time. What we can do is control how we consume time. Much like how we consume our other dependencies, we can place time behind an interface. An interface as such would look like the following:

interface Clock
{
    public function now(): DateTimeImmutable
}

Now our production code can rely on an actual clock, we'll call it the SystemClock:

class SystemClock implements Clock
{
    public function now(): DateTimeImmutable
    {
        return new DateTimeImmutable();
    }
}

Our system can now use this implementation when it needs to know the current time.

Testing with time.

Let's say we're creating an invitation system and invites expire after a period of time. How can we test this?

Let's look at the service we want to test:

class InviteService
{
    public function __construct(Clock $clock)
    {
        $this->clock = $clock;
    }

    public function issueInvite(): Invite
    {
        $timeOfExpiration = $this->clock
            ->now()
            ->modify('+14 days')
            ->setTime(23, 59, 59);

        return new Invite($timeOfExpiration);
    }

    public function accept(Invite $invite)
    {
        if ($invite->timeOfExpiration() <= $this->clock->now()) {
            throw new DomainException("Invitation Expired!");
        }

        // handle accepting the invite...
    }
}

For testing we can create an alternate implementation of the Clock:

<?php

class TestClock implements Clock
{
    private $fixedNow;

    public function __construct(DateTimeImmutable $now = null)
    {
        $this->fixedNow = $now ?: new DateTimeImmutable();
    }

    public function tick(DateTimeImmutable $now = null)
    {
        $this->fixedNow = $now ?: new DateTimeImmutable();
    }

    public function now(): DateTimeImmutable
    {
        return $this->fixedNow;
    }
}

The TestClock has a tick method which allows us to set the time. This mean's we're now in full control, and we can test the full component with ease!

<?php

use PHPUnit\Framework\TestCase;

class InviteService extends TestCase;
{
    private $clock;
    private $invites;
    
    public function setUp()
    {
        $this->clock = new TestClock();
        $this->invites = new InviteService();
    }

    public function testAcceptingAnExpriredInvite()
    {
        $this->expectException(LogicException::class);

        $invite = $this->invites->issueInvite();

        // Move time forward
        $this->clock->tick(
                $this->clock->now()
                    ->modify('+15 days')
                    ->setTime(0, 0, 0)
        );

        $this->invites->accept($invite);
    }
}

Consuming time as a service.

In this example I've shown an alternate way of dealing with time. Is this the only way to do it? No. Should you always do it like this? Absolutely not. However, I hope this illustrates there's more than one way of dealing with time. In general it doesn't matter what clock you use, they all do pretty much the same thing. What's important is that you use one if you need one.

If you want to use a clock in your PHP project you can create your own clock or use a package like lcobucci/clock. Don't like it? Just search for one on Packagist!

You can also use tools to make handling time easier. The league/period package provides a solid (immutable) implementation for dealing with time ranges.

Want to read some more about handling time? Check out this article by Ross Tuck's about it. I'm personally not a fan of mocking, but if/when you are you might also be interested in this article from Matthias Noback.


Photo by JMortonPhoto.com & OtoGodfrey.com, CC BY-SA 4.0

Subscribe for updates

Get the latest posts delivered right to your inbox.

Sign up