【API】Laravelでフォロー機能を作る

## テーブル作成

フォロー・フォロワーの状態管理をするため、followsテーブルを作成します。
マイグレーションを作成します。

```
php artisan make:migration create_follows_table
```
マイグレーションファイルを以下のように編集します。
```php:2024_02_04_005729_create_follows_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('follows', function (Blueprint $table) {
            $table->id();
            $table->foreignId('following')->constrained('users')->onDelete('cascade');
            $table->foreignId('followed')->constrained('users')->onDelete('cascade');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('follows');
    }
};

```
followingとfollowedの２つのカラムを追加しただけですが、ここがポイントとなります。
foreignIdを使用した外部キーなので本来は命名法則に従ったカラム名にする必要があります。
usersテーブルに紐づくので、命名法則(テーブル名_id)に従えば"user_id"とすべきです。
しかし、今回フォロー(following)とフォロワー(followed)はどちらもuserテーブルの情報が
紐付きます。となると、命名法則に従えばフォロー＆フォロワーのカラム名がどちらも"user_id"となり
重複することになります。
そこで、今回はfollowingとfollowedという命名法則を無視したユニークな名前にしています。
ここで、ユニークな名前のカラム名でリレーションをするためにconstrained()が活躍します。
大抵は引数は何も書かないことが多いですが今回は引数に"users"を指定しています。
constrained()の引数にリレーションしたいテーブル名を指定することで指定したテーブルと
リレーションが可能になります。
このうよにして今回はユニークな名前のカラム名でリレーションを実現しています。

マイグレーションファイルの編集が済んだらマイグレートしましょう。
```
php artisan migrate
```

## モデルの設定
まずはモデルを作成します。
```
php artisan make:model Follow
```
モデルファイルをを作成できたら、以下のように編集する。
Followモデル特に編集はしませんが、後にコントローラーで使用しますので準備しておきます。

続いて、Userモデルを編集します。
ユーザー情報を返す際に、フォロー・フォロワーの情報をリレーションするようにします。
```php:User.php

class User extends Authenticatable 

    //省略

    //フォローしているユーザー
    public function following()
    {
        return $this->belongsToMany(User::class, 'follows','following', 'followed');
        
        
    }

    //フォローされているユーザー
    public function followed()
    {
        
        return $this->belongsToMany(User::class, 'follows','followed','following');
        
    }


}


```

## コントローラー編集
プロフィール画面でユーザーの情報を返すコントローラーと
フォローの追加・解除のDB操作を行うコントローラーを別々に作成します。

```php:ProfileController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;//登録ユーザーのDBを使用

class ProfileController extends Controller
{
    //
    public function get_user($user_id){

        $user = User::with('following')->with('followed')->findOrFail($user_id);
        return response()->json($user);
    }
}

```
```php:FollowController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Follow;//Followモデルをインポート
use Illuminate\Support\Facades\Auth; // Authファサードを読み込む

class FollowController extends Controller
{
    //フォローしているかどうかの状態確認
    public function check_following($id){

        //自分がフォローしているかどうか検索
        $check = Follow::where('following', Auth::id() )->where('followed', $id);

        if($check->count() == 0):
            //フォローしている場合
            return response()->json(['status' => false]);
        elseif($check->count() != 0):
            //まだフォローしていない場合
            return response()->json(['status' => true]); 
        endif;
            

    }

    //フォローする(中間テーブルをインサート)
    public function following(Request $request){

        //自分がフォローしているかどうか検索
        $check = Follow::where('following', Auth::id())->where('followed', $request->user_id);

        //検索結果が0(まだフォローしていない)場合のみフォローする
        if($check->count() == 0):
            $follow = new Follow;
            $follow->following = Auth::id();
            $follow->followed = $request->user_id;
            $follow->save();
        endif;

    }


    //フォローを外す
    public function unfollowing(Request $request){

        //削除対象のレコードを検索して削除
        $unfollowing = Follow::where('following', Auth::id())->where('followed', $request->user_id)->delete();


    }
}

```

