CakePHP3で自作アプリを作ってみた part7 〜キャッシュ設定〜

7回に渡って記載していきます。
**CakePHP3で自作アプリを作ってみた シリーズ**
今回でファイナルです！
長々とお世話になりました。

今回part7では

* キャッシュ設定

を解説したいと思います。

ブツはこちら公開済み。
https://cocktail-com.herokuapp.com/

データ登録が間に合わなかったので悪しからず。。。
少しづつ登録します。

今までの記事
**CakePHP3で自作アプリを作ってみた part1 〜イントロ〜**
https://qiita.com/m-hatano/items/61392c33fdbd49376747

**CakePHP3で自作アプリを作ってみた part2 〜herokuでHello World!!まで〜**
https://qiita.com/m-hatano/items/79480fa380ebc49c0209

**CakePHP3で自作アプリを作ってみた part3 〜検索機能の実装〜**
https://qiita.com/m-hatano/items/e85b8aa8fcaa0c3410f0

**CakePHP3で自作アプリを作ってみた part4 〜登録・編集機能の実装〜**
https://qiita.com/m-hatano/items/86657cf8b69fcb542d66

**CakePHP3で自作アプリを作ってみた part5 〜画像アップロードの実装〜**
https://qiita.com/m-hatano/items/47cd8607e19a523749f0

**CakePHP3で自作アプリを作ってみた part６ 〜削除、ログイン、認証・認可〜**
https://qiita.com/m-hatano/items/980e7df5ff2577c4544d

---

# キャッシュ設定
早速ですが、WEBアプリでキャッシュと言っても様々な場面で登場してきます。
自分が知っている限りあげますと、

* クライアント側
 * js, css, 画像などの静的コンテンツのブラウザキャッシュ
 * cookieで保持するキャッシュ
* CDNなど中間サーバ側
 * 静的コンテンツのキャッシュ配信
 * リクエストに対してのレスポンスやデータのキャッシュ
* サーバー側
 * WEBサーバやAPサーバでのメモリキャッシュ
 * memcached, Redis、NoSQL等キーバリューストア型の溜め込み型のキャッシュ設定
 * DBのクエリキャッシュ、データブロックのキャッシュ
 
良く言われるのはこれくらいでしょうか。
今回のアプリでは、このうち「js, css, 画像などの静的コンテンツのブラウザキャッシュ」、「memcached, Redis、NoSQL等キーバリューストア型の溜め込み型のキャッシュ設定」を設定してあります。

順に説明していきます。

## ブラウザでコンテンツキャッシュの設定

配信するコンテンツにhttpのヘッダー情報を付与することによってブラウザで解釈してキャッシュしてくれます。
apache mod_headerを使います。

* .htaccess

```apache
<ifModule mod_headers.c>
    # CACHE media files for 30days
    <Files ~ ".(gif|jpe?g|png|ico|js|gz)$">
        Header set Cache-Control "max-age=2592000"
    </Files>

    # CACHE media files for 12hours
    <Files ~ ".(css)$">
        Header set Cache-Control "max-age=43200"
    </Files>
</ifModule>
```

max-ageは秒数で記載なので間違えないようにしてください。
上段の塊では拡張子がgif, jpeg, png, ico, js, gzの場合は30日キャッシュする設定が書かれています。
下段の塊で、cssのみは12時間のキャッシュとしています。
cssは変更することが多いため長時間はキャッシュせずに変更できるようにという意図です。

キャッシュ設定ですが、一度ブラウザにキャッシュされてしまってから変更したい場合、それぞれのブラウザの設定に依存されてしまうため非常に厄介です。

キャッシュを逃れるためには、ファイル名にタイムスタンプやランダムな配列を付与して更新を読み込まれるように仕組むことが多いです。

ついでにjs,cssなどのコンテンツはapacheに圧縮してもらって配信しましょう。
mod_deflateを使用します。
herokuなどのサーバが物理的に遠いところにある場合は配信容量を抑えるとレスポンス速度向上の効果が高いです。

