当ブログ人気記事!
転職体験談
Laravelで学ぶWebアプリ開発
誰でも作れるチャットアプリ
未経験への勧め

【Laravel初心者向け講座】インプレッション機能を実装してみる(API編)

プログラミング

インプレッション機能のAPIはどうやって実装するんだろう?
インプレッション数のデータ管理はどうする?

こういった悩みに対して書きました!


この記事を書いている私(@Shoot58153748)は、
2020年2月現在メガベンチャーの社内スタートアップの部署でエンジニア(1年目)をしており、

プログラミング未経験からメガベンチャーへの転職を成功させた経験・ノウハウ
Webエンジニアになってから学んだこと

をブログにまとめています。


前回は、インプレッション機能のフロント側実装として、
ポーリングという技術を中心に解説しました。

今回はインプレッション機能のAPI(サーバーサイド側)の実装
前回の内容と合わせてついにインプレッション機能の完成です!

インプレッション数のデータの管理に関しては、
Redisで管理します。


今回のポイント

  • Redisによるインプレッションデータ管理
  • フロント側と結合
スポンサーリンク

画面イメージ

データ設計

インプレッション機能はいいね機能と同様、
リクエスト回数が多いことが予想されるので全てRedisで管理します。

SortedSet型

Key

“laravel_database_shoot_tweet:impression”

Score

{impressionCount}

Member

{tweetId}


Hash型

Key

“laravel_database_shoot_tweet:{userId}:impression”

Field

{tweetId}

Value

true or false

インプレッション機能API実装

今回の制作物は以下になります。
いいね機能、通報機能と基本的には同じ流れですね。

Redisクラスのメソッドを共通化したりして工夫は混ぜていきます。

  • コントローラークラス(+ルーティング)
  • サービスクラス
  • Tweetクラス
  • Redisクラス
  • Configファイル


まずは、ルーティングにパスを定義

ルーティング

ファイル名:routes/web.php

Route::post('/tweet/impression', 'TweetController@postImpression');

コントローラークラス

ファイル名:app/Http/Controllers/TweetController.php

    public function postImpression(Request $request)
    {
        $decodedImpressionTweetIdList = json_decode($request->impressionTweetIdList, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            return response()->json(['errorMessage' => json_last_error_msg()], 500);

        }

        $this->tweetService->updateImpressionCount($decodedImpressionTweetIdList);
        return $request->impressionTweetIdList;
     }

コントローラーはデータの入出力の流れが分かりやすいように出来るだけシンプルに、スマートに。サービスクラスにビジネスロジックを投げることが望ましいです。

リストをjson_decodeした後に、

DI(依存注入)してあるtweetServiceupdateImpressionCountメソッドに
インプレッションツイートのIDリストを渡し、データの更新を行います。

サービスクラス

ファイル名:app/Http/Services/TweetService.php

    public function extractShowTweets($fetchedTweetIdList, $page)
    {
        $limit = 10;
        $offset = $page * $limit;
        $tweets = Tweet::orderBy('created_at', 'desc')->offset($offset)->take($limit)->get();
        if (is_null($tweets)) {
            return [];
        }

        if (is_null($fetchedTweetIdList)) {
            return $tweets;
        }

        $user = Auth::user();
        $tweetIdList = $tweets->pluck('id')->toArray();
        $likedTweetIdList = RedisModel::getActionToTweetPerUser(config('tweet.USER_ACTION.LIKE'), $user->id, $tweetIdList);
        $reportedTweetIdList = RedisModel::getActionToTweetPerUser(config('tweet.USER_ACTION.REPORT'), $user->id, $tweetIdList);
// 追記
+        $viewedTweetIdList = RedisModel::getActionToTweetPerUser(config('tweet.USER_ACTION.IMPRESSION'), $user->id, $tweetIdList);

        $showableTweets = [];
        foreach ($tweets as $index => $tweet) {
            if (!in_array($tweet->id, $fetchedTweetIdList)) {
                $tweet->is_liked = $likedTweetIdList[$index];
                $tweet->is_reported = $reportedTweetIdList[$index];
// 追記
+                $tweet->is_viewed = $viewedTweetIdList[$index];

                $showableTweets[] = $tweet;
            }
        }

...

+    public function updateImpressionCount($impressionTweetIdList)
+    {
+        $user = Auth::user();
+        RedisModel::setActionToTweetPerUser(config('tweet.USER_ACTION.IMPRESSION'), $user->id, $impressionTweetIdList, true);

+        RedisModel::incrTweetImpressionCount($impressionTweetIdList);
+    }
    }


コントローラーから呼び出される
updateImpressionCountというメソッドでインプレッション情報を処理します。

処理の流れはシンプルです。

というのは、いいね機能や通報機能と異なり
インプレッション取り消しのアクションがないので場合分けが必要ありません。

ただ、そしてインプレッションツイートIDは変数ではなく、配列です。
ただ、同じユーザーアクションであるため、
配列でも対応できるよう、
Redisのメソッド(setActionToTweetPerUser)を共通化しました。

認証ユーザーを取得

各ユーザーのインプレッション情報を登録

総インプレッション回数を更新

いいねや通報機能のAPI実装を見ている人は必要ありませんが、

RedisModelRedisの処理を定義しているので、
ファイルの頭に、use App\RedisModel;を忘れずに記載しましょう。


そして各ユーザーに対して表示済みツイートかどうかのデータを
画面に渡す必要があるので、

無限スクロールの際に実装したextractShowTweetsメソッドを修正

Redisに格納してあるインプレッションデータを取得し、埋め込んであげます。


ここで、Tweet.phpに少し手を加えます。
というのは、既存のままだとtweetsテーブルのカラムに存在する属性しかありません。

通報データを格納するis_viewedという属性も追加して定義してあげます。

Tweetクラス

ファイル名:app/Tweet.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tweet extends Model
{
    /**
     * モデルの配列形態に追加するアクセサ
     *
     * @var array
     */
+    protected $appends = ['is_liked', 'is_reported', 'is_viewed'];

    public function getIsLikedAttribute() {
        return $this->attributes['is_liked'];
    }

    public function getIsReportedAttribute() {
        return $this->attributes['is_reported'];
    }

+    public function getIsViewedAttribute() {
+        return $this->attributes['is_viewed'];
+    }
}



データベースに対応するカラムがない属性の配列を追加するためには、
まずはじめに値のアクセサ(getIsViewedAttribute)を定義します。

アクセサの定義後は、
$appendsというプロパティに属性名を追加してあげることでモデルの配列とJSON形式両方に含まれるようになります。

残りは、Redisの実装です!

Redisクラス

ファイル名:app/RedisModel.php

<?php

namespace App;

use Illuminate\Support\Facades\Redis;

class RedisModel
{

    public static function setActionToTweetPerUser ($actionType, $userId, $tweetId, $actionPushed)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $userId, $actionType]);
        $expireDays = 60;
+         $isSet = false;
+        if (is_array($tweetId)) {
+            $isSet = self::msetActionToTweetPerUser($baseKey, $tweetId, $actionPushed);
+        } else {
+            $isSet = Redis::hset($baseKey, $tweetId, $actionPushed);

+        }
        Redis::expire($baseKey, 60 * 60 * 24 * $expireDays);
+        return $isSet;
    }

+    private static function msetActionToTweetPerUser ($baseKey, $tweetIdList, $actionPushed)
+    {
+        $values = [];
+        foreach($tweetIdList as $tweetId) {
+            $values[$tweetId] = $actionPushed;
+        }
+        Redis::hMSet($baseKey, $values);
+    }

...

+    public static function incrTweetImpressionCount($impressionTweetIdList)
+    {
+        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), config('tweet.USER_ACTION.IMPRESSION')]);
+        $expireDays = 60;
+        foreach($impressionTweetIdList as $tweetId) {
+            Redis::zincrby($baseKey, 1, $tweetId);
+        }
+        Redis::expire($baseKey, 60 * 60 * 24 * $expireDays);
+    }

}


インプレッションも同じユーザーアクションの1つなので、メソッドをなるべく共通化させたい。
そこでsetActionToTweetPerUserも配列に対応させます。

プライベートメソッド
msetActionToTweetPerUserを定義します。

最初Staticメソッドで定義してはなく、
$this->msetActionToTweetPerUserという風に実装していたのですが、
Syntac errorが出てしまった。。

そこでStaticなメソッドにしたらエラーが解消されたので、Staticメソッドにしています。

総インプレッション回数を更新するincrTweetImpressionCountメソッドも加えてやって、

最後に、Configファイルで変数を追加して終わりです。

Configファイル

ファイル名:configs/tweet.php

<?php

return [

    'TWEET_BASE_KEY' => 'shoot_tweet',
    'USER_ACTION' => [
        'LIKE' => 'like',
        'REPORT' => 'report',
+        'IMPRESSION' => 'impression'
    ]

];


マジックナンバーや生の文字列、というのは可読性を下げるので
Configファイルなどに切り出してあげると良いでしょう。

まとめ

以上、インプレッション機能のAPI実装でした。
これでインプレッション機能が完成しました!!

今回のポイントと成果物をまとめます。

ポイント

  • Redisによるインプレッションデータ管理
  • フロント側と結合

制作物

  • コントローラークラス(+ルーティング)
  • サービスクラス
  • Tweetクラス
  • Redisクラス
  • Configファイル


これで、いいね、通報、インプレッションなどのユーザーアクション機能は
全て実装できました。

現在、いいねとインプレッション数はRedisで管理していますが、
次回は、それらのデータをDBに反映させる「バッチ」を実装します!

Laravelでのバッチの作り方、バルクアップデート(SQL)
Cronなどが学べます。


楽しみにしていてください!

それではまた!

プログラミング
スポンサーリンク
シェアする
SHOOTをフォローする
WebエンジニアSHOOTのブログ

コメント

タイトルとURLをコピーしました