Компонент Clock

Дата оновлення перекладу 2024-05-02

Компонент 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, які ви можете використати як глобальні годинники у вашому додатку, залежно від ваших потреб.

Доступні реалізації Clock

Компонент 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. Це невелика обгортка над 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
// ви можете вказати часовий пояс
$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);

// негативні часові мітки (для дат до 1го січня 1970 року) та плаваючі часові мітки
// (для високоточних субсекундних часових міток) також підтримуються
$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:

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, необхідної для компонента Clock, ви можете використовувати їх, навіть якщо ваш проект ще не використовує PHP 8.3.