通報機能の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で管理することにします
tweetsreport_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(依存注入)してあるtweetService
のupdateReportCount
メソッドに
ツイートの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をアクションタイプ毎に変えました。
いいね機能実装の時と同様RedisModel
でRedis
の処理を定義しているので、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と実装内容は似ていましたね。
工夫としては、
メソッドを共通化したり、
データの管理方法を少し変えたりしました。
機能が増えてくると、コード量が増えてきます。
そんな中、可読性、保守性を保つためには
似たような処理を共通化したり、
変数を切り出したりすることは基本テクニックです。
効率的かつ美しいコードはエンジニアは常に意識していく必要があります。
なるべく皆さんの参考になるようなコードを書いていけるように
常に学んでいきたいと思います。
それでは、次回は「インプレッション機能(フロント編)」の実装を通じて、ポーリングという技術が学べます!
楽しみにしていてください。
それではまた!
コメント