Компонент Clock
Дата обновления перевода 2023-08-21
Компонент Clock
Компонент Clock разделяет приложения и системные часы. Это позволяет вам исправить время, чтобы улучшить тестируемость некоторой чувствительной ко времени логики.
Компонент предоставляет ClockInterface
со следующими реализациями для разных случаев
использования:
- NativeClock
-
Предоставляет способ взаимодействовать с системными часами, то же самое, что делать
new \DateTimeImmutable()
. - MockClock
-
Часто используется в тестах как заменитель для
NativeClock
, чтобы иметь возможность останавливать и изменять текущее время, используя либоsleep()
, либоmodify()
. - MonotonicClock
-
Полагается на
hrtime()
и педоставляет монотонные часы высокого разрешения, когда вам нужен точный секундомер.
Установка
1
$ composer require symfony/clock
Note
Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно
подключить файл 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;
// по умолчанию, Clock использует реализацию NativeClock, но вы можете изменить
// это, установив другую реализацию
Clock::set(new MockClock());
// Затем вы можете получить экземпляр часов
$clock = Clock::get();
// Дополнительно вы можете установить часовой пояс
$clock->withTimeZone('Europe/Paris');
// Отсюда вы мжете получить текущее время
$now = $clock->now();
// И уйти в режим сна на любое количество секунд
$clock->sleep(2.5);
Компонент Clock также представляет функцию now()
:
1 2 3 4
use function Symfony\Component\Clock\now;
// Получить текущее время как экземпляр DateTimeImmutable
$now = now();
Функция now()
принимает необязательный аргумент modifier
,
который будет применен к текущему времени:
1 2 3
$later = now('+3 hours');
$yesterday = now('-1 day');
Вы можете использовать любую строку, принятую конструктором DateTime.
Позже на этой странице вы узнаете, как использовать эти часы в своих сервисах и тестах. При использовании компонента Clock вы манипулируете экземплярами DatePoint. Вы можете узнать больше об этом в специальном разделе .
Доступные реализации часов
Компонент Clock предоставляет несколько готовых реализаций ClockInterface, которые вы можете использовать в качестве глобальных часов в вашем приложении в зависимости от ваших потребностей.
NativeClock
Сервис часов заменяет создание нового DateTime
или объекта DateTimeImmutable
для
текущего времени. Вместо этого, вы внедряете ClockInterface
и вызываете now()
. По
умолчанию, ваше приложение скорее всего будет использовать NativeClock
, который всегда
возвращает текущее время системы. В тестах он заменяется на MockClock
.
Следующий пример представляет сервис, использующий компонент Clock, чтобы определить текущее время:
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 в будущем, поэтому у него не закончился срок действия
static::assertFalse($expirationChecker->isExpired($validUntil));
// Часы спят 10 минут, поэтому сейчас '2022-11-16 15:30:00'
$clock->sleep(600); // Мгновенно меняет время, как будто мы ждали 10 минут (600 секунд)
// изменить часы, принимает все форматы, поддерживаемые DateTimeImmutable::modify()
static::assertTrue($expirationChecker->isExpired($validUntil));
$clock->modify('2022-11-16 15:00:00');
// $validUntil снова в будущем, поэтому у него не закончился срок действия
static::assertFalse($expirationChecker->isExpired($validUntil));
}
}
Монотонные часы
MonotonicClock
позволяет вам реализовывать точный секундомер; в зависимости от системы,
с точностью до наносекунды. Он может быть использован для измерения истёкшего времени между
двумя запросами, не поддаваясь влияюнию неточностей, иногда представленных системными часами,
например, их обновлением. Вместо этого, он последовательно увеличивает время, что особенно
полезно для измерения производительности.
Использование часов внутри сервиса
Использование компонента Clock в ваших сервисах для получения текущего времени, упрощает их
тестирование. Например, используя реализацию MockClock
по умолчанию во время тестирования,
вы будете иметь полный контроль над установкой "текущего времени" на любую произвольную дату/время.
Для того чтобы использовать этот компонент в своих сервисах, необходимо сделать так, чтобы их классы
использовали ClockAwareTrait. Благодаря
автоконфигурации сервисов , метод черты 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
Компонент Clock использует специальный класс
DatePoint.
Это небольшая обертка поверх DateTimeImmutable PHP.
Вы можете использовать его везде, где есть 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
// вы можете указать часовой пояс
$withTimezone = new DatePoint(timezone: new \DateTimezone('UTC'));
// вы также можете создать DatePoint из указанной даты
$referenceDate = new \DateTimeImmutable();
$relativeDate = new DatePoint('+1month', reference: $referenceDate);
Класс DatePoint
также предоставляет именованный конструктор для создания дат из
временных меток:
1 2 3 4 5 6 7
$dateOfFirstCommitToSymfonyProject = DatePoint::createFromTimestamp(1129645656);
// эквивалентно:
// $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.
Note
Кроме того, DatePoint
предлагает более строгие типы возврата и
обеспечивает согласованную обработку ошибок в разных версиях PHP,
благодаря поведению PHP 8.3 полизаполнения по теме.
DatePoint
также позволяет устанавливать и получать микросекундную часть даты и времени:
1 2 3
$datePoint = new DatePoint();
$datePoint->setMicrosecond(345);
$microseconds = $datePoint->getMicrosecond();
Note
Эта функция предоставляет поведение полизаполнения PHP 8.4, так как манипуляция микросекундами недоступна в предыдущих версиях PHP.
7.1
Методы setMicrosecond() и getMicrosecond() были представлены в Symfony 7.1.
Написание тестов, чувствительных ко времени
Компонент Clock предоставляет еще одну черту, называемую ClockSensitiveTrait, которая помогает писать тесты, чувствительные ко времени. Эта черта предоставляет методы для остановки времени и восстановления глобальных часов после каждого теста.
Используйте метод 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, вы получаете полный контроль над поведением кода, чувствительного к времени.
Управление исключениями
Компонент Clock использует все преимущества некоторых исключений PHP DateTime.
Если вы передадите часам невалидную строку (например, при создании часов или
изменении MockClock
), вы получите DateMalformedStringException
. Если вы
передадите невалидный часовой пояс, то получите DateInvalidTimeZoneException
:
$userInput = 'invalid timezone';
- try {
- $clock = Clock::get()->withTimeZone($userInput);
- } catch (DateInvalidTimeZoneException $exception) {
- // ...
}
Эти исключения доступны начиная с версии PHP 8.3. Однако, благодаря зависимости symfony/polyfill-php83, необходимой компоненту Clock, вы можете использовать их, даже если ваш проект еще не использует PHP 8.3.