## routeの定義
routeを定義します。下記のように追記して下さい。
また、今回はAPIでの利用という前提ですのでapi.phpに書いてますが
フロントエンドはbladeテンプレート実装したい場合は同じ内容をweb.phpに追記すればOKです。
```php:api.php

//省略


use App\Http\Controllers\ProfileController;//追加
use App\Http\Controllers\FollowController;//追加


//省略


//プロフィール閲覧で使用するユーザー情報の取得
Route::get('/profile/{id}',[ProfileController::class,'get_user']);

//フォロー状態の確認
Route::get('/follow/status/{id}',[FollowController::class,'check_following']);

//フォロー付与
Route::post('/follow/add',[FollowController::class,'following']);

//フォロー解除
Route::post('/follow/remove',[FollowController::class,'unfollowing']);


//省略

```


## 参考：フロントエンド
最後におまけでフロントエンドの実装例です。
自分の場合は、React(TypeScript)で実装しています。
パラメータ付きURlでユーザーのidを送り、それを受け取りプロフィールを表示します。
ユーザー情報にlaravel側でリレーションしているため、フォロー数とフォロワー数を取得できます。
フォロー/フォロー解除のボタンは別ファイルでコンポーネントを作り、Profile.tsxで使用しています。


```react:Profile.tsx
import { FC } from "react";
import axios,{AxiosRequestConfig, AxiosResponse, AxiosError} from 'axios';
import { useParams } from 'react-router-dom';
import { useState, useEffect, useContext } from "react";
import { Following } from "./Following";


export const Profile:FC =()=>{

    const { user_id } = useParams();
    const [user,setUser] = useState<any>();
    const [update,setUpdate] = useState<boolean>(false);

    useEffect(()=>{

        // パラメータ(暗号化されたid)付きでアクセスし、該当データをDBより取得
        axios.get('/api/profile/' + user_id).then((response:any) => { 

            setUser(response.data);
            console.log(response.data);

        }).catch((error) => { 
            console.log('通信エラーが発生しました');
        });
    },[]);

    return(
        <>
            <div className="w-full mt-1 mb-10 p-5 rounded-3xl bg-white text-slate-600">
                {user && (
                    <div className="w-full rounded-3xl bg-white text-slate-600">

                        
                        <div className="text-2xl">{user.name}</div>
                        <Following id={user.id} />
                        <div className="flex">
                            <div className="p-1">
                                <b>{user.following.length}</b> フォロー
                            </div>
                            <div className="p-1">
                                <b>{user.followed.length}</b> フォロワー
                            </div>
                        </div>

                       
                    </div>
                )}
            </div>
        </>
    );
}
```

```react:Following.tsx
import { FC } from "react";
import axios,{AxiosRequestConfig, AxiosResponse, AxiosError} from 'axios';
import { useParams } from 'react-router-dom';
import { useState, useEffect, useContext } from "react";





export const Following:FC<{id:any}> =({id})=>{

    const [status,setStatus] = useState<boolean>();
    const [toggle,setToggle] = useState<boolean>(false);

    //フォロー状態確認
    useEffect(()=>{
        // パラメータidでアクセスし、該当データをDBより取得
        axios.get('/api/follow/status/'+ id).then((response) => { 
            console.log(response.data.status);
            setStatus(response.data.status);
            
        }).catch((error) => { 
            
        });

    },[toggle]);


    //フォロー付与
    const Add = () =>{

        // パラメータ(暗号化されたid)付きでアクセスし、該当データをDBより取得
        axios.post('/api/follow/add',{user_id:id}).then((response) => { 
        
            alert('フォローしました');
            setToggle(true);

        }).catch((error) => { 
            alert('エラー');
        });
    }

    //フォロー付与
    const Remove = () =>{

        // パラメータ(暗号化されたid)付きでアクセスし、該当データをDBより取得
        axios.post('/api/follow/remove',{user_id:id}).then((response) => { 
        
            alert('フォロー解除');
            setToggle(false);

        }).catch((error) => { 
            alert('エラー');
        });
    }

    return(
        <>
            {status ?

                <button 
                    className="bg-gray-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                    onClick={Remove}>
                    フォロー解除
                </button>

            :


                <button 
                    className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                    onClick={Add}>
                    フォロー
                </button>
            }

        </>
    );
}

```