My friends and family are under attack in Ukraine.
Donate to protect them directly or help international organizations.

Organizing Miscellaneous Utility Functions

June 26th, 2021

Let's say that your application needs to frequently calculate age from a birthdate. It would be very easy to create a DateUtil::ageFromDob. However, with this approach, you'll end up with Util classes all over the place, that will often have multiple similar functions, depending on whether your birthdate is a DateTime or a string.

Instead of passing raw data to various static methods, I recommend creating a specialized class for the DateOfBirth itself. Let's start simple:

final class DateOfBirth
{
    private DateTimeImmutable $date;

    public function __construct(DateTimeImmutable $date)
    {
        $this->date = $date;
    }

    public function getDate(): DateTimeImmutable
    {
        return $this->date;
    }

    public function getAge(): int
    {
        return (new DateTimeImmutable())
            ->diff($this->date)
            ->y;
    }
}

From the same object, we can get both the date and the age. We would use the object like this:

$date = new DateTimeImmutable('1985-09-09');
$dateOfBirth = new DateOfBirth($date);
echo $dateOfBirth->getAge();
// 35

So now instead of passing a date around, we can pass a DateOfBirth object that is capable of calculating its own age. No need for additional Util classes.

To make this object easier to use with strings, we can give ourselves a named constructor:

public static function fromString(string $value): self
{
    return new self(DateTimeImmutable::createFromFormat('Y-m-d', $value));
}

This allows us to instantiate like this:

$dateOfBirth = DateOfBirth::fromString('1985-09-09');
echo $dateOfBirth->getAge();

Alright, but what if I need to also calculate the person's age on a different date? Let's change the method to getAgeOn, and provide a reference date:

public function getAgeOn(DateTimeImmutable $referenceDate): int
{
    return $referenceDate
        ->diff($this->date)
        ->y;
}

We can now check the person's age today, or at the time they graduated from college:

echo $dateOfBirth->getAgeOn(new DateTimeImmutable());
// 35
echo $dateOfBirth->getAgeOn($graduationDate);
// 20

This also makes the tests more repeatable, as we are in control of all the dates:

final class DateOfBirthTest extends \PHPUnit\Framework\TestCase
{
    public function testGetAge_WithBirthdate18MonthsAgo_Return1()
    {
        $dateOfBirth = DateOfBirth::fromString('2000-01-01');
        $referenceDate = new DateTimeImmutable('2001-07-01');

        self::assertEquals(
            1,
            $dateOfBirth->getAgeOn($referenceDate)
        );
    }
}

This approach saves you a whole lof of Util functions. It's also more testable, your code is more organized, and it reads like English:

$dateOfBirth->getAgeOn($graduationDate);
// Given a date of birth, get their age on graduation date

We can crank it up a notch and centralize the creation of today's date. I once worked on an application that had 6 different ways to create a new date; some didn't respect the timezone. I usually do this with a simple Clock service:

interface Clock
{
    public function now(): DateTimeImmutable;
}

final class DateTimeClock implements Clock
{
    public function now(): DateTimeImmutable
    {
        return new DateTimeImmutable(
            'now', 
            new DateTimeZone('UTC')
        );
    }
}

In automated tests, we can mock the Clock interface. Here is a simplified example of a real life test that failed when the expiry date of the card arrived in 2020. Refactoring the code to use the clock service solved the problem.

final class CreditCardValidatorTest extends \PHPUnit\Framework\TestCase
{
    public function testComplexObject()
    {
        // ...
        $object = new CreditCardValidator($this->clockMock);
        $object->validate($creditCard);
        // ...
    }
}

Previous: Code is Cheap, Knowledge is Expensive Next: Should Repositories Only Return Aggregates?