时钟组件
时钟组件将应用程序与系统时钟解耦。这允许你固定时间,以提高时间敏感逻辑的可测试性。
该组件提供了一个 ClockInterface
,针对不同的用例有以下实现:
- NativeClock
- 提供了一种与系统时钟交互的方式,这与执行
new \DateTimeImmutable()
相同。 - MockClock
- 通常在测试中用作
NativeClock
的替代品,以便能够使用sleep()
或modify()
冻结和更改当前时间。 - Monotonic Clock
- 依赖于
hrtime()
,并在你需要精确的秒表时提供高分辨率的单调时钟。
安装
1
$ composer require symfony/clock
注意
如果你在 Symfony 应用程序之外安装此组件,则必须在代码中 require vendor/autoload.php
文件,以启用 Composer 提供的类自动加载机制。阅读这篇文章以获取更多详细信息。
使用方法
Clock 类返回当前时间,并允许在你的应用程序中使用任何 PSR-20 兼容的实现作为全局时钟。
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);
时钟组件还提供了 now()
函数
1 2 3 4
use function Symfony\Component\Clock\now;
// Get the current time as a DatePoint instance
$now = now();
now()
函数接受一个可选的 modifier
参数,该参数将应用于当前时间
1 2 3
$later = now('+3 hours');
$yesterday = now('-1 day');
你可以使用 DateTime 构造函数接受的任何字符串。
在本页的后面,你可以学习如何在你的服务和测试中使用这个时钟。当使用时钟组件时,你操作的是 DatePoint 实例。你可以在专用章节中了解更多信息。
可用的时钟实现
时钟组件提供了一些 ClockInterface 的即用型实现,你可以根据你的需求在你的应用程序中用作全局时钟。
NativeClock
时钟服务取代了为当前时间创建新的 DateTime
或 DateTimeImmutable
对象。相反,你注入 ClockInterface
并调用 now()
。默认情况下,你的应用程序可能会使用 NativeClock
,它总是返回当前系统时间。在测试中,它被替换为 MockClock
。
以下示例介绍了一个利用时钟组件来确定当前时间的服务
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
MockClock
使用一个时间实例化,并且不会自行前进。时间被固定,直到调用 sleep()
或 modify()
。这让你完全控制你的代码认为的当前时间。
在为此服务编写测试时,你可以通过修改时钟的时间来检查某物是否过期或未过期这两种情况
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));
}
}
单调时钟
MonotonicClock
允许你实现精确的秒表;根据系统,精度可达纳秒级。它可以用于测量两次调用之间经过的时间,而不会受到系统时钟有时引入的不一致性的影响,例如通过更新系统时钟。相反,它持续增加时间,使其特别适用于测量性能。
在服务中使用时钟
在你的服务中使用时钟组件来检索当前时间,使它们更容易测试。例如,通过在测试期间使用 MockClock
实现作为默认实现,你将完全控制将“当前时间”设置为任何任意日期/时间。
为了在你的服务中使用此组件,请使你的类使用 ClockAwareTrait。感谢服务自动配置,trait 的 setClock()
方法将由服务容器自动调用。
你现在可以调用 $this->now()
方法来获取当前时间
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,
};
}
}
感谢 ClockAwareTrait
,并通过使用 MockClock
实现,你可以任意设置当前时间,而无需更改你的服务代码。这将帮助你测试你的方法的每种情况,而无需实际处于某个月份或其他月份。
DatePoint
类
时钟组件使用一个特殊的 DatePoint 类。这是 PHP 的 DateTimeImmutable 之上的一个小包装器。你可以在期望 DateTimeImmutable 或 DateTimeInterface 的任何地方无缝使用它。DatePoint
对象从 Clock 类获取日期和时间。这意味着,如果你像 用法部分中所述对时钟进行了任何更改,则在创建新的 DatePoint
时将反映出来。你也可以直接创建一个新的 DatePoint
实例,例如当将其用作默认值时
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(),
) {
}
}
构造函数还允许设置时区或自定义参考日期
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);
DatePoint
类还提供了一个命名构造函数,用于从时间戳创建日期
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
createFromTimestamp()
方法在 Symfony 7.1 中引入。
注意
此外,DatePoint
提供了更严格的返回类型,并提供了跨 PHP 版本的持续错误处理,这要归功于在主题上对 PHP 8.3 行为的polyfill。
DatePoint
也允许设置和获取日期和时间的微秒部分
1 2 3
$datePoint = new DatePoint();
$datePoint->setMicrosecond(345);
$microseconds = $datePoint->getMicrosecond();
注意
此功能对 PHP 8.4 在此主题上的行为进行了 polyfill,因为微秒操作在以前版本的 PHP 中不可用。
7.1
setMicrosecond() 和 getMicrosecond() 方法在 Symfony 7.1 中引入。
编写时间敏感的测试
时钟组件提供了另一个 trait,称为 ClockSensitiveTrait,以帮助你编写时间敏感的测试。此 trait 提供了在每次测试后冻结时间和恢复全局时钟的方法。
使用 ClockSensitiveTrait::mockTime()
方法在你的测试中与模拟时钟进行交互。此方法接受不同类型作为其唯一参数
- 一个字符串,它可以是一个日期来设置时钟(例如,
1996-07-01
)或一个间隔来修改时钟(例如,+2 days
); - 一个
DateTimeImmutable
来设置时钟; - 一个布尔值,用于冻结或恢复全局时钟。
假设你想测试上面示例的 MonthSensitive::isWinterMonth()
方法。这就是你可以编写该测试的方式
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());
}
}
无论你一年中的什么时间运行此测试,此测试的行为都相同。通过结合 ClockAwareTrait 和 ClockSensitiveTrait,你可以完全控制你的时间敏感代码的行为。
异常管理
时钟组件充分利用了一些 PHP DateTime 异常。如果你将无效字符串传递给时钟(例如,在创建时钟或修改 MockClock
时),你将获得 DateMalformedStringException
。如果你传递了无效的时区,你将获得 DateInvalidTimeZoneException
1 2 3 4 5 6 7
$userInput = 'invalid timezone';
try {
$clock = Clock::get()->withTimeZone($userInput);
} catch (\DateInvalidTimeZoneException $exception) {
// ...
}
这些异常从 PHP 8.3 开始可用。但是,感谢时钟组件所需的 symfony/polyfill-php83 依赖项,即使你的项目尚未使用 PHP 8.3,你也可以使用它们。