DoctrineCacheBundleが非推奨になったのでSymfonyのCacheを使うようにした

## 概要

Symfonyのアプリケーションで、キャッシュをする際に `DoctrineCacheBundle` を使っていたが、
`doctrine-bundle` が2.0系にバージョンアップしたタイミングで、
`DoctrineCacheBundle` が非推奨になっていたので、
SymfonyのCacheで実装したのと、その際に **クリアキャッシュで少しハマった話。**

## 環境

- Docker
  - Symfonyアプリケーションが動作するWebコンテナ(`:443`, `:80`)
      - Apache 2.4.41
      - PHP 7.3.9
      - Symfony 4.4.3
      - PHPUnit 8.5.2
  - DBコンテナ(`:3306`)
      - MySQL 5.6
  - Memcachedコンテナ(`:11211`)

## 実装方法


`doctrine-bundle` パッケージが 2.0系にバージョンアップしたタイミングで非推奨になった。

https://github.com/doctrine/DoctrineBundle/blob/2.0.6/UPGRADE-2.0.md#deprecation-of-doctrinecachebundle

> Deprecation of DoctrineCacheBundle
Configuring caches through DoctrineCacheBundle is no longer possible. Please use symfony/cache through the pool type or configure your cache services manually and use the service type.

Symfony/cache を使えと書いてあるので今回はそれに従って修正した :pencil: 


実装としてはこんな感じ

```
# framework.yaml
framework:
    cache:
        pools:
            app_memcached:
                public: true
                adapter: cache.adapter.memcached
                provider: app_memcached_provider

```

HogeというクラスにキャッシュをDIさせる

```
# services.yaml
parameters:
    memcached_servers:
        - 'memcached://memcached:11211' # 環境によって複数台書いたり

services:
    app_memcached_provider:
        class: \Memcached
        factory: ['Symfony\Component\Cache\Adapter\MemcachedAdapter', 'createConnection']
        arguments:
            - "%memcached_servers%"

    hoge:
        class: Hoge
        arguments:
            - "@app_memcached"

```

使い方はこんな感じ（関数名とか動作内容はテキトー）

```php
..
class Hoge
{
    /** @var CacheInterface */
    private $cache;

    public function __construct(
        CacheInterface $cache
    ) {
        $this->cache = $cache;
    }

    /**
     * キャッシュされていればtrue キャッシュされていなければ保存してfalseを返す
     * @param string $key
     * @return bool
     */
    public function isCached(string $key): bool
    {
        /** @var CacheItem $item */
        $item = $this->cache->getItem($key);

        if ($item->isHit()) {
            return true;
        }

        $item->set(true);
        $item->expiresAfter(3600);
        $this->cache->save($item);
        return false;
    }
```

これを検証するために書いたテスト。いたってシンプル。

```php
..
class HogeTest extends WebTestCase
{
    use KernelTestTrait;

    public function setUp(): void
    {
        static::bootKernel();

        // mt_srand(1); // この乱数のシード値を設定するとテストが落ちる。なぜでしょう。
        $cache = $this->getContainer()->get('app_memcached');
        $cache->clear(); // テストの前にはキャッシュをクリアさせる
    }

    public function test_キャッシュが既にされていればTrueがかえること()
    {
        $hoge = $this->getContainer()->get('hoge');
        // 一旦キャッシュさせる
        $hoge->isCached('key');
        $this->assertTrue($hoge->isCached('key'));
    }

    public function test_キャッシュしてなければ新規にキャッシュされてFalseがかえること()
    {
        $hoge = $this->getContainer()->get('hoge');
        $this->assertFalse($hoge->isCached('key'));
    }
}
```

コメントに書いたように、SymfonyのCacheを使うようにしたことによって、ハマったというのが、

この **キャッシュをクリアする前に乱数のシード値を設定しているとキャッシュがクリアされない** という問題。

イメージでは、 `$cache->clear()` を実行すれば、キャッシュ内は全てクリアされるものだと思っていたが、

`Symfony\Component\Cache\Traits\AbstractTrait.php` を確認すると、

https://github.com/symfony/symfony/blob/v4.4.3/src/Symfony/Component/Cache/Traits/AbstractTrait.php#L112-L136

どうやら **ネームスペースを更新することで実質キャッシュクリア** をしている模様。 実質。

※ `versioningIsEnabled` が TRUE である場合

そして、その **ネームスペースの更新は乱数を元に生成されていること** 。

つまり、キャッシュをクリアする前に乱数のシード値を固定値で設定してしまうと、その後 `$cache->clear()` をしても、同じネームスペースが作成されるため、実質キャッシュクリアができておらず、前のテストで実行したキャッシュが残っており、意図した挙動とならない。

Symfonyキャッシュクリアのイメージ

![image_symfony_cache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547621/8cc97127-ed09-15f3-3c9f-7e7976390619.png)

自分の場合は、テストデータを作成するパッケージが内部で乱数のシード値を設定していたために発生していた。

実際にキャッシュの中身を確認すると、ネームスペースがキャッシュのキーに含まれていることが分かる。

```
telnet memcached 11211
..
Connected to memcached.
Escape character is '^]'.

stats items
STAT items:1:number 16
..
stats cachedump 1 100
ITEM ivGyRuQzLa%3AXiCMZ%3Akey [4 b; 1580539034 s]
..
get ivGyRuQzLa%3AXiCMZ%3Akey
VALUE ivGyRuQzLa%3AXiCMZ%3Akey 0 4
b:1;
END
```

キャッシュのキーが `ivGyRuQzLa:XiCMZ:key` で 間の `XiCMZ` がネームスペースのバージョン。

もちろん `$cache->clear()` してもexpiredしない限り残り続けている。

回避策としては、

- キャッシュクリアの前に、乱数の初期化をする
    - `mt_srand()`
- もしくは、テストで検証したいキャッシュのキーが分かっているのであれば、明示的に消してからテストを実行する
    - `$cache->delete('key')`
    - 意図せず別のキーでキャッシュされてしまい、他のテストに影響を及ぼす可能性はある

他の人はどうやっているのか気になるところ。

　　

今回、MemcachedAdapterでしか動作確認してませんが、RedisAdapterも同様に AbstractAdapterを継承しているので、clearの挙動は同じかもです。

そもそも、キャッシュの部分を検証したい内容に含めるかどうか（モックで代用する）とか考えてもいいかもしれない。