時間を扱う、"Clock"

[Symfony Component Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony-component)の23日目の記事です。

## 最初に
[Symfony](https://symfony.com)はPHPのフレームワークのひとつです。しかし、公式サイトの説明文には

> Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
  （SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。）

と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。

今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。

※記事内ではautoloadのインポートは省略します。


## 時間を扱う、"Clock"

[Clock](https://github.com/symfony/clock)は、時間を扱うコンポーネントです。Symfony 6.2から導入されました。


### インストール

```shell
composer require symfony/clock
```

### これはなに

`Clock`　コンポーネントは、[PSR-20: Clock](https://www.php-fig.org/psr/psr-20/)を**拡張した**、時間を扱うコンポーネントです。
PSR-20では、`ClockInterface`を実装したクラスを用意して、`DateTimeImmutable`を返す`now()`を用意しようと定義されています。
Symfonyの`Clock`コンポーネントは、PSR-20の`ClockInterface`を拡張した、　独自の`ClockInterface`を使って、PRS-20にない機能`sleep()`, `withTimeZone()`を実装しています。

#### PSR-20の`ClockInterface`

```php:ClockInterface.php

namespace Psr\Clock;

interface ClockInterface
{
    /**
     * Returns the current time as a DateTimeImmutable Object
     */
    public function now(): \DateTimeImmutable;

}
```

#### Clockコンポーネントの`ClockInterface`

```php:ClockInterface.php

namespace Symfony\Component\Clock;

use Psr\Clock\ClockInterface as PsrClockInterface;

interface ClockInterface extends PsrClockInterface
{
    public function sleep(float|int $seconds): void;

    public function withTimeZone(\DateTimeZone|string $timezone): static;
}
```

`now()`は現在時刻を返し、独自の`sleep()`は指定された時間スリープ、`withTimezone`はタイムゾーンを変更した`Clock`を返します。
#### 現在時刻を返す

説明するほどでもないかもですが、`now()`は現在時刻を`DateTimeImmutable`返します。

```php

use Symfony\Component\Clock\NativeClock;

$clock = new NativeClock();

// 現在時刻を返す
echo $clock->now()->format('Y-m-d h:i:s'); // 2022-12-23 07:00:00

 // 10秒スリープ
$clock->sleep(10);

// タイムゾーンを変える
$newClock = $clock->withTimeZone('Asia/Tokyo');
```

当然ながら、オートワイヤリング可能なので、色々な箇所で`$now = new DateTimeImmutable()`しなくてよくなります。

```php
class SomeService
{
    public function __construct(private readonly ClockInterface $clock)
    {
    }
    
    publi function someMethod()
    {
        $item = new Item();
        $item
            ->setName('商品名')
            ->setPrice(1000)
            ->setCreatedAt($this->clock->now())
        ;
    }
}
```

### 3つのClock

`Clock`では`ClockInterface`を実装した3つのClockを用意しており、それぞれの用途を定めています。


| クラス | 用途 | 備考 |
|---|---|---|
| NativeClock | 通常利用 |　`now()`を呼ぶごとにその瞬間の時刻を返す |
| MockClock | テスト用 | `sleep()`はスリープしない<br>`now()`は常に同じ時刻を返す |
| MonotonicClock | パフォーマンス測定用 | [hrtime()](https://www.php.net/manual/ja/function.hrtime.php)を利用 |

`NativeClock`と`MockClock`の`now()`の違いは、実際のコードを見てもらった方が早いです。`NativeClock`は常にメソッド呼び出し時に`new`していて、`MockClock`はプロパティの`now`をクローンして返します。

```php:NativeClock.php
    public function now(): \DateTimeImmutable
    {
        return new \DateTimeImmutable('now', $this->timezone);
    }
```

```php:MockClock.php
    public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZone|string $timezone = null)
    {
        if (\is_string($timezone)) {
            $timezone = new \DateTimeZone($timezone);
        }

        if (\is_string($now)) {
            $now = new \DateTimeImmutable($now, $timezone ?? new \DateTimeZone('UTC'));
        }

        $this->now = null !== $timezone ? $now->setTimezone($timezone) : $now;
    }

    public function now(): \DateTimeImmutable
    {
        return clone $this->now;
    }
```


## まとめ

今回は、`Clock`を紹介しました。PSR準拠なものを使えるのは魅力的です。
（個人的にはFrozenTime的なメソッドもあったら便利だなと思いました。あ、独自で作れば良いのか）