The Clock Component
The Clock component decouples applications from the system clock. This allows you to fix time to improve testability of time-sensitive logic.
The component provides a ClockInterface
with the following implementations
for different use cases:
- NativeClock
-
Provides a way to interact with the system clock, this is the same as doing
new \DateTimeImmutable()
. - MockClock
-
Commonly used in tests as a replacement for the
NativeClock
to be able to freeze and change the current time using eithersleep()
ormodify()
. - MonotonicClock
-
Relies on
hrtime()
and provides a high resolution, monotonic clock, when you need a precise stopwatch.
Installation
1
$ composer require symfony/clock
Note
If you install this component outside of a Symfony application, you must
require the vendor/autoload.php
file in your code to enable the class
autoloading mechanism provided by Composer. Read
this article for more details.
Usage
The Clock class returns the current time and allows to use any PSR-20 compatible implementation as a global clock in your application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\MockClock;
// by default, Clock uses the NativeClock implementation, but you can change
// this by setting any other implementation
Clock::set(new MockClock());
// Then, you can get the clock instance
$clock = Clock::get();
// Additionally, you can set a timezone
$clock->withTimeZone('Europe/Paris');
// From here, you can get the current time
$now = $clock->now();
// And sleep for any number of seconds
$clock->sleep(2.5);
The Clock component also provides the now()
function:
1 2 3 4
use function Symfony\Component\Clock\now;
// Get the current time as a DatePoint instance
$now = now();
The now()
function takes an optional modifier
argument
which will be applied to the current time:
1 2 3
$later = now('+3 hours');
$yesterday = now('-1 day');
You can use any string accepted by the DateTime constructor.
Later on this page you can learn how to use this clock in your services and tests. When using the Clock component, you manipulate DatePoint instances. You can learn more about it in the dedicated section.
Available Clocks Implementations
The Clock component provides some ready-to-use implementations of the ClockInterface, which you can use as global clocks in your application depending on your needs.
NativeClock
A clock service replaces creating a new DateTime
or
DateTimeImmutable
object for the current time. Instead, you inject the
ClockInterface
and call now()
. By default, your application will likely
use a NativeClock
, which always returns the current system time. In tests it is replaced with a MockClock
.
The following example introduces a service utilizing the Clock component to determine the current time:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Symfony\Component\Clock\ClockInterface;
class ExpirationChecker
{
public function __construct(
private ClockInterface $clock
) {}
public function isExpired(DateTimeInterface $validUntil): bool
{
return $this->clock->now() > $validUntil;
}
}
MockClock
The MockClock
is instantiated with a time and does not move forward on its own. The time is
fixed until sleep()
or modify()
are called. This gives you full control over what your code
assumes is the current time.
When writing a test for this service, you can check both cases where something is expired or not, by modifying the clock's time:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
use PHPUnit\Framework\TestCase;
use Symfony\Component\Clock\MockClock;
class ExpirationCheckerTest extends TestCase
{
public function testIsExpired(): void
{
$clock = new MockClock('2022-11-16 15:20:00');
$expirationChecker = new ExpirationChecker($clock);
$validUntil = new DateTimeImmutable('2022-11-16 15:25:00');
// $validUntil is in the future, so it is not expired
$this->assertFalse($expirationChecker->isExpired($validUntil));
// Clock sleeps for 10 minutes, so now is '2022-11-16 15:30:00'
$clock->sleep(600); // Instantly changes time as if we waited for 10 minutes (600 seconds)
// modify the clock, accepts all formats supported by DateTimeImmutable::modify()
$this->assertTrue($expirationChecker->isExpired($validUntil));
$clock->modify('2022-11-16 15:00:00');
// $validUntil is in the future again, so it is no longer expired
$this->assertFalse($expirationChecker->isExpired($validUntil));
}
}
Monotonic Clock
The MonotonicClock
allows you to implement a precise stopwatch; depending on
the system up to nanosecond precision. It can be used to measure the elapsed
time between two calls without being affected by inconsistencies sometimes introduced
by the system clock, e.g. by updating it. Instead, it consistently increases time,
making it especially useful for measuring performance.
Using a Clock inside a Service
Using the Clock component in your services to retrieve the current time makes
them easier to test. For example, by using the MockClock
implementation as
the default one during tests, you will have full control to set the "current time"
to any arbitrary date/time.
In order to use this component in your services, make their classes use the
ClockAwareTrait. Thanks to
service autoconfiguration, the setClock()
method
of the trait will automatically be called by the service container.
You can now call the $this->now()
method to get the current time:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
namespace App\TimeUtils;
use Symfony\Component\Clock\ClockAwareTrait;
class MonthSensitive
{
use ClockAwareTrait;
public function isWinterMonth(): bool
{
$now = $this->now();
return match ($now->format('F')) {
'December', 'January', 'February', 'March' => true,
default => false,
};
}
}
Thanks to the ClockAwareTrait
, and by using the MockClock
implementation,
you can set the current time arbitrarily without having to change your service code.
This will help you test every case of your method without the need of actually
being in a month or another.
The DatePoint
Class
The Clock component uses a special DatePoint
class. This is a small wrapper on top of PHP's DateTimeImmutable.
You can use it seamlessly everywhere a DateTimeImmutable or
DateTimeInterface is expected. The DatePoint
object fetches the
date and time from the Clock class. This means
that if you did any changes to the clock as stated in the
usage section, it will be reflected when creating a new
DatePoint
. You can also create a new DatePoint
instance directly, for
instance when using it as a default value:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\Clock\DatePoint;
class Post
{
public function __construct(
// ...
private \DateTimeImmutable $createdAt = new DatePoint(),
) {
}
}
The constructor also allows setting a timezone or custom referenced date:
1 2 3 4 5 6
// you can specify a timezone
$withTimezone = new DatePoint(timezone: new \DateTimezone('UTC'));
// you can also create a DatePoint from a reference date
$referenceDate = new \DateTimeImmutable();
$relativeDate = new DatePoint('+1month', reference: $referenceDate);
The DatePoint
class also provides a named constructor to create dates from
timestamps:
1 2 3 4 5 6 7
$dateOfFirstCommitToSymfonyProject = DatePoint::createFromTimestamp(1129645656);
// equivalent to:
// $dateOfFirstCommitToSymfonyProject = (new \DateTimeImmutable())->setTimestamp(1129645656);
// negative timestamps (for dates before January 1, 1970) and float timestamps
// (for high precision sub-second datetimes) are also supported
$dateOfFirstMoonLanding = DatePoint::createFromTimestamp(-14182940);
7.1
The createFromTimestamp()
method was introduced in Symfony 7.1.
Note
In addition DatePoint
offers stricter return types and provides consistent
error handling across versions of PHP, thanks to polyfilling PHP 8.3's behavior
on the topic.
DatePoint
also allows to set and get the microsecond part of the date and time:
1 2 3
$datePoint = new DatePoint();
$datePoint->setMicrosecond(345);
$microseconds = $datePoint->getMicrosecond();
Note
This feature polyfills PHP 8.4's behavior on the topic, as microseconds manipulation is not available in previous versions of PHP.
7.1
The setMicrosecond() and getMicrosecond() methods were introduced in Symfony 7.1.
Writing Time-Sensitive Tests
The Clock component provides another trait, called ClockSensitiveTrait, to help you write time-sensitive tests. This trait provides methods to freeze time and restore the global clock after each test.
Use the ClockSensitiveTrait::mockTime()
method to interact with the mocked
clock in your tests. This method accepts different types as its only argument:
- A string, which can be a date to set the clock at (e.g.
1996-07-01
) or an interval to modify the clock (e.g.+2 days
); - A
DateTimeImmutable
to set the clock at; - A boolean, to freeze or restore the global clock.
Let's say you want to test the method MonthSensitive::isWinterMonth()
of the
above example. This is how you can write that test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
namespace App\Tests\TimeUtils;
use App\TimeUtils\MonthSensitive;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Clock\Test\ClockSensitiveTrait;
class MonthSensitiveTest extends TestCase
{
use ClockSensitiveTrait;
public function testIsWinterMonth(): void
{
$clock = static::mockTime(new \DateTimeImmutable('2022-03-02'));
$monthSensitive = new MonthSensitive();
$monthSensitive->setClock($clock);
$this->assertTrue($monthSensitive->isWinterMonth());
}
public function testIsNotWinterMonth(): void
{
$clock = static::mockTime(new \DateTimeImmutable('2023-06-02'));
$monthSensitive = new MonthSensitive();
$monthSensitive->setClock($clock);
$this->assertFalse($monthSensitive->isWinterMonth());
}
}
This test will behave the same no matter which time of the year you run it. By combining the ClockAwareTrait and ClockSensitiveTrait, you have full control on your time-sensitive code's behavior.
Exceptions Management
The Clock component takes full advantage of some PHP DateTime exceptions.
If you pass an invalid string to the clock (e.g. when creating a clock or
modifying a MockClock
) you'll get a DateMalformedStringException
. If you
pass an invalid timezone, you'll get a DateInvalidTimeZoneException
:
1 2 3 4 5 6 7
$userInput = 'invalid timezone';
try {
$clock = Clock::get()->withTimeZone($userInput);
} catch (\DateInvalidTimeZoneException $exception) {
// ...
}
These exceptions are available starting from PHP 8.3. However, thanks to the symfony/polyfill-php83 dependency required by the Clock component, you can use them even if your project doesn't use PHP 8.3 yet.