DEV Community

Cover image for Stopping time with PHP
Rubén Rubio for Filmin Engineering

Posted on

Stopping time with PHP

Introduction

It is a good practice to use an interface to manage the clock in an application, as it allows having full control of time. For example, it eases testing, as it lets us define the concrete time for each test. Frank de Jonge and Matthias Noback have blog posts about it, brick has an implementation, and there is even a PSR proposal to have a ClockInterface.

However, there are more use cases for ClockInterface, besides testing. In our case, we had to implement endpoints to show the movies that are just released, and the ones that are about to release in the next few days or weeks.

Filmin coming soon page

Being an API, it is easy to test using PHPUnit, as you could easily set the required date for each test. However, our frontend team had to integrate the website (a Vue app) with this endpoint. This feature is easy to develop and test locally if you have a fresh backup of the production database, as the date of your local system will be in sync with the data within your local database. However, what if you do not have your local database updated, and getting a copy from production takes too much time?

For sure, an option would be to update the local data either by querying directly the database or by using a back-office, if there is one. Nonetheless, this is a manual task that could take some time if you have to update many elements. And it is something that you have to do recurrently if you need to review the implementation after the date you set up passes.

But there is another option: we can take advantage of the ClockInterface to fixate the date of the whole system.

Implementation

Having a ClockInterface such as:

interface ClockInterface
{
    public function now(): DateTimeImmutable;
}
Enter fullscreen mode Exit fullscreen mode

We could have this FixedClock implementation:

use DateTimeImmutable;
use DateTimeZone;
use Exception;
use InvalidArgumentException;

final class FixedClock implements ClockInterface
{
    public const FORMAT = 'Y-m-d H:i:s';

    private ?DateTimeImmutable $dateTime;
    private DateTimeZone $timeZone;

    public function __construct(string $timeZone, string $dateTime = '')
    {
        $this->timeZone = $this->parseAndValidateTimeZone($timeZone);
        $this->dateTime = $this->parseAndValidateDateTime($dateTime);
    }

    public function now(): DateTimeImmutable
    {
        if ($this->dateTime !== null) {
            return $this->dateTime;
        }

        return new DateTimeImmutable('now', $this->timeZone);
    }

    private function parseAndValidateTimeZone(string $timeZone): DateTimeZone
    {
        try {
            return new DateTimeZone($timeZone);
        } catch (Exception) {
            throw new InvalidArgumentException(
                sprintf('Value "%s" for time zone is not valid.', $timeZone)
            );
        }
    }

    private function parseAndValidateDateTime(string $dateTime): ?DateTimeImmutable
    {
        $parsedDateTime = trim($dateTime);

        if ($parsedDateTime === '') {
            return null;
        }

        $dateTimeImmutable = DateTimeImmutable::createFromFormat(
            self::FORMAT,
            $parsedDateTime,
            $this->timeZone
        );

        if (! $dateTimeImmutable) {
            throw new InvalidArgumentException(
                sprintf(
                    'Value "%s" for date time is not valid. Expected to have the format "%s"',
                    $dateTime,
                    self::FORMAT
                )
            );
        }

        return $dateTimeImmutable;
    }
}

Enter fullscreen mode Exit fullscreen mode

It is not a good practice to perform anything besides assign properties in the constructor, but it will prove useful afterwards.

Configuration

Symfony

We only need to set the fixed clock up for our development environment. By default, Symfony loads the services.yaml file and then the services_{environment.yaml}, so you can override any service on per-environment basis. To do so, we add to our services_dev.yaml the following lines:

parameters:
    timezone: '%env(string:TIMEZONE)%'
    now: '%env(string:NOW)%'

services:
    _defaults:
        autowire: true
        autoconfigure: true

    Filmin\SharedKernel\Domain\ValueObject\Time\FixedClock:
        arguments:
            - '%timezone%'
            - '%now%'

    Filmin\SharedKernel\Domain\ValueObject\Time\ClockInterface: '@Filmin\SharedKernel\Domain\ValueObject\Time\FixedClock'

Enter fullscreen mode Exit fullscreen mode

What we do here is:

  • Lines 1-3: set up container parameters that contain the timezone and the current time from environment variables.
  • Lines 9-12: setup our FixedClock implementation as a service. In Symfony, we can not use named constructors when defining value objects as services. Thereby, we validated the values within the constructor, as shown above.
  • Line 14: alias the ClockInterface to our FixedClock, so any service that type hint a ClockInterface will use the FixedClock implementation, thus overriding any previous, generic definition.

If we do not set up any value for the environment variable NOW, the application will use the system time. Therefore, switching between a fixed time and the system time is as easy as to update an environment variable.

Laravel

In a Laravel application, we would configure the ClockInterface within a service provider using a singleton, such as:

$this->app->singleton(ClockInterface::class, function (Application $app) {
    if ($app->environment('local')) {
        return new FixedClock(getenv('TIMEZONE'), getenv('NOW'));
    }

    return new SystemClock();
});

Enter fullscreen mode Exit fullscreen mode

In this case, SystemClock is an implementation of ClockInterface that uses the system time, that is what we want in production. As with the previous case, switching between the system time and a fixed time is as easy as to update an environment variable.

Note that we are not experts in Laravel, so this configuration could probably be improved.

Conclusion

We saw that using a ClockInterface eases the testing process and allow fixating the time of the whole application, that can be useful for local testing purposes.

If you are implementing a new application, it is simple to start using ClockInterface everywhere. However, if you are working on legacy code, it could take some time until you have the ClockInterface in your whole application. In those cases, it could be useful to apply the boy scout rule: "Always leave the code better than you found it".

Discussion (0)