【Laravel】Redisを使った閲覧数のランキング機能

キャッシュの管理にRedisを使って、ランキング機能をつくった際のメモです。

[
Laravel 5.8 Redis
](https://readouble.com/laravel/5.8/ja/redis.html?header=Redis%25E3%2581%25AE%25E6%2593%258D%25E4%25BD%259C)

環境は以下の通りです。
- laravel 5.8
- php 7.2
- predis 1.1

##Redisのインストール
Redisを使うためにRedisのPHP拡張をインストールします。[Laravel 5.8 Redis](https://readouble.com/laravel/5.8/ja/redis.html?header=%25E3%2582%25A4%25E3%2583%25B3%25E3%2583%2588%25E3%2583%25AD%25E3%2583%2580%25E3%2582%25AF%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%25B3)
ここではLaravelは`5.8`であるため**predis**を使用しますが、'6.0'ではpredisは非推奨となっています。[Laravel 6.x Redis](https://readouble.com/laravel/6.x/ja/redis.html)
なので`6.0`の場合は**phpredis**を使用しましょう。(本記事ではphpredisは扱いません)

###composerでpredisのインストール
predis/predisパッケージをcomposerでインストールします。

```
$ composer require predis/predis
```

###Redisの設定
データベースの設定ファイルを編集します。自分の場合、redisをdockerコンテナで運用していたためホスト名はコンテナ名となっています。

```php:database.php
'redis' => [

    'client' => env('REDIS_CLIENT', 'predis'),

    'default' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', 'redis'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => env('REDIS_DB', 0),
    ],
]
```

```php:.env
CACHE_DRIVER=redis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
```
###動作確認
Tinkerを使って動作確認をしてみます。

```
$ php artisan tinker
Psy Shell v0.9.12 (PHP 7.3.14 — cli) by Justin Hileman
>>> use Redis
>>> Redis::set('test', 'TEST');
=> Predis\Response\Status {#3050}
>>> Redis::get('test')
=> "TEST"
```

##設計
機能を大きく二つに分けると

**キャッシュへの記事の閲覧数の保存**
・keyに記事ごとのid、valueに閲覧数を保存
　記事の閲覧があった場合
　->既にkeyの保存があれば、閲覧数をインクリメント(valueを+1)。
　->まだkeyの保存がなければ、新しくkeyを作成し、value=1で保存。

**キャッシュからランキングの結果を取得し、配列で返す**
・すべてのkeyを取得し、valueの降順にソートした配列を返す。

##RankingService
ランキング機能はRankingServiceクラスとし、ライブラリとして切り出します。

```php:app\Libraries\RankingService
<?php
namespace app\Libraries;

use Illuminate\Http\Request;
use Redis;

class RankingService
{
    //keyの保存、閲覧数のインクリメント
    public function incrementViewRanking($id)
    {
        $key = "ranking-"."id:".$id;
        $value = Redis::get($key);

        if(empty($value)){
            Redis::set($key, "1");
            Redis::expire($key, 3600*24); 
        } else {
            Redis::set($key, $value + 1);
        }
    }

    //ランキング結果を配列で取得
    public function getRankingAll()
    {
        $keys = Redis::keys('ranking-*');
        $results = Array();

        if(empty($keys) != true){
            for($i = 0; $i < sizeof($keys); $i++){
                $id = explode(':', $keys[$i])[1];
                $point = Redis::get('ranking-id:'. $id);
                $results[$id] = $point;
            }
            arsort($results, SORT_NUMERIC);
        }
        return $results;
    }
}
```

###keyの保存、閲覧数のインクリメント
```php
public function incrementViewRanking($id)
{
    $key = "ranking-"."id:".$id;
    $value = Redis::get($key);

    if(empty($value)){
        Redis::set($key, "1");
        Redis::expire($key, 3600*24); 
    } else {
        Redis::set($key, $value + 1);
    }
}
```
`incrementViewRanking`では閲覧のあった記事のidを引数として受け取り閲覧数の管理を行います。
一つずつ見ていきます。

```php
$key = "ranking-"."id:".$id;
$value = Redis::get($key);
```
keyからvalueの取得をします。
Redisでは`Redis::get()`でvalueの取得ができます。

```php
if(empty($value)){
    Redis::set($key, "1");
    Redis::expire($key, 3600*24); 
} else {
    Redis::set($key, $value + 1);
}
```
keyがあればvalueをインクリメントします。
なければ記事のidを`ranking-id:$id`としてキーを保存します。
`Redis::expire()`でキャッシュする時間を指定します。今回は3600秒×24で24時間に指定します。

###ランキング結果の取得

```php
public function getRankingAll()
{
    $keys = Redis::keys('ranking-*');
    $results = Array();

    if(!empty($keys)){
        for($i = 0; $i < sizeof($keys); $i++){
            $id = explode(':', $keys[$i])[1];
            $point = Redis::get('ranking-id:'. $id);
            $results[$id] = $point;
        }
        arsort($results, SORT_NUMERIC);
    }
    return $results;
}
```
`getRankingAll`ではidと閲覧数をランキングの順で配列に格納します。
保存したすべてのkeyから、`arsort()`によってvalueの降順にソートしています。

`Redis::keys('ranking-*')`はkeyの取得です。`*`でワイルドカードの指定ができます。

ここは正直ごちゃごちゃしてるので改善の余地がありそう。

##各コントローラ内での使用
###閲覧数のインクリメント
記事の表示があるたびにインクリメントを行うので、`incrementViewRanking()`はコントローラの表示メソッド内で使います。

```php
    public function show(Article $article)
    {
        $ranking = new RankingService;
        $ranking->incrementViewRanking($article->id);  //インクリメント
        
        $movie->getArticle($article->id);  //記事の取得

        return view('articles.show', [
            'article' => $article
        ]);
    }
```

###ランキングの取得

```php
    public function ranking(Article $article)
    {
        $ranking = new RankingService;
        $results = $ranking->getRankingAll();
        $article_ranking = $article->getArticleRanking($results);

        return view('article.ranking', [
            'article_ranking' => $article_ranking
        ]);
    }
```
`getRankingAll()`で取得したランキングの配列から記事を取得します。

```php
    public function getArticleRanking(Array $results)
    {
        $article_ids = array_keys($results);
        $ids_order = implode(',', $article_ids);
        $article_ranking = $this->whereIn('id', $article_ids)
                                ->orderByRaw(DB::raw("FIELD(id, $ids_order)"))
                                ->paginate(10);
        return $article_ranking;
    }
```
配列の順序のまま記事を取得したいので、こんな風に生のクエリを書くことになりました。普通に`whereIn`だけで取得すると自動的にLaravelがid順に並べ替えてしまうのです。
ごちゃごちゃしてますね。