フォロー、タイムライン機能　(実務未経験状態でLaravelを使用した共同開発　タスク6)

## 0.背景
本記事は、記事「実務未経験状態でLaravelを使用した共同開発」で取り組んだタスクのうち、フォロー、タイムライン機能についてまとめたもの

https://qiita.com/ashi__mo/items/9abff3c5227660dcddba

## 1.仕様内容
・自身以外のユーザ詳細ページからフォロー可能
・フォロー中かどうかがひと目で分かるUI
・各ユーザの投稿をタイムラインタブへ
・各ユーザのフォローリスト／フォロワーリストを表示
・各リストにページネーション（10件ごと）

##### UI(デプロイしたもの)
![スクリーンショット (24).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842139/4b31ce4f-7f48-4a53-93c0-0ab20f64ec33.png)
![スクリーンショット (22).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842139/4695d98b-1c5c-40c3-a7d7-45e36a9fa9d1.png)
![スクリーンショット (23).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842139/3868276d-cf55-42ba-8128-559ecd2972e1.png)
![スクリーンショット (21).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842139/b3138e57-8409-49aa-a080-ab9aec841e77.png)


## 2.タスクの見積もり
##### 期間
・１４日
##### 理由
・フォロー、フォロワーの設計のイメージが出来なかった
・上記に加えて未知のタスクのためエラー、イレギュラー発生及びレビュー、修正期間を想定
##### 実績
・仕様作成完了　４日
・PR完了　４日
・PRレビュー修正　５日
・マージ完了　６日

## 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に集約する形で実装