結果を一定期間保持する、"Cache"

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

## 最初に
[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のインポートは省略します。


## 結果を一定期間保持する、"Cache"

[Cache](https://symfony.com/doc/current/components/cache.html)は、処理結果を一定期間保持し、その値を返すことでパフォーマンス向上を行うコンポーネントです。

実はちゃんと使ったことがないコンポーネントです。が、ざっとドキュメントを読んでみると非常に魅力的なコンポーネントでした。

### インストール

```shell
composer require symfony/cache
```

#### キャッシュの作成・利用

値をキャッシュするには`Cache`オブジェクトを使います。`get()`メソッドで、取得するためのキーと取得できなかった場合の処理（= キャッシュする値）を記述します。


```php:SampleCommand.php

use Symfony\Contracts\Cache\ItemInterface;

class SompleCommand extends Command
{
    public function __construct(private readonly CacheInterface $cache)
    {
        parent::__construct();
    }

    public function execute(InputInterface $input, OutputInterface $output): int
    {
        // キャッシュの作成・利用
        $value = $this->cache->get('my_value', function (ItemInterface $item) {
            $randomValue = rand(0, 100); // ランダムな値

            $item->expiresAfter(30); // 30秒間キャッシュ

            return $randomValue;
        });

        // 最初のランダムな値が28だった場合、30秒間はコマンドを何度実行しても28が出力される。
        $io->success($value);
    }
}
```

`ItemInterface`はキャッシュの設定が行えるアイテムクラスです。各キーごとにオブジェクトが生成されます。

#### キャッシュの設定

通常、キャッシュは`var/cache`にファイルとして保存されますが、他にもいくつかの保存先が利用でき、設定ファイルでアダプター（＆プロバイダー）を設定することで切り替えができます。


```yaml:config/packages/cache.yaml
framework:
    cache:
        app: cache.adapter.filesystem
        system: cache.adapter.system
```

`framework.cache.app`はアプリケーション側（= 自分達が作るコード側）のプール設定、`framework.cache.system`はSymfony側のプール設定になります。基本は`app`側のみさわることになると思います。用意されているアダプターは以下の通りです。


| 設定名 | 保存先 |
|---|---|
| cache.adapter.apcu | APC User Cache |
| cache.adapter.array | メモリー内（1プロセス内のみ生きることができる） |
| cache.adapter.filesystem | ファイル（var/cache） |
| cache.adapter.memcached | Memcached |
| cache.adapter.pdo | DB |
| cache.adapter.psr6 | PSR6準拠　 |
| cache.adapter.redis | Redis |
| cache.adapter.redis_tag_aware | Redis(タグベース) |
| cache.adapter.system | Symfony用、appでは使わない |

プロバイダーの設定が必要な場合は、別途設定を追記します。

```yaml:config/packages/cache.yaml
framework:
    cache:
        app: cache.adapter.filesystem
        system: cache.adapter.system

        default_redis_provider: 'redis://localhost'
```

また、`app`, `system`以外のプールも用意することができ、プール名のキャメルケースにした引数をコンストラクタに持つことでオートワイヤリングできます。

```yaml:config/packages/cache.yaml
framework:
    cache:
        pools:
            my_great.cache:
                adapter: cache.adapter.redis
```

```php:SomeService.php
class SomeService
{
    public function __construct(CacheInterface $myGreatCache)
    {
    }
}
```

#### HTTPレスポンスのキャッシュ

HTTPレスポンスのキャッシュはまた別の設定・記述が必要です。まず、`framework.yaml`でキャッシュを有効化します。

```yaml:service/packages/framework.yaml
framework:
    http_cache: true
```

そして、出力するレスポンスオブジェクトで`setPublic()`, `setMaxAge()`メソッドを呼び出します。

```php:ItemController.php
class ItemController extends AbstractController
{
    public function index(ItemRepository $itemRepository): Response
    {
        $items = $itemRepository->findBy(['isPublished' => true]);
        $response = $this->render('item/index.html.twig', [
            'items' => $items,
        ]);
        
        $response
            ->setPublic() // "Cache-Control"に"public"追加
            ->setMaxAge(60) // "Cache-Control"に"maxage=60"追加

        return $response;
    }
}
```
これにより、レスポンスヘッダに`Cache-Controle: public, maxage=60`が追加され、60秒間はコンテンツがキャッシュされます。


##### Symfony6.2から！ アトリビュート設定

Symfony6.2ではアトリビュートで指定できるようになり、さらに設定がしやすくなります。

```diff:ItemController.php
+ use Symfony\Component\HttpKernel\Attribute\Cache;

class ItemController extends AbstractController
{

+    #[Cache(public: true, maxage: 60)]
    public function index(ItemRepository $itemRepository): Response
    {
        $items = $itemRepository->findBy(['isPublished' => true]);
+        return $this->render('item/index.html.twig', [
-        $response =  $this->render('item/index.html.twig', [
            'items' => $items,
        ]);
-
-        $response
-            ->setPublic() // "Cache-Control"に"public"追加
-            ->setMaxAge(60) // "Cache-Control"に"maxage=60"追加
-
-        return $response;
    }
}
```

## まとめ

今回は`Cache`を紹介しました。キャッシュは扱いがとても難しいですが、ちゃんと使っていくとパフォーマンスが飛躍的に向上します。
特にSymfony6.2からはアトリビュートでの指定ができるので、トライ＆エラーが非常にやりやすくなるので、試してみる価値大かなと思います。