Laravel + redis で閲覧回数ランキングを作る

#はじめに
いろんなサイトで見かける「よく見られている記事」的な機能を
Laravelプロジェクトにredisで実装しました。
一部サンプル化して共有します。

サンプルコードは以下
https://github.com/esuEichi/sample_view_ranking_with_laravel_redis.git

手っ取り早く動きだけ見たいときは、上記のサンプルコードをcloneした後、
`composer install`
`composer update`
を実行し最下部に記載の結果にある手順で動作確認ができるかと思います。


##今回の学び
- redis はキーバリューなので、基本的に一意の値で表現できるのは一つ
	- categoryと投稿日でランキングを作るなどのときはkeyの値を工夫する必要がある

##困ったこと
- 今回作成したRankingModluleをControllerとして作っていますが、MVCとしてのControllerの役割は持っていないため、なんとなく気持ち悪い



##考え方
- 記事IDをもとに一意なredisのキーを作成する
- 表示のためのController が呼ばれたときredisにキーがあるかを確認する
	- なければvalue1にセット
	- あればvalueをインクリメント
- キーから記事IDとビュー数を持った配列を作る
- value順にソートしてviewへ渡す
- viewでforeachで出力する


#手順
##プロジェクトを作る
`composer create-project laravel/laravel sample_ranking_with_redis`


##composer で redis を入れる
以下のようにcomposer.jsonへpredisを追記してください。

```composer.json
    "require": {
        "php": ">=5.6.4",
        "laravel/framework": "5.4.*",
        "laravel/tinker": "~1.0",
        "predis/predis":"^1.1"
    },
```
追記したら以下コマンドでredisを使える状態にします。

`composer install`
`composer update`

##redisの立ち上げ
以下コマンドでredisを立ち上げます。簡単ですね。
`redis-server`

##route/webの書き換え
デフォルトのtopへのパスはコメントアウトして、以下二行を足します。
基本的には、topへの遷移時にControllerを経由するようにするのと、
記事ページ（LinkView）への遷移時にControllerを経由するようにします。この時、
記事のIDをControllerで受け取るようにしておきます。

```php:web.php追加分
 Route::get('/', 'TopController@index');
 Route::get('/{id}','LinkController@index');
```


##Ranking機能に必要なファイルの作成
###RankingModule の作成
今回の肝となるランキング部分を司るモジュールを作成します。
まずは以下のコマンドでControllerとして作成します。
`php artisan make:contoller RankingModule`


以下の2つのメソッドを用意します。
####increment_view_ranking メソッド
ユーザーによって記事ページを参照されたときに呼び出され、
閲覧数をインクリメントするメソッドです。
IDを引数として受け取るようにしておくことで
呼び出し元はlinkControllerになります。
####get_ranking_all メソッド
ランキングの結果を配列で返すメソッドです。
redisからキーをすべて取得し、valueの値でソートした配列を返します。
呼び出し元はTopControllerになります。
中身は以下

```php:RankingModule.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Redis;


class RankingModule extends Controller
{
    //
    public function increment_view_ranking($id){
        $key = "ranking-"."id:".$id;

        $value = Redis::get($key);
        if(empty($value)){
            Redis::set($key, "1");
            Redis::expire($key, 60*60); 
        } else {
            Redis::set($key,$value + 1);
        }
        echo "view count:".Redis::get($key);
    }
	public function get_ranking_all(){
        $keys = Redis::keys('ranking-*');
        $results = Array();
        
        if(empty($keys) != true){
            for($i=0; $i < sizeof($keys); $i++){
                $point = Redis::get($keys[$i]);
                $id = explode(':', $keys[$i])[1];
                $results[$id] = $point;
            }
            arsort($results, SORT_NUMERIC);
        }
        return $results;
    }
}
```


###link view の作成
今回はデフォルトのwelcomeテンプレートをコピペしてリネームしただけとします。


###LinkController の作成
上で作ったlinkへの遷移を行うControllerです。


```php:LinkController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class LinkController extends Controller
{
    //
    function index(Request $request){
    	$id = $request->id;
    	$ranking = new RankingModule;
    	$ranking->increment_view_ranking($id);
    	return view('link')->with(compact('id'));
    }
}

```

#welocome viewに記事へのリンク先を作成し、ランキングを作成する
##TopControllerの作成
route/web.phpで書き換えたように、Topページ（welcomeView)への遷移を司るコントローラーです。
役割としてはRankingModuleから現在のランキング表示用の配列を作成し、
viewへ渡すことです。

```php:TopController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
class TopController extends Controller
{
    //
    function index(Request $request){
    	$ranking = new RankingModule;
    	$results = $ranking->get_ranking_all();
    	return view('welcome')->with(compact('results'));
    }
}

```
##welcomeviewの書き換え
welcomeview(topページ）にはリンク先を作成します。
###記事へのリンク
topページから遷移できるダミーの記事ページへのリンクを作成します。
今回はダミーなのでfor文で10記事リンクを作成し、各記事へのIDとします。

###ランキングの作成
TopControllerから受け取ったランキング用の結果配列を使い、
閲覧が多い順に該当の記事へのリンクを作成します。ついでに、何回見られているのかもリンクに表示しておきます。
ついでにランキング用の配列もdumpで吐き出しておきます。

```php:welcomeView追加分
3d2
< 
85,94c84,90
<                 <div class="test_links">
<                 @for ($i = 0; $i < 10; $i++)
<                     <div><a href="./{{{$i}}}">link_{{{$i}}}</a></div>
<                 @endfor
<                 </div> 
<                 <div class="ranking">
<                 @foreach($results as $id => $point)
<                     <div><a href="./{{{$id}}}">link_{{{$id}}}_view_count_is_{{{$point}}}</a></div>
<                 @endforeach
<                 <?php var_dump($results);?>
```

#結果
以下2つのコマンドを打つ
`php artisan serve`
`redis-server`

http://localhost:8000
へアクセス
http://localhost:8000/1
などのページを見ると、一番下の順番が変わっていきます。

![スクリーンショット 2017-04-16 15.22.36.png](https://qiita-image-store.s3.amazonaws.com/0/94194/d2fff0b5-5139-ec67-30a1-cddf09a57ef5.png "スクリーンショット 2017-04-16 15.22.36.png")

#拡張性について
##特定のカテゴリなど条件で絞り込んだランキングが作りたいとき
redisのキーは今回以下の様になってます。
`$key = "ranking-"."id:".$id;`
例えばカテゴリが複数あって、/category ページで配下コンテンツのランキングを作りたいときはまずキーを以下の様にします。

`$key = "ranking-"."id:".$id."_category:".$category;`

こうすることで全体ランキングだけではなく、categoryでkeyを取得することでカテゴリランキングを作成することができます。

#終わりに
実際のサービスではもう少し拡張して、拡張性に書いたようなカテゴリごとのランキングと、全体でのランキングとして使っています。
ランキングを作ることで弱小サービスでもユーザーが使ってくれてる感じが出てきて
楽しくなってきます。

#補足
niisan-tokyo さんからredis の sorted set 機能を使うのが楽だとご指摘をいただきました。
記事は以下です。
http://qiita.com/niisan-tokyo/items/170ef3ac47ee39cfb46c

確かに今回のように無期限かつ常に同一のキーの値を参照し続ける場合はこちらのほうが良さそうだし、
計算コストも少なそうです。

ご指摘ありがとうございました。