📲

Laravelでフォロー機能を実装する方法

2025/03/09に公開

Laravel でフォロー機能を API 経由で実装し、JavaScript でフォローボタンを制御する方法を解説します。

実行環境

PHP 8.3
Laravel 11
MySQL 8.0
JavaScript (fetch API使用)

上記を使用してフォロー機能を実装していきます。
また、サイトはログインしなければ使用できない仕様になっています。

フォロー処理の流れ

  1. ページにアクセス時にフォロー状況をチェック
    • フォローしていなければ「フォローボタン」を表示。
    • フォローしていれば「フォロー解除ボタン」を表示。
  2. フォローボタンを押すと API を呼び出してフォロー
  3. フォロー解除ボタンを押すと API を呼び出してフォロー解除
  4. ボタンの状態を動的に切り替え
    • フォロー後は「フォロー解除」ボタンを表示。
    • フォロー解除後は「フォロー」ボタンを表示。

使用したコード

blade(一部抜粋)

laravelのフォロー、フォロー解除ボタンがあるファイルです。

<div class="follow-action">
                <button id="follow" class="follow-button {{ $isFollow->value == 'following' ? 'hidden' : '' }}" >
                    フォロー
                </button>
                <button id="unfollow" class="unfollow-button {{ $isFollow->value == 'not_following' ? 'hidden' : '' }}">
                    フォロー中
                </button>
            </div>
<script>
        const loggedInUserId = @json(auth()->id()); //ログイン中のユーザーのidをjsに渡す
        const followTargetUserId = @json($material->posts[0]->user->id); //フォローするユーザーをjsに渡す
        const isFollow = @json($isFollow); //フォロー状況をjsに渡す
    </script>

api.php

apiのルーティングが記載されているファイルです。

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\FollowController;

Route::post('/follow/{loggedInUserId}/{followUser}', [FollowController::class, 'follow'])->name('api.follow');
Route::post('/unfollow/{loggedInUserId}/{followUser}', [FollowController::class, 'unfollow'])->name('api.unfollow');

followController.php

フォロー、フォロー解除メソッドが記載されているファイルです。

<?php

namespace App\Http\Controllers;

use App\Services\FollowService;

class FollowController extends Controller
{

    protected $followService;

    public function __construct(FollowService $followService)
    {
        $this->followService = $followService;
    } 

    public function follow($loggedInUserId, $followUserId)
    {
        $result = $this->followService->follow($loggedInUserId, $followUserId);
        return response()->json($result);
    }

    public function unfollow($loggedInUserId, $followUserId)
    {
        $result = $this->followService->unfollow($loggedInUserId, $followUserId);
        return response()->json($result);
    }
}

FollowService.php

コントローラーの肥大化を防ぐためにサービスを使用します。

<?php

namespace App\Services;

use App\Models\User_follow;
use Illuminate\Support\Facades\Auth;
use Exception;

class FollowService
{
    public function follow($loggedInUserId, $followUserId)
    {
        if ($loggedInUserId == $followUserId) {
            return ['error' => "自分自身をフォローすることはできません"];
        }

        if (User_follow::where('follower_id', $loggedInUserId)->where('following_id', $followUserId)->exists()) {
            return ['error' => "すでにフォロー済みです"];
        }

        try {
            User_follow::create([
                'follower_id' => $loggedInUserId,
                'following_id' => $followUserId
            ]);

            return [
                'success' => true, // 追加
                'message' => 'フォロー処理成功'
            ];
        } catch (\Exception $e) {
            return ['error' => "フォロー処理に失敗しました", 'details' => $e->getMessage()];
        }
    }

    public function unfollow($loggedInUserId, $followUserId)
    {
        // フォロー状態を取得
        $existingFollow = User_follow::where('follower_id', $loggedInUserId)
        ->where('following_id', $followUserId)
        ->first();

        try {
            if ($existingFollow) {
                // フォロー解除(レコード削除)
                $existingFollow->delete();

                return [
                    'success' => true,
                    'message' => 'フォロー解除成功'
                ];
            } else {
                // すでにフォローしていない場合
                return [
                    'success' => false,
                    'message' => 'フォローしていません'
                ];
            }
        } catch (Exception $e) {
            return [
                'error' => "フォロー解除に失敗しました",
                'details' => $e->getMessage()
            ];
        }
    }
}

JavaScript

このファイルにAPIの処理を記載しています。

document.addEventListener("DOMContentLoaded", function () {
    const followButton = document.getElementById("follow");
    const unfollowButton = document.getElementById("unfollow");

    if (!followButton) return;

    followButton.addEventListener("click", function () {

        if (!loggedInUserId) {
            console.error("ログインユーザーIDが取得できません");
            return;
        }

        fetch(`/api/follow/${loggedInUserId}/${followTargetUserId}`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
                "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
            },
            credentials: "include"
        })
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
            return response.json();
        })
        .then(data => {
            console.log("API Response:", data);
            if (data.success) {
                followButton.classList.add("hidden");  // フォローボタンを非表示
                unfollowButton.classList.remove("hidden"); // フォロー解除ボタンを表示
            } else {
                alert("エラーが発生しました");
            }
        })
        
        .catch(error => console.error("Error:", error));

    });

    if (!unfollowButton) return;

    unfollowButton.addEventListener("click", function () {

        if (!loggedInUserId) {
            console.error("ログインユーザーIDが取得できません");
            return;
        }

        fetch(`/api/unfollow/${loggedInUserId}/${followTargetUserId}`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
                "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
            },
            credentials: "include"
        })
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
            return response.json();
        })
        .then(data => {
            console.log("API Response:", data);
            if (data.success) {
                unfollowButton.classList.add("hidden");  // フォローボタンを非表示
                followButton.classList.remove("hidden"); // フォロー解除ボタンを表示
            } else {
                alert("エラーが発生しました");
            }
        })
        
        .catch(error => console.error("Error:", error));
    });
});

データベース

テーブルは以下のSQL文で作成しました。

CREATE TABLE user_follows (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    follower_id BIGINT UNSIGNED NOT NULL,
    following_id BIGINT UNSIGNED NOT NULL,
    FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (following_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY (follower_id, following_id)
);

フォローの動作

bladeのフォローボタンを押したら、APIを使用してコントローラーを介してフォローする人とフォローされる人をデーターベースに登録します。
そして、成功すると処理成功と返します。失敗すると処理失敗を返します。

フォロー解除の動作

フォロー解除ボタン(フォロー中ボタン)を押したら、フォロー同様にAPIを使用してコントローラーを介してフォローする人とフォローされる人をデーターベースに登録します。
そして、成功すると処理成功と返します。失敗すると処理失敗を返します。

おわりに

今回はフォロー機能を実装しました。
X(Twitter)などでフォローするとページ遷移を行わずに実行できます、それにならいAPIを使用して実装しました。また、コードは多くないですが、今後の保守・運用のことを考えてサービスを使用しました。
今後実装していく際にもこれからの保守・運用性のことを考えて実装していきたいと思います。

Discussion