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.
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;
}
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;
}
}
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'
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 ourFixedClock
, so any service that type hint aClockInterface
will use theFixedClock
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();
});
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".
Top comments (0)