0.背景
本記事は、記事「実務未経験状態でLaravelを使用した共同開発」で取り組んだタスクのうち、フォロー、タイムライン機能についてまとめたもの
1.仕様内容
・自身以外のユーザ詳細ページからフォロー可能
・フォロー中かどうかがひと目で分かるUI
・各ユーザの投稿をタイムラインタブへ
・各ユーザのフォローリスト/フォロワーリストを表示
・各リストにページネーション(10件ごと)
UI(デプロイしたもの)
2.タスクの見積もり
期間
・14日
理由
・フォロー、フォロワーの設計のイメージが出来なかった
・上記に加えて未知のタスクのためエラー、イレギュラー発生及びレビュー、修正期間を想定
実績
・仕様作成完了 4日
・PR完了 4日
・PRレビュー修正 5日
・マージ完了 6日
3.実装
①Routing
・web.php
・認証済ユーザのみ可能な処理
Route::group(['middleware' => 'auth'], function () {
・上記の(['middleware' => 'auth'])下に記載
・フォロー、フォロー解除処理
Route::post('follow/{id}','FollowsController@follow')->name('user.follow'); // フォロー処理
Route::delete('unfollow/{id}','FollowsController@unfollow')->name('user.unfollow'); // フォロー解除処理
・ユーザ認証不要の処理
Route::prefix('/users')->group(function(){
・上記のprefix('/user')下に記載
・フォロー、フォロワーリスト閲覧処理
Route::get('following/{id}','FollowsController@followingList')->name('list.following'); // フォローリスト表示
Route::get('follower/{id}','FollowsController@FollowerList')->name('list.follower'); // フォロワーリスト表示
②Model
・User.php
・フォロー、フォロワーのリレーション(多対多)
・フォロー、フォロー解除、フォローチェックのメソッドを記載(コントローラで呼び出し)
// フォローリレーション(フォローしているユーザを取得)
public function following()
{
return $this->belongsToMany(User::class, 'follows', 'follower_id', 'following_id');
}
// フォロワーリレーション(フォローされているユーザを取得)
public function followers()
{
return $this->belongsToMany(User::class, 'follows', 'following_id', 'follower_id');
}
// フォローチェック
public function isFollowing($userId)
{
return $this->following()->where('following_id', $userId)->exists();
}
// フォローメソッド
public function follow($userId)
{
//自分をフォローした場合処理中断
if(Auth::id() === $userId){
return false;
}
//存在しないユーザをフォローした場合処理中断
if(!User::find($userId)){
return false;
}
if(!$this->isFollowing($userId)){
$this->following()->attach($userId);
return true;
}
}
// フォロー解除メソッド
public function unFollow($userId)
{
if($this->isFollowing($userId)){
$this->following()->detach($userId);
return true;
}
}
③Migration
・create_Follows_Tableを作成
public function up()
{
Schema::create('follows', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('follower_id')->unsigned()->index();
$table->bigInteger('following_id')->unsigned()->index();
$table->timestamps();
//外部キー制約
$table->foreign('follower_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('following_id')->references('id')->on('users')->onDelete('cascade');
$table->unique(['follower_id', 'following_id']);
});
}
④Controller
・FolloesController.php
・フォロー処理
・フォロー解除処理
・フォローリスト表示
・フォロワーリスト表示
// フォロー処理
public function follow($id)
{
Auth::user()->follow($id);
return back();
}
// フォロー解除処理
public function unFollow($id)
{
Auth::user()->unFollow($id);
return back();
}
// フォローリスト表示
public function followingList($id)
{
$user = User::findOrFail($id);
$following = $user->following()->paginate(10);
return view('users.follows.following', compact('user', 'following'));
}
// フォロワーリスト表示
public function followerList($id)
{
$user = User::findOrFail($id);
$followers = $user->follower()->paginate(10);
return view('users.follows.followers', compact('user', 'followers'));
}
⑤View
・users/follows/following.blade.php
・フォローリスト取得
@if ($user->following->isEmpty())
<p class="text-muted">フォローしているユーザーがいません</p>
@else
<ul>
@foreach ($user->following as $follow)
<li class="d-flex justify-content-start my-4">
<img class="mr-2 rounded-circle" src="{{ Gravatar::src($follow->email, 50) }}" alt="ユーザのアバター画像">
<a class="mx-3" href="{{ route('user.show', ['id' => $follow->id]) }}">{{ $follow->name }}</a>
</li>
@endforeach
</ul>
@endif
・users/follows/followers.php
・フォロワーリスト取得
@if ($user->followers->isEmpty())
<p class="text-muted">フォロワーがいません</p>
@else
<ul>
@foreach ($user->followers as $follow)
<li class="d-flex justify-content-start my-4">
<img class="mr-2 rounded-circle" src="{{ Gravatar::src($follow->email, 50) }}" alt="ユーザのアバター画像">
<a class="mx-3" href="{{ route('user.show', ['id' => $follow->id]) }}">{{ $follow->name }}</a>
</li>
@endforeach
</ul>
@endif
・users/detail.blade.php
・ナビタブ部分に各bladeファイルをinclude
・タイムライン部分は他メンバータスクのposts/postsをinclude
<section class="col-md-8">
<div class="card text-center">
<div class="card-header bg-white">
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active bg-white text-primary" id="nav-post-tab" data-bs-toggle="tab" data-bs-target="#nav-post" type="button" role="tab" aria-controls="nav-post" aria-selected="true">タイムライン</button>
<button class="nav-link bg-white text-primary" id="nav-follow-tab" data-bs-toggle="tab" data-bs-target="#nav-follow" type="button" role="tab" aria-controls="nav-follow" aria-selected="false">フォロー</button>
<button class="nav-link bg-white text-primary" id="nav-follower-tab" data-bs-toggle="tab" data-bs-target="#nav-follower" type="button" role="tab" aria-controls="nav-follower" aria-selected="false">フォロワー</button>
<button class="nav-link bg-white text-primary" id="nav-favorite-tab" data-bs-toggle="tab" data-bs-target="#nav-favorite" type="button" role="tab" aria-controls="nav-favorite" aria-selected="false">お気に入り</button>
</div>
</nav>
<div class="tab-content mt-3" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-post" role="tabpanel" aria-labelledby="nav-post-tab" tabindex="0"> @include('posts.posts', ['posts' => $posts])</div>
<div class="tab-pane fade" id="nav-follow" role="tabpanel" aria-labelledby="nav-follow-tab" tabindex="0"> @include('users.follows.following', ['id' => $user->id])</div>
<div class="tab-pane fade" id="nav-follower" role="tabpanel" aria-labelledby="nav-follower-tab" tabindex="0">@include('users.follows.followers', ['id' => $user->id])</div>
<div class="tab-pane fade text-secondary" id="nav-favorite" role="tabpanel" aria-labelledby="nav-favorite-tab" tabindex="0">to be continued</div>
</div>
</div>
</div>
</section>
4.テスト実施
・フォロー/フォロー解除時のUIおよびDBの変化を確認(followsテーブルの確認)
・各ナビタブ表示内容が正しいか確認
・ページネーションが正しく動作するかを確認(1ページ10件)
5.PRレビュー
・インデント不備指摘(@if〜@endifの間)
→自動生成で対応不可
→自己確認及びaiにインデント確認を依頼し以後の不備対応
6.本実装に関して
・可読性、保守性:フォロー、フォロー解除、フォローチェックのメソッドをControllerかModelのどちらに記載するべきか悩んだ
→結果として、ドキュメントの「Controllerはロジックを持たずシンプルにすべき」という方針に従い、Modelに集約する形で実装