* .htaccess

```apache
<IfModule mod_deflate.c>
    # mod_deflateを利用して Gzip圧縮
    SetOutputFilter DEFLATE

    # Mozilla4系、IE7、8の古いブラウザでは無効にする
    BrowserMatch ^Mozilla/4\.0[678] no-gzip
    BrowserMatch ^Mozilla/4 gzip-only-text/html
    BrowserMatch \bMSIE\s(7|8) !no-gzip !gzip-only-text/html

    # GIF、JPEG、PNG、ICOなど圧縮済みの画像は再圧縮しない
    SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|ico)$ no-gzip dont-vary
    # プロクシサーバが間違ったコンテンツを配布しないようにする
    Header append Vary Accept-Encoding env=!dont-vary

    # 各コンテンツを圧縮する設定
    AddOutputFilterByType DEFLATE text/plain
    AddOutputFilterByType DEFLATE text/html
    AddOutputFilterByType DEFLATE text/xml
    AddOutputFilterByType DEFLATE text/css
    AddOutputFilterByType DEFLATE text/js
    AddOutputFilterByType DEFLATE text/javascript
    AddOutputFilterByType DEFLATE image/svg+xml
    AddOutputFilterByType DEFLATE application/xml
    AddOutputFilterByType DEFLATE application/xhtml+xml
    AddOutputFilterByType DEFLATE application/rss+xml
    AddOutputFilterByType DEFLATE application/atom_xml
    AddOutputFilterByType DEFLATE application/javascript
    AddOutputFilterByType DEFLATE application/x-javascript
    AddOutputFilterByType DEFLATE application/x-httpd-php
    AddOutputFilterByType DEFLATE application/x-font-ttf
    AddOutputFilterByType DEFLATE application/x-font-woff
    AddOutputFilterByType DEFLATE application/x-font-opentype
    AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
</IfModule>
```

## Herokuでmemcachedを設定する
参考記事様。
https://qiita.com/ryusukefuda/items/07e5cbf2abd10d5472d8

参考記事様を見てherokuへ設定します。
heroku環境変数へmemcachedへの接続情報が格納されるのでそれを使います。

* config/app_heroku.php

```php
    'Cache' => [
        'default' => [
            'className' => 'Memcached',
            'prefix' => 'myapp_cake_',
            'servers' => [env('MEMCACHIER_SERVERS')],
            'username' => env('MEMCACHIER_USERNAME'),
            'password' => env('MEMCACHIER_PASSWORD'),
            'duration' => '+1440 minutes'
        ],

        'session' => [
            'className' => 'Memcached',
            'prefix' => 'myapp_cake_session_',
            'servers' => [env('MEMCACHIER_SERVERS')],
            'username' => env('MEMCACHIER_USERNAME'),
            'password' => env('MEMCACHIER_PASSWORD'),
            'duration' => '+1440 minutes'
        ],

        '_cake_core_' => [
            'className' => 'Memcached',
            'prefix' => 'myapp_cake_core_',
            'servers' => [env('MEMCACHIER_SERVERS')],
            'username' => env('MEMCACHIER_USERNAME'),
            'password' => env('MEMCACHIER_PASSWORD'),
            'duration' => '+1 years',
            'serialize' => 'php'
        ],

        '_cake_model_' => [
            'className' => 'Memcached',
            'prefix' => 'myapp_cake_model_',
            'servers' => [env('MEMCACHIER_SERVERS')],
            'username' => env('MEMCACHIER_USERNAME'),
            'password' => env('MEMCACHIER_PASSWORD'),
            'duration' => '+1 years',
            'serialize' => 'php'
        ],
    ],
```

ちなみにローカル環境では、ファイルキャッシュにしています

* config/app.php

```php
    Cache' => [
        'default' => [
            'className' => 'File',
            'path' => CACHE,
            'url' => env('CACHE_DEFAULT_URL', null),
        ],

        /**
         * Configure the cache used for general framework caching.
         * Translation cache files are stored with this configuration.
         * Duration will be set to '+2 minutes' in bootstrap.php when debug = true
         * If you set 'className' => 'Null' core cache will be disabled.
         */
        '_cake_core_' => [
            'className' => 'File',
            'prefix' => 'myapp_cake_core_',
            'path' => CACHE . 'persistent/',
            'serialize' => true,
            'duration' => '+1 years',
            'url' => env('CACHE_CAKECORE_URL', null),
        ],

        /**
         * Configure the cache for model and datasource caches. This cache
         * configuration is used to store schema descriptions, and table listings
         * in connections.
         * Duration will be set to '+2 minutes' in bootstrap.php when debug = true
         */
        '_cake_model_' => [
            'className' => 'File',
            'prefix' => 'myapp_cake_model_',
            'path' => CACHE . 'models/',
            'serialize' => true,
            'duration' => '+1 years',
            'url' => env('CACHE_CAKEMODEL_URL', null),
        ],
    ],
```

キャッシュの使いどころとしては、マスタキャッシュに使用しています。
読み込みをApplication#bootstrap()に記載することでアプリケーションがリクエストを処理する前に実行してくれます。

* Application#bootstrap()

```php
    public function bootstrap()
    {
        // config/bootstrap.php を `require_once`  するために parent を呼びます。
        parent::bootstrap();

        // エレメントマスタキャッシュがない場合はDBより読み込む
        if(($elements_master = Cache::read('elements_master')) === false ){
            $elementsRepository = TableRegistry::get('Elements');
            $elements_master = $elementsRepository->find('all', [
                'order' => ['Elements.name' => 'asc']
            ])->toArray();
            Cache::write('elements_master', $elements_master);
        }

        // タグマスタキャッシュがない場合はDBより読み込む
        if(($tags_master = Cache::read('tags_master')) === false ){
            $tagsRepository = TableRegistry::get('Tags');
            $tags_master = $tagsRepository->find('all', [
                'order' => ['Tags.name' => 'asc']
            ])->toArray();
            Cache::write('tags_master', $tags_master);
        }

    }
```

キャッシュの設定された場所へCache::read, Cache::writeで読み書きすることができます。
非常に簡易的に記述できて便利です。
処理はマスタキャッシュがない場合にだけ読み込むことにしてあります。

マスタを変更した場合に反映しないとならないのでもちろんリロードも実装しました。

* Controller/CachesControler.php

```php
    /**
     * マスタキャッシュをリロードする
     */
    public function reloadMaster()
    {
        // タグマスタリロード
        $tagsRepository = TableRegistry::get('Tags');
        $tags_master = $tagsRepository->find('all', [
            'order' => ['Tags.name' => 'asc']
        ])->toArray();
        // エレメントマスタリロード
        $elementsRepository = TableRegistry::get('Elements');
        $elements_master = $elementsRepository->find('all', [
            'order' => ['Elements.name' => 'asc']
        ])->toArray();

        if(Cache::write('tags_master', $tags_master) && Cache::write('elements_master', $elements_master)){
            $this->Flash->success('リロードしました。');
        }else {
            $this->Flash->error(MessageUtil::getMsg(MessageUtil::SAVE_ERROR));
        }
        $this->redirect($this->referer());
    }
```

キャッシュしてあるエンティティが少ないためこの実装でやっていますが、多い場合は工夫が必要かもしれません。


あとは要所要所でキャッシュを入れ替える設定を入れています。
管理画面からタグを追加したり、編集した時はreloadMaster()を個別に実装して呼んでいます。

---

以上で自作アプリの解説が終わりました。
かなり参考記事様たちに頼りながら進めていきましたが、一通りは解説しましたので、
質問や、もっとこうしたらいいんじゃないかがあればご連絡ください。

あと、ローンチが先にきてしまいましたが、データの投入を少しづつ進めて行こうと思いますw

以上です。
最後まで読んでいただきありがとうございました！