Компонент 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.