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

【Laravel初心者向け講座】通報機能を実装してみる(API編)

プログラミング

通報機能のAPIはどうやって実装するんだろう?
通報のデータ管理はどうやってするんだろう?

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


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

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

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


前回は、通報機能のフロント側実装として、
vueによるモーダル画面を中心に解説しました。

今回は通報機能のAPI(サーバーサイド側)の実装
前回の内容と合わせてついにいいね機能の完成です!

通報データの管理で、
ユーザーアクションに関してはRedis
総通報回数はMySQL(RDB)で管理します。


今回のポイント

  • RedisとMySQLの通報データ管理
  • フロント側と結合
スポンサーリンク

画面イメージ

データ設計

いいね機能の時はリクエスト回数が多いことが予想されるので全てRedisで管理しましたが、
通報機能はそこまでデータの更新頻度が高くありません。(おそらく)


ユーザーの通報情報はRedisで管理

Hash型

Key

“laravel_database_shoot_tweet:{userId}:report”

Field

{tweetId}

Value

true or false

通報の総数はMySQLで管理することにします

tweets
report_count:Integer


通報機能API実装

今回の制作物は以下になります。

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


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

ルーティング

ファイル名:routes/web.php

Route::post('/tweet/report', 'TweetController@postReport');

コントローラークラス

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

    public function postReport(Request $request)
    {
        $this->tweetService->updateReportCount($request->tweetId, $request->reportPushed);
        return $request->tweetId;
    }

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


DI(依存注入)してあるtweetServiceupdateReportCountメソッドに
ツイートのIDと通報のプッシュ情報(true or false)を渡し、データの更新を行います。

サービスクラス

ファイル名: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();
// Redisのメソッドを共通化。action_typeを渡すことで格納するkeyを可変に
        $likedTweetIdList = RedisModel::getActionToTweetPerUser(config('tweet.USER_ACTION.LIKE'), $user->id, $tweetIdList);
        $reportedTweetIdList = RedisModel::getActionToTweetPerUser(config('tweet.USER_ACTION.REPORT'), $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];
                $showableTweets[] = $tweet;
            }
        }

    public function updateLikeCount($tweetId, $likePushed)
    {
        $user = Auth::user();

        if ($likePushed) {
// Redisのメソッドを共通化。action_typeを渡すことで格納するkeyを可変に
            if (RedisModel::setActionToTweetPerUser(config('tweet.USER_ACTION.LIKE'), $user->id, $tweetId, $likePushed)) {
                RedisModel::incrTweetLikeCount($tweetId);
            }
        }
        if (!$likePushed) {
// Redisのメソッドを共通化。action_typeを渡すことで格納するkeyを可変に
            if (RedisModel::delActionToTweetPerUser(config('tweet.USER_ACTION.LIKE'), $user->id, $tweetId, $likePushed)) {
                RedisModel::decrTweetLikeCount($tweetId);
            }
        }

        return $likePushed;
    }

    public function updateReportCount($tweetId, $reportPushed)
    {
        $user = Auth::user();

        if ($reportPushed) {
// Redisのメソッドを共通化。action_typeを渡すことで格納するkeyを可変に。configファイルに変数を新たに定義
            if (RedisModel::setActionToTweetPerUser(config('tweet.USER_ACTION.REPORT'), $user->id, $tweetId, $reportPushed)) {
// tweetsテーブルのreport_countをインクリメント
                Tweet::whereId($tweetId)->increment('report_count');
            }
        }
        if (!$reportPushed) {
// Redisのメソッドを共通化。action_typeを渡すことで格納するkeyを可変にconfigファイルに変数を新たに定義
            if (RedisModel::delActionToTweetPerUser(config('tweet.USER_ACTION.REPORT'), $user->id, $tweetId, $reportPushed)) {
// tweetsテーブルのreport_countをデクリメント
                Tweet::whereId($tweetId)->decrement('report_count');
            }
        }

    }


コントローラーから呼び出される
一番下のupdateReportCountというメソッドで通報情報を処理します。

処理の流れはいいね機能とほぼ同じ

認証ユーザーを取得

reportPushedのtrue or falseで場合わけ

各ユーザーの通報情報を処理(追加or削除)

総通報回数を更新


今回1つ工夫したポイントとして、
ユーザー毎のいいね情報を格納するRedisのメソッドと共通化して使用しました。

よくみるとメソッド名がいいね機能の時と若干変わっています。

setTweetLikePerUser => setActionToTweetPerUser
delTweetLikePerUser => delActionToTweetPerUser

configファイルに切り出した変数でアクションタイプを渡してあげて、
RedisのKeyをアクションタイプ毎に変えました。


いいね機能実装の時と同様
RedisModelRedisの処理を定義しているので、
use App\RedisModel;を忘れずに記載しましょう。


そして通報データを更新した内容を、ツイートを取得する際にデータを埋め込んで
画面に渡す必要があるので、

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

Redisに格納してある通報データを取得し、埋め込んであげます。


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

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

Tweetクラス

ファイル名:app/Tweet.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

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

}



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

アクセサの定義後は、
$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;
        Redis::hset($baseKey, $tweetId, $actionPushed);
        Redis::expire($baseKey, 60 * 60 * 24 * $expireDays);
    }

    public static function getActionToTweetPerUser ($actionType, $userId, $tweetIdList)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $userId, $actionType]);
        return Redis::hMGet($baseKey, $tweetIdList);
    }


    public static function delActionToTweetPerUser ($actionType, $userId, $tweetId)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $userId, $actionType]);
        $expireDays = 60;
        Redis::hdel($baseKey, $tweetId);
    }

    public static function incrTweetLikeCount($tweetId)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), config('tweet.USER_ACTION.LIKE')]);
        $expireDays = 60;
        Redis::zincrby($baseKey, 1, $tweetId);
        Redis::expire($baseKey, 60 * 60 * 24 * $expireDays);
    }

    public static function decrTweetLikeCount($tweetId)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), config('tweet.USER_ACTION.LIKE')]);
        $expireDays = 60;
        Redis::zincrby($baseKey, -1, $tweetId);
    }

}

共通部分が多いので、UserAction(Like, Report, Impression:Impressionはまだ未実装)のRedis格納のメソッドを1つにまとめることにしました。

名前も分かりやすく再命名
getActionToTweetPerUser
setActionToTweetPerUser
delActionToTweetPerUser

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

Configファイル

ファイル名:configs/tweet.php

<?php

return [

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

];


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

まとめ

以上、通報機能のAPI実装でした。
これで通報機能が完成しました!!

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

ポイント

  • RedisとMySQLの通報データ管理
  • フロント側と結合

制作物

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


正直、いいね機能のAPIと実装内容は似ていましたね。

工夫としては、
メソッドを共通化したり、
データの管理方法を少し変えたりしました。

機能が増えてくると、コード量が増えてきます。

そんな中、可読性、保守性を保つためには

似たような処理を共通化したり、
変数を切り出したりすることは基本テクニックです。

効率的かつ美しいコードはエンジニアは常に意識していく必要があります。

なるべく皆さんの参考になるようなコードを書いていけるように
常に学んでいきたいと思います。


それでは、次回は「インプレッション機能(フロント編)」の実装を通じて、ポーリングという技術が学べます!



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

それではまた!

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

コメント